From a0ed517c85a7585b75d158cd9194696c9ab168db Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 4 May 2021 21:01:12 +0530 Subject: [PATCH 001/951] fix: function call to update payment schedule labels --- erpnext/public/js/controllers/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a2b95cb757b..2ac2c46eacb 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1384,7 +1384,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ company_currency, "payment_schedule"); this.frm.set_currency_labels(["payment_amount", "outstanding", "paid_amount"], this.frm.doc.currency, "payment_schedule"); - + var schedule_grid = this.frm.fields_dict["payment_schedule"].grid; $.each(["base_payment_amount", "base_outstanding", "base_paid_amount"], function(i, fname) { if (frappe.meta.get_docfield(schedule_grid.doctype, fname)) @@ -2034,7 +2034,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(r.message && !r.exc) { me.frm.set_value("payment_schedule", r.message); const company_currency = me.get_company_currency(); - this.update_payment_schedule_grid_labels(company_currency); + me.update_payment_schedule_grid_labels(company_currency); } } }) From be66ee9723fa01fd213809c9ddf76daf8ad2d1a5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 5 May 2021 12:19:57 +0530 Subject: [PATCH 002/951] fix: Check if payment schedule exits before updating label --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2ac2c46eacb..9f1ea19b082 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1379,7 +1379,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ update_payment_schedule_grid_labels: function(company_currency) { const me = this; - if (this.frm.fields_dict["payment_schedule"]) { + if (this.frm.doc.payment_schedule.length > 0) { this.frm.set_currency_labels(["base_payment_amount", "base_outstanding", "base_paid_amount"], company_currency, "payment_schedule"); this.frm.set_currency_labels(["payment_amount", "outstanding", "paid_amount"], From daf6c124a98e426fe300f9f0c34f3f1dc78d116c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 5 May 2021 12:28:40 +0530 Subject: [PATCH 003/951] fix: Check if payment schedule exists --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9f1ea19b082..121e9d0cf0e 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1379,7 +1379,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ update_payment_schedule_grid_labels: function(company_currency) { const me = this; - if (this.frm.doc.payment_schedule.length > 0) { + if (this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length > 0) { this.frm.set_currency_labels(["base_payment_amount", "base_outstanding", "base_paid_amount"], company_currency, "payment_schedule"); this.frm.set_currency_labels(["payment_amount", "outstanding", "paid_amount"], From 92cefd565568cf3e24c49f079696d55281071ea3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 6 May 2021 17:03:16 +0530 Subject: [PATCH 004/951] feat(pos): ability to retry on pos closing failure (#25604) * feat(pos): ability to retry on pos closing failure * fix: sider issues * fix: sider issues * fix: mark all queued closing entry as failed * feat: add headline message --- .../pos_closing_entry/pos_closing_entry.js | 104 ++++++++++-------- .../pos_closing_entry/pos_closing_entry.json | 21 +++- .../pos_closing_entry/pos_closing_entry.py | 4 + .../pos_closing_entry_list.js | 1 + .../pos_invoice_merge_log.py | 87 +++++++++++---- erpnext/controllers/status_updater.py | 1 + erpnext/patches.txt | 1 + .../v13_0/set_pos_closing_as_failed.py | 7 ++ 8 files changed, 158 insertions(+), 68 deletions(-) create mode 100644 erpnext/patches/v13_0/set_pos_closing_as_failed.py diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 9ea616f8e77..aa0c53e228b 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -22,7 +22,43 @@ frappe.ui.form.on('POS Closing Entry', { }); if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime()); - if (frm.doc.docstatus === 1) set_html_data(frm); + + frappe.realtime.on('closing_process_complete', async function(data) { + await frm.reload_doc(); + if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) { + frappe.msgprint({ + title: __('POS Closing Failed'), + message: frm.doc.error_message, + indicator: 'orange', + clear: true + }); + } + }); + + set_html_data(frm); + }, + + refresh: function(frm) { + if (frm.doc.docstatus == 1 && frm.doc.status == 'Failed') { + const issue = 'issue'; + frm.dashboard.set_headline( + __('POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.', [issue])); + + $('#jump_to_error').on('click', (e) => { + e.preventDefault(); + frappe.utils.scroll_to( + cur_frm.get_field("error_message").$wrapper, + true, + 30 + ); + }); + + frm.add_custom_button(__('Retry'), function () { + frm.call('retry', {}, () => { + frm.reload_doc(); + }); + }); + } }, pos_opening_entry(frm) { @@ -61,44 +97,24 @@ frappe.ui.form.on('POS Closing Entry', { refresh_fields(frm); set_html_data(frm); } - }) + }); + }, + + before_save: function(frm) { + for (let row of frm.doc.pos_transactions) { + frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => { + cur_frm.doc.grand_total -= flt(doc.grand_total); + cur_frm.doc.net_total -= flt(doc.net_total); + cur_frm.doc.total_quantity -= flt(doc.total_qty); + refresh_payments(doc, cur_frm, 1); + refresh_taxes(doc, cur_frm, 1); + refresh_fields(cur_frm); + set_html_data(cur_frm); + }); + } } }); -cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) { - const removed_row = locals[cdt][cdn]; - - if (!removed_row.pos_invoice) return; - - frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => { - cur_frm.doc.grand_total -= flt(doc.grand_total); - cur_frm.doc.net_total -= flt(doc.net_total); - cur_frm.doc.total_quantity -= flt(doc.total_qty); - refresh_payments(doc, cur_frm, 1); - refresh_taxes(doc, cur_frm, 1); - refresh_fields(cur_frm); - set_html_data(cur_frm); - }); -} - -frappe.ui.form.on('POS Invoice Reference', { - pos_invoice(frm, cdt, cdn) { - const added_row = locals[cdt][cdn]; - - if (!added_row.pos_invoice) return; - - frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - }); - } -}) - frappe.ui.form.on('POS Closing Entry Detail', { closing_amount: (frm, cdt, cdn) => { const row = locals[cdt][cdn]; @@ -177,11 +193,13 @@ function refresh_fields(frm) { } function set_html_data(frm) { - frappe.call({ - method: "get_payment_reconciliation_details", - doc: frm.doc, - callback: (r) => { - frm.get_field("payment_reconciliation_details").$wrapper.html(r.message); - } - }) + if (frm.doc.docstatus === 1 && frm.doc.status == 'Submitted') { + frappe.call({ + method: "get_payment_reconciliation_details", + doc: frm.doc, + callback: (r) => { + frm.get_field("payment_reconciliation_details").$wrapper.html(r.message); + } + }); + } } diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json index a9b91e02a9d..4d6e4a2ba07 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json @@ -30,6 +30,8 @@ "total_quantity", "column_break_16", "taxes", + "failure_description_section", + "error_message", "section_break_14", "amended_from" ], @@ -195,7 +197,7 @@ "fieldtype": "Select", "hidden": 1, "label": "Status", - "options": "Draft\nSubmitted\nQueued\nCancelled", + "options": "Draft\nSubmitted\nQueued\nFailed\nCancelled", "print_hide": 1, "read_only": 1 }, @@ -203,6 +205,21 @@ "fieldname": "period_details_section", "fieldtype": "Section Break", "label": "Period Details" + }, + { + "collapsible": 1, + "collapsible_depends_on": "error_message", + "depends_on": "error_message", + "fieldname": "failure_description_section", + "fieldtype": "Section Break", + "label": "Failure Description" + }, + { + "depends_on": "error_message", + "fieldname": "error_message", + "fieldtype": "Small Text", + "label": "Error", + "read_only": 1 } ], "is_submittable": 1, @@ -212,7 +229,7 @@ "link_fieldname": "pos_closing_entry" } ], - "modified": "2021-02-01 13:47:20.722104", + "modified": "2021-05-05 16:59:49.723261", "modified_by": "Administrator", "module": "Accounts", "name": "POS Closing Entry", diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 1065168a50c..82528728ddc 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -60,6 +60,10 @@ class POSClosingEntry(StatusUpdater): def on_cancel(self): unconsolidate_pos_invoices(closing_entry=self) + @frappe.whitelist() + def retry(self): + consolidate_pos_invoices(closing_entry=self) + def update_opening_entry(self, for_cancel=False): opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry) opening_entry.pos_closing_entry = self.name if not for_cancel else None diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js index 20fd610899e..cffeb4d5351 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js @@ -8,6 +8,7 @@ frappe.listview_settings['POS Closing Entry'] = { "Draft": "red", "Submitted": "blue", "Queued": "orange", + "Failed": "red", "Cancelled": "red" }; diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 4d5472df4b4..bc7874305c0 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -13,8 +13,7 @@ from frappe.model.mapper import map_doc, map_child_doc from frappe.utils.scheduler import is_scheduler_inactive from frappe.core.page.background_jobs.background_jobs import get_info import json - -from six import iteritems +import six class POSInvoiceMergeLog(Document): def validate(self): @@ -239,7 +238,7 @@ def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices() invoice_by_customer = get_invoice_customer_map(invoices) - if len(invoices) >= 1 and closing_entry: + if len(invoices) >= 10 and closing_entry: closing_entry.set_status(update=True, status='Queued') enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry) else: @@ -252,36 +251,68 @@ def unconsolidate_pos_invoices(closing_entry): pluck='name' ) - if len(merge_logs) >= 1: + if len(merge_logs) >= 10: closing_entry.set_status(update=True, status='Queued') enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry) else: cancel_merge_logs(merge_logs, closing_entry) def create_merge_logs(invoice_by_customer, closing_entry=None): - for customer, invoices in iteritems(invoice_by_customer): - merge_log = frappe.new_doc('POS Invoice Merge Log') - merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() - merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None + try: + for customer, invoices in six.iteritems(invoice_by_customer): + merge_log = frappe.new_doc('POS Invoice Merge Log') + merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() + merge_log.customer = customer + merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None - merge_log.set('pos_invoices', invoices) - merge_log.save(ignore_permissions=True) - merge_log.submit() + merge_log.set('pos_invoices', invoices) + merge_log.save(ignore_permissions=True) + merge_log.submit() - if closing_entry: - closing_entry.set_status(update=True, status='Submitted') - closing_entry.update_opening_entry() + if closing_entry: + closing_entry.set_status(update=True, status='Submitted') + closing_entry.db_set('error_message', '') + closing_entry.update_opening_entry() + + except Exception: + frappe.db.rollback() + message_log = frappe.message_log.pop() + error_message = safe_load_json(message_log) + + if closing_entry: + closing_entry.set_status(update=True, status='Failed') + closing_entry.db_set('error_message', error_message) + raise + + finally: + frappe.db.commit() + frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user}) def cancel_merge_logs(merge_logs, closing_entry=None): - for log in merge_logs: - merge_log = frappe.get_doc('POS Invoice Merge Log', log) - merge_log.flags.ignore_permissions = True - merge_log.cancel() + try: + for log in merge_logs: + merge_log = frappe.get_doc('POS Invoice Merge Log', log) + merge_log.flags.ignore_permissions = True + merge_log.cancel() - if closing_entry: - closing_entry.set_status(update=True, status='Cancelled') - closing_entry.update_opening_entry(for_cancel=True) + if closing_entry: + closing_entry.set_status(update=True, status='Cancelled') + closing_entry.db_set('error_message', '') + closing_entry.update_opening_entry(for_cancel=True) + + except Exception: + frappe.db.rollback() + message_log = frappe.message_log.pop() + error_message = safe_load_json(message_log) + + if closing_entry: + closing_entry.set_status(update=True, status='Submitted') + closing_entry.db_set('error_message', error_message) + raise + + finally: + frappe.db.commit() + frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user}) def enqueue_job(job, **kwargs): check_scheduler_status() @@ -314,4 +345,14 @@ def check_scheduler_status(): def job_already_enqueued(job_name): enqueued_jobs = [d.get("job_name") for d in get_info()] if job_name in enqueued_jobs: - return True \ No newline at end of file + return True + +def safe_load_json(message): + JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError + + try: + json_message = json.loads(message).get('message') + except JSONDecodeError: + json_message = message + + return json_message \ No newline at end of file diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 5276da97200..4bb6138e5d7 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -98,6 +98,7 @@ status_map = { ["Draft", None], ["Submitted", "eval:self.docstatus == 1"], ["Queued", "eval:self.status == 'Queued'"], + ["Failed", "eval:self.status == 'Failed'"], ["Cancelled", "eval:self.docstatus == 2"], ] } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index de9f6e31f80..9ef949c2c73 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -774,3 +774,4 @@ erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 erpnext.patches.v13_0.update_shipment_status erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting +erpnext.patches.v13_0.set_pos_closing_as_failed diff --git a/erpnext/patches/v13_0/set_pos_closing_as_failed.py b/erpnext/patches/v13_0/set_pos_closing_as_failed.py new file mode 100644 index 00000000000..1c576db1c7e --- /dev/null +++ b/erpnext/patches/v13_0/set_pos_closing_as_failed.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('accounts', 'doctype', 'pos_closing_entry') + + frappe.db.sql("update `tabPOS Closing Entry` set `status` = 'Failed' where `status` = 'Queued'") \ No newline at end of file From 0e41295c0e6d2f541e161dee9ae1e8d9985311ce Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 6 May 2021 19:14:06 +0530 Subject: [PATCH 005/951] perf: Performance enhancement on setup wizard (#25606) * perf: Performance enhancement on setup wizard * fix: create departments without updating nsm --- erpnext/accounts/doctype/account/account.py | 2 +- .../chart_of_accounts/chart_of_accounts.py | 4 +- .../accounts_settings/accounts_settings.py | 4 +- .../education_settings/education_settings.py | 4 +- erpnext/hr/doctype/department/department.py | 3 +- .../payroll_settings/payroll_settings.py | 4 +- .../selling_settings/selling_settings.py | 4 +- .../global_defaults/global_defaults.py | 12 +- .../doctype/naming_series/naming_series.py | 16 +-- .../operations/install_fixtures.py | 134 ++++++++++-------- erpnext/setup/setup_wizard/setup_wizard.py | 9 -- .../doctype/stock_settings/stock_settings.py | 10 +- 12 files changed, 105 insertions(+), 101 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 06068238213..1be2fbf5c81 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -13,7 +13,7 @@ class BalanceMismatchError(frappe.ValidationError): pass class Account(NestedSet): nsm_parent_field = 'parent_account' def on_update(self): - if frappe.local.flags.ignore_on_update: + if frappe.local.flags.ignore_update_nsm: return else: super(Account, self).on_update() diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 0e3b24cda3d..927adc7086c 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -57,10 +57,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch # Rebuild NestedSet HSM tree for Account Doctype # after all accounts are already inserted. - frappe.local.flags.ignore_on_update = True + frappe.local.flags.ignore_update_nsm = True _import_accounts(chart, None, None, root_account=True) rebuild_tree("Account", "parent_account") - frappe.local.flags.ignore_on_update = False + frappe.local.flags.ignore_update_nsm = False def add_suffix_if_duplicate(account_name, account_number, accounts): if account_number: diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 5593466fc2b..4d3388090dc 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -30,5 +30,5 @@ class AccountsSettings(Document): def enable_payment_schedule_in_print(self): show_in_print = cint(self.show_payment_schedule_in_print) for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"): - make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check") - make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check") + make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False) diff --git a/erpnext/education/doctype/education_settings/education_settings.py b/erpnext/education/doctype/education_settings/education_settings.py index a85d3e70f34..658380ea429 100644 --- a/erpnext/education/doctype/education_settings/education_settings.py +++ b/erpnext/education/doctype/education_settings/education_settings.py @@ -31,9 +31,9 @@ class EducationSettings(Document): def validate(self): from frappe.custom.doctype.property_setter.property_setter import make_property_setter if self.get('instructor_created_by')=='Naming Series': - make_property_setter('Instructor', "naming_series", "hidden", 0, "Check") + make_property_setter('Instructor', "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False) else: - make_property_setter('Instructor', "naming_series", "hidden", 1, "Check") + make_property_setter('Instructor', "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False) def update_website_context(context): context["lms_enabled"] = frappe.get_doc("Education Settings").enable_lms \ No newline at end of file diff --git a/erpnext/hr/doctype/department/department.py b/erpnext/hr/doctype/department/department.py index 2cef5092767..539a360269f 100644 --- a/erpnext/hr/doctype/department/department.py +++ b/erpnext/hr/doctype/department/department.py @@ -31,7 +31,8 @@ class Department(NestedSet): return new def on_update(self): - NestedSet.on_update(self) + if not frappe.local.flags.ignore_update_nsm: + super(Department, self).on_update() def on_trash(self): super(Department, self).on_trash() diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.py b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py index 5efa41db1f7..459b7eacb43 100644 --- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.py +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py @@ -28,5 +28,5 @@ class PayrollSettings(Document): def toggle_rounded_total(self): self.disable_rounded_total = cint(self.disable_rounded_total) - make_property_setter("Salary Slip", "rounded_total", "hidden", self.disable_rounded_total, "Check") - make_property_setter("Salary Slip", "rounded_total", "print_hide", self.disable_rounded_total, "Check") + make_property_setter("Salary Slip", "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter("Salary Slip", "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index d2978838763..b219e7ecce0 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -30,8 +30,8 @@ class SellingSettings(Document): # Make property setters to hide tax_id fields for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"): - make_property_setter(doctype, "tax_id", "hidden", self.hide_tax_id, "Check") - make_property_setter(doctype, "tax_id", "print_hide", self.hide_tax_id, "Check") + make_property_setter(doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False) def set_default_customer_group_and_territory(self): if not self.customer_group: diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index 76a84508291..e5872171815 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -60,11 +60,11 @@ class GlobalDefaults(Document): # Make property setters to hide rounded total fields for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", "Supplier Quotation", "Purchase Order", "Purchase Invoice"): - make_property_setter(doctype, "base_rounded_total", "hidden", self.disable_rounded_total, "Check") - make_property_setter(doctype, "base_rounded_total", "print_hide", 1, "Check") + make_property_setter(doctype, "base_rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "rounded_total", "hidden", self.disable_rounded_total, "Check") - make_property_setter(doctype, "rounded_total", "print_hide", self.disable_rounded_total, "Check") + make_property_setter(doctype, "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) def toggle_in_words(self): self.disable_in_words = cint(self.disable_in_words) @@ -72,5 +72,5 @@ class GlobalDefaults(Document): # Make property setters to hide in words fields for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"): - make_property_setter(doctype, "in_words", "hidden", self.disable_in_words, "Check") - make_property_setter(doctype, "in_words", "print_hide", self.disable_in_words, "Check") + make_property_setter(doctype, "in_words", "hidden", self.disable_in_words, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "in_words", "print_hide", self.disable_in_words, "Check", validate_fields_for_doctype=False) diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index 373b0a58c98..c1f9433b411 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -183,8 +183,8 @@ class NamingSeries(Document): def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True): from frappe.custom.doctype.property_setter.property_setter import make_property_setter if naming_series: - make_property_setter(doctype, "naming_series", "hidden", 0, "Check") - make_property_setter(doctype, "naming_series", "reqd", 1, "Check") + make_property_setter(doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "naming_series", "reqd", 1, "Check", validate_fields_for_doctype=False) # set values for mandatory try: @@ -195,15 +195,15 @@ def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True pass if hide_name_field: - make_property_setter(doctype, fieldname, "reqd", 0, "Check") - make_property_setter(doctype, fieldname, "hidden", 1, "Check") + make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False) else: - make_property_setter(doctype, "naming_series", "reqd", 0, "Check") - make_property_setter(doctype, "naming_series", "hidden", 1, "Check") + make_property_setter(doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False) if hide_name_field: - make_property_setter(doctype, fieldname, "hidden", 0, "Check") - make_property_setter(doctype, fieldname, "reqd", 1, "Check") + make_property_setter(doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False) + make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False) # set values for mandatory frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=`name` where diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 5053c6a5124..f21d55fe214 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -12,6 +12,7 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import up from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates +from frappe.utils.nestedset import rebuild_tree default_lead_sources = ["Existing Customer", "Reference", "Advertisement", "Cold Calling", "Exhibition", "Supplier Reference", "Mass Mailing", @@ -280,13 +281,15 @@ def install(country=None): set_more_defaults() update_global_search_doctypes() - # path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) - # if os.path.exists(path.encode("utf-8")): - # frappe.get_attr("erpnext.regional.{0}.setup.setup_company_independent_fixtures".format(frappe.scrub(country)))() - - def set_more_defaults(): # Do more setup stuff that can be done here with no dependencies + update_selling_defaults() + update_buying_defaults() + update_hr_defaults() + add_uom_data() + update_item_variant_settings() + +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" @@ -296,13 +299,7 @@ def set_more_defaults(): selling_settings.sales_update_frequency = "Each Transaction" selling_settings.save() - add_uom_data() - - # set no copy fields of an item doctype to item variant settings - doc = frappe.get_doc('Item Variant Settings') - doc.set_default_fields() - doc.save() - +def update_buying_defaults(): buying_settings = frappe.get_doc("Buying Settings") buying_settings.supp_master_name = "Supplier Name" buying_settings.po_required = "No" @@ -311,12 +308,19 @@ def set_more_defaults(): buying_settings.allow_multiple_items = 1 buying_settings.save() +def update_hr_defaults(): hr_settings = frappe.get_doc("HR Settings") hr_settings.emp_created_by = "Naming Series" hr_settings.leave_approval_notification_template = _("Leave Approval Notification") hr_settings.leave_status_notification_template = _("Leave Status Notification") hr_settings.save() +def update_item_variant_settings(): + # set no copy fields of an item doctype to item variant settings + doc = frappe.get_doc('Item Variant Settings') + doc.set_default_fields() + doc.save() + def add_uom_data(): # add UOMs uoms = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read()) @@ -327,7 +331,7 @@ def add_uom_data(): "uom_name": _(d.get("uom_name")), "name": _(d.get("uom_name")), "must_be_whole_number": d.get("must_be_whole_number") - }).insert(ignore_permissions=True) + }).db_insert() # bootstrap uom conversion factors uom_conversions = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_conversion_data.json")).read()) @@ -336,7 +340,7 @@ def add_uom_data(): frappe.get_doc({ "doctype": "UOM Category", "category_name": _(d.get("category")) - }).insert(ignore_permissions=True) + }).db_insert() if not frappe.db.exists("UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}): uom_conversion = frappe.get_doc({ @@ -369,8 +373,8 @@ def add_sale_stages(): {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")} ] - - make_records(records) + for sales_stage in records: + frappe.get_doc(sales_stage).db_insert() def install_company(args): records = [ @@ -418,7 +422,14 @@ def install_post_company_fixtures(args=None): {'doctype': 'Department', 'department_name': _('Legal'), 'parent_department': _('All Departments'), 'company': args.company_name}, ] - make_records(records) + # Make root department with NSM updation + make_records(records[:1]) + + frappe.local.flags.ignore_update_nsm = True + make_records(records[1:]) + frappe.local.flags.ignore_update_nsm = False + + rebuild_tree("Department", "parent_department") def install_defaults(args=None): @@ -432,7 +443,15 @@ def install_defaults(args=None): # enable default currency frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) + frappe.db.set_value("Stock Settings", None, "email_footer_address", args.get("company_name")) + set_global_defaults(args) + set_active_domains(args) + update_stock_settings() + update_shopping_cart_settings(args) + create_bank_account(args) + +def set_global_defaults(args): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") current_fiscal_year = frappe.get_all("Fiscal Year")[0] @@ -445,13 +464,10 @@ def install_defaults(args=None): global_defaults.save() - system_settings = frappe.get_doc("System Settings") - system_settings.email_footer_address = args.get("company_name") - system_settings.save() - - domain_settings = frappe.get_single('Domain Settings') - domain_settings.set_active_domains(args.get('domains')) +def set_active_domains(args): + frappe.get_single('Domain Settings').set_active_domains(args.get('domains')) +def update_stock_settings(): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" @@ -463,48 +479,44 @@ def install_defaults(args=None): stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.save() - if args.bank_account: - company_name = args.company_name - bank_account_group = frappe.db.get_value("Account", - {"account_type": "Bank", "is_group": 1, "root_type": "Asset", - "company": company_name}) - if bank_account_group: - bank_account = frappe.get_doc({ - "doctype": "Account", - 'account_name': args.bank_account, - 'parent_account': bank_account_group, - 'is_group':0, - 'company': company_name, - "account_type": "Bank", - }) - try: - doc = bank_account.insert() +def create_bank_account(args): + if not args.bank_account: + return - frappe.db.set_value("Company", args.company_name, "default_bank_account", bank_account.name, update_modified=False) + company_name = args.company_name + bank_account_group = frappe.db.get_value("Account", + {"account_type": "Bank", "is_group": 1, "root_type": "Asset", + "company": company_name}) + if bank_account_group: + bank_account = frappe.get_doc({ + "doctype": "Account", + 'account_name': args.bank_account, + 'parent_account': bank_account_group, + 'is_group':0, + 'company': company_name, + "account_type": "Bank", + }) + try: + doc = bank_account.insert() - except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.bank_account)) - except frappe.DuplicateEntryError: - # bank account same as a CoA entry - pass + frappe.db.set_value("Company", args.company_name, "default_bank_account", bank_account.name, update_modified=False) - # Now, with fixtures out of the way, onto concrete stuff - records = [ - - # Shopping cart: needs price lists - { - "doctype": "Shopping Cart Settings", - "enabled": 1, - 'company': args.company_name, - # uh oh - 'price_list': frappe.db.get_value("Price List", {"selling": 1}), - 'default_customer_group': _("Individual"), - 'quotation_series': "QTN-", - }, - ] - - make_records(records) + except RootNotEditable: + frappe.throw(_("Bank account cannot be named as {0}").format(args.bank_account)) + except frappe.DuplicateEntryError: + # bank account same as a CoA entry + pass +def update_shopping_cart_settings(args): + shopping_cart = frappe.get_doc("Shopping Cart Settings") + shopping_cart.update({ + "enabled": 1, + 'company': args.company_name, + 'price_list': frappe.db.get_value("Price List", {"selling": 1}), + 'default_customer_group': _("Individual"), + 'quotation_series': "QTN-", + }) + shopping_cart.update_single(shopping_cart.get_valid_dict()) def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index e74d837ef5c..f63d2695aa3 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -51,11 +51,6 @@ def get_setup_stages(args=None): 'status': _('Setting defaults'), 'fail_msg': 'Failed to set defaults', 'tasks': [ - { - 'fn': setup_post_company_fixtures, - 'args': args, - 'fail_msg': _("Failed to setup post company fixtures") - }, { 'fn': setup_defaults, 'args': args, @@ -94,9 +89,6 @@ def stage_fixtures(args): def setup_company(args): fixtures.install_company(args) -def setup_post_company_fixtures(args): - fixtures.install_post_company_fixtures(args) - def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) @@ -129,7 +121,6 @@ def login_as_first_user(args): def setup_complete(args=None): stage_fixtures(args) setup_company(args) - setup_post_company_fixtures(args) setup_defaults(args) stage_four(args) fin(args) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 3b9608b8056..2dd7c6f35b8 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -30,7 +30,7 @@ class StockSettings(Document): # show/hide barcode field for name in ["barcode", "barcodes", "scan_barcode"]: frappe.make_property_setter({'fieldname': name, 'property': 'hidden', - 'value': 0 if self.show_barcode_field else 1}) + 'value': 0 if self.show_barcode_field else 1}, validate_fields_for_doctype=False) self.validate_warehouses() self.cant_change_valuation_method() @@ -67,10 +67,10 @@ class StockSettings(Document): self.toggle_warehouse_field_for_inter_warehouse_transfer() def toggle_warehouse_field_for_inter_warehouse_transfer(self): - make_property_setter("Sales Invoice Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check") - make_property_setter("Delivery Note Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check") - make_property_setter("Purchase Invoice Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check") - make_property_setter("Purchase Receipt Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check") + make_property_setter("Sales Invoice Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) + make_property_setter("Delivery Note Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) + make_property_setter("Purchase Invoice Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) + make_property_setter("Purchase Receipt Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) def clean_all_descriptions(): From ae18efaa0aaa2434395eac0cbe3483fc7e82a1e1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 6 May 2021 19:38:17 +0550 Subject: [PATCH 006/951] bumped to version 13.2.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a988d7217db..6775398532b 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.2.0' +__version__ = '13.2.1' def get_default_company(user=None): '''Get default company for user''' From dd1822ef58dc24f32d5bf62d634c886a50d7a386 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 12 May 2021 13:01:53 +0530 Subject: [PATCH 007/951] fix: change links in workspace (#25673) --- .../workspace/accounting/accounting.json | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index 9ffa481c1cb..df68318052f 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -15,6 +15,7 @@ "hide_custom": 0, "icon": "accounting", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Accounting", "links": [ @@ -625,9 +626,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Bank Reconciliation", - "link_to": "bank-reconciliation", - "link_type": "Page", + "label": "Bank Reconciliation Tool", + "link_to": "Bank Reconciliation Tool", + "link_type": "DocType", "onboard": 0, "type": "Link" }, @@ -641,26 +642,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Bank Statement Transaction Entry", - "link_to": "Bank Statement Transaction Entry", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Bank Statement Settings", - "link_to": "Bank Statement Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -1071,7 +1052,7 @@ "type": "Link" } ], - "modified": "2021-03-04 00:38:35.349024", + "modified": "2021-05-12 11:48:01.905144", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", From aaca8335f080dca5078d90212bd810ced5f56d84 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 12 May 2021 16:36:25 +0530 Subject: [PATCH 008/951] fix: updated modified time to pull new fields --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 24e67febca5..d3d3ffa17fa 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1380,7 +1380,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-03-30 22:45:58.334107", + "modified": "2021-04-30 22:45:58.334107", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", From 6578c045ca3d7cb40481caf0b4239420d5ab5cbb Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 12 May 2021 17:41:50 +0530 Subject: [PATCH 009/951] fix: Dialog variable assignment after definition in POS (#25680) --- erpnext/selling/page/point_of_sale/pos_controller.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 8e0a1e1c185..4f4f1b2240b 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -56,10 +56,6 @@ erpnext.PointOfSale.Controller = class { dialog.fields_dict.balance_details.grid.refresh(); }); } - const pos_profile_query = { - query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', - filters: { company: dialog.fields_dict.company.get_value() } - } const dialog = new frappe.ui.Dialog({ title: __('Create POS Opening Entry'), static: true, @@ -105,6 +101,10 @@ erpnext.PointOfSale.Controller = class { primary_action_label: __('Submit') }); dialog.show(); + const pos_profile_query = { + query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', + filters: { company: dialog.fields_dict.company.get_value() } + }; } async prepare_app_defaults(data) { From fe68a0ff80c8d807a5886d46f2d51ad33e6949f1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 12 May 2021 19:42:04 +0530 Subject: [PATCH 010/951] fix: Woocommerce order sync issue --- .../connectors/woocommerce_connection.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 6dedaa8c530..a505ee09d28 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import frappe, base64, hashlib, hmac, json +from frappe.utils import cstr from frappe import _ def verify_request(): @@ -146,22 +147,19 @@ def rename_address(address, customer): def link_items(items_list, woocommerce_settings, sys_lang): for item_data in items_list: - item_woo_com_id = item_data.get("product_id") + item_woo_com_id = cstr(item_data.get("product_id")) - if frappe.get_value("Item", {"woocommerce_id": item_woo_com_id}): - #Edit Item - item = frappe.get_doc("Item", {"woocommerce_id": item_woo_com_id}) - else: + if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, 'name'): #Create Item item = frappe.new_doc("Item") + item.item_code = _("woocommerce - {0}", sys_lang).format(item_woo_com_id) + item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang) + item.item_group = _("WooCommerce Products", sys_lang) - item.item_name = item_data.get("name") - item.item_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id")) - item.woocommerce_id = item_data.get("product_id") - item.item_group = _("WooCommerce Products", sys_lang) - item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang) - item.flags.ignore_mandatory = True - item.save() + item.item_name = item_data.get("name") + item.woocommerce_id = item_woo_com_id + item.flags.ignore_mandatory = True + item.save() def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): new_sales_order = frappe.new_doc("Sales Order") @@ -194,12 +192,12 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l for item in order.get("line_items"): woocomm_item_id = item.get("product_id") - found_item = frappe.get_doc("Item", {"woocommerce_id": woocomm_item_id}) + found_item = frappe.get_doc("Item", {"woocommerce_id": cstr(woocomm_item_id)}) ordered_items_tax = item.get("total_tax") - new_sales_order.append("items",{ - "item_code": found_item.item_code, + new_sales_order.append("items", { + "item_code": found_item.name, "item_name": found_item.item_name, "description": found_item.item_name, "delivery_date": new_sales_order.delivery_date, @@ -207,7 +205,7 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l "qty": item.get("quantity"), "rate": item.get("price"), "warehouse": woocommerce_settings.warehouse or default_warehouse - }) + }) add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account) From 8e748f8451db11e08cc0a920e1d0eea7c75e6a56 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 13 May 2021 14:59:28 +0530 Subject: [PATCH 011/951] fix: Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet --- erpnext/accounts/report/balance_sheet/balance_sheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index 287b8a7484f..26bb44f4f7b 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -135,7 +135,7 @@ def get_report_summary(period_list, asset, liability, equity, provisional_profit # from consolidated financial statement if filters.get('accumulated_in_group_company'): - period_list = get_filtered_list_for_consolidated_report(period_list) + period_list = get_filtered_list_for_consolidated_report(filters, period_list) for period in period_list: key = period if consolidated else period.key From 7c6de1a8ac2ec818a796fe54268d0650d71ffb9b Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Thu, 13 May 2021 17:28:49 +0530 Subject: [PATCH 012/951] fix: bank statement import via google sheet (#25677) * fix: change links in workspace * fix: google sheet bank statement import * chore: quotes * fix: capitalization * fix: typo * chore: add translation --- .../bank_statement_import.js | 1 + .../bank_statement_import.json | 6 +++--- .../bank_statement_import.py | 18 ++++++++++++++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index 3dbd6053441..016f29a7b51 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -239,6 +239,7 @@ frappe.ui.form.on("Bank Statement Import", { "withdrawal", "description", "reference_number", + "bank_account" ], }, }); diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json index 5e913cc2aac..7ffff02850c 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json @@ -146,7 +146,7 @@ }, { "depends_on": "eval:!doc.__islocal && !doc.import_file\n", - "description": "Must be a publicly accessible Google Sheets URL", + "description": "Must be a publicly accessible Google Sheets URL and adding Bank Account column is necessary for importing via Google Sheets", "fieldname": "google_sheets_url", "fieldtype": "Data", "label": "Import from Google Sheets" @@ -202,7 +202,7 @@ ], "hide_toolbar": 1, "links": [], - "modified": "2021-02-10 19:29:59.027325", + "modified": "2021-05-12 14:17:37.777246", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Statement Import", @@ -224,4 +224,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py index 9f41b13f4b6..5f110e2727c 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py @@ -47,6 +47,13 @@ class BankStatementImport(DataImport): def start_import(self): + preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template( + self.import_file, self.google_sheets_url + ) + + if 'Bank Account' not in json.dumps(preview): + frappe.throw(_("Please add the Bank Account column")) + from frappe.core.page.background_jobs.background_jobs import get_info from frappe.utils.scheduler import is_scheduler_inactive @@ -67,6 +74,7 @@ class BankStatementImport(DataImport): data_import=self.name, bank_account=self.bank_account, import_file_path=self.import_file, + google_sheets_url=self.google_sheets_url, bank=self.bank, template_options=self.template_options, now=frappe.conf.developer_mode or frappe.flags.in_test, @@ -90,18 +98,20 @@ def download_errored_template(data_import_name): data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import.export_errored_rows() -def start_import(data_import, bank_account, import_file_path, bank, template_options): +def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options): """This method runs in background job""" update_mapping_db(bank, template_options) data_import = frappe.get_doc("Bank Statement Import", data_import) + file = import_file_path if import_file_path else google_sheets_url - import_file = ImportFile("Bank Transaction", file = import_file_path, import_type="Insert New Records") + import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records") data = import_file.raw_data - add_bank_account(data, bank_account) - write_files(import_file, data) + if import_file_path: + add_bank_account(data, bank_account) + write_files(import_file, data) try: i = Importer(data_import.reference_doctype, data_import=data_import) From a0a88a710ebd989eae8c2918a286961c79e4f65d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 13 May 2021 17:42:06 +0530 Subject: [PATCH 013/951] fix: change today to now to get data for reposting --- .../doctype/repost_item_valuation/repost_item_valuation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3f837805695..63c71891e44 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form, add_to_date, today +from frappe.utils import cint, get_link_to_form, add_to_date, now, today from erpnext.stock.stock_ledger import repost_future_sle from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced from frappe.utils.user import get_users_with_role @@ -127,7 +127,7 @@ def repost_entries(): check_if_stock_and_account_balance_synced(today(), d.name) def get_repost_item_valuation_entries(): - date = add_to_date(today(), hours=-3) + date = add_to_date(now(), hours=-3) return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` WHERE status != 'Completed' and creation <= %s and docstatus = 1 From e9f6c8cdb19b93fb5c9a96e5f0e920032059e1ed Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 14 May 2021 12:34:13 +0530 Subject: [PATCH 014/951] fix: validation message of quality inspection in purchase receipt (#25667) --- erpnext/controllers/stock_controller.py | 3 +-- .../doctype/quality_inspection/test_quality_inspection.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index b14c2745159..41ca404d9b8 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -379,8 +379,7 @@ class StockController(AccountsController): link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) - qa_failed = any([r.status=="Rejected" for r in qa_doc.readings]) - if qa_failed: + if qa_doc.status != 'Accepted': frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") .format(d.idx, d.item_code), QualityInspectionRejectedError) elif qa_required : diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index a7dfc9ee288..56b046a92e1 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -27,10 +27,11 @@ class TestQualityInspection(unittest.TestCase): dn.reload() self.assertRaises(QualityInspectionRejectedError, dn.submit) - frappe.db.set_value("Quality Inspection Reading", {"parent": qa.name}, "status", "Accepted") + frappe.db.set_value("Quality Inspection", qa.name, "status", "Accepted") dn.reload() dn.submit() + qa.reload() qa.cancel() dn.reload() dn.cancel() From ad0b8fdd1e63c01c0819e129940fae5b83924560 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 17 May 2021 10:49:21 +0530 Subject: [PATCH 015/951] chore: Added change log for v13.3.0 --- erpnext/change_log/v13/v13_3_0.md | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 erpnext/change_log/v13/v13_3_0.md diff --git a/erpnext/change_log/v13/v13_3_0.md b/erpnext/change_log/v13/v13_3_0.md new file mode 100644 index 00000000000..016dbb01f4d --- /dev/null +++ b/erpnext/change_log/v13/v13_3_0.md @@ -0,0 +1,73 @@ +# Version 13.3.0 Release Notes + +### Features & Enhancements + +- Purchase receipt creation from purchase invoice ([#25126](https://github.com/frappe/erpnext/pull/25126)) +- New Document Transaction Deletion ([#25354](https://github.com/frappe/erpnext/pull/25354)) +- Employee Referral ([#24997](https://github.com/frappe/erpnext/pull/24997)) +- Add Create Expense Claim button in Delivery Trip ([#25526](https://github.com/frappe/erpnext/pull/25526)) +- Reduced rate of asset depreciation as per IT Act ([#25648](https://github.com/frappe/erpnext/pull/25648)) +- Improve DATEV export ([#25238](https://github.com/frappe/erpnext/pull/25238)) +- Add pick batch button ([#25413](https://github.com/frappe/erpnext/pull/25413)) +- Enable custom field search on POS ([#25421](https://github.com/frappe/erpnext/pull/25421)) +- New check field in subscriptions for (not) submitting invoices ([#25394](https://github.com/frappe/erpnext/pull/25394)) +- Show POS reserved stock in stock projected qty report ([#25593](https://github.com/frappe/erpnext/pull/25593)) +- e-way bill validity field ([#25555](https://github.com/frappe/erpnext/pull/25555)) +- Significant reduction in time taken to save sales documents ([#25475](https://github.com/frappe/erpnext/pull/25475)) + +### Fixes + +- Bank statement import via google sheet ([#25677](https://github.com/frappe/erpnext/pull/25677)) +- Invoices not getting fetched during payment reconciliation ([#25598](https://github.com/frappe/erpnext/pull/25598)) +- Error on applying TDS without party ([#25632](https://github.com/frappe/erpnext/pull/25632)) +- Allow to cancel loan with cancelled repayment entry ([#25507](https://github.com/frappe/erpnext/pull/25507)) +- Can't open general ledger from consolidated financial report ([#25542](https://github.com/frappe/erpnext/pull/25542)) +- Add 'Partially Received' to Status drop-down list in Material Request ([#24857](https://github.com/frappe/erpnext/pull/24857)) +- Updated item filters for material request ([#25531](https://github.com/frappe/erpnext/pull/25531)) +- Added validation in stock entry to check duplicate serial nos ([#25611](https://github.com/frappe/erpnext/pull/25611)) +- Update shopify api version ([#25600](https://github.com/frappe/erpnext/pull/25600)) +- Dialog variable assignment after definition in POS ([#25680](https://github.com/frappe/erpnext/pull/25680)) +- Added tax_types list ([#25587](https://github.com/frappe/erpnext/pull/25587)) +- Include search fields in Project Link field query ([#25505](https://github.com/frappe/erpnext/pull/25505)) +- Item stock levels displaying inconsistently ([#25506](https://github.com/frappe/erpnext/pull/25506)) +- Change today to now to get data for reposting ([#25703](https://github.com/frappe/erpnext/pull/25703)) +- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25700](https://github.com/frappe/erpnext/pull/25700)) +- Minor fixes in loan ([#25546](https://github.com/frappe/erpnext/pull/25546)) +- Fieldname when updating docfield property ([#25516](https://github.com/frappe/erpnext/pull/25516)) +- Use get_serial_nos for splitting ([#25590](https://github.com/frappe/erpnext/pull/25590)) +- Show item's full name on hover over item in POS ([#25554](https://github.com/frappe/erpnext/pull/25554)) +- Stock ledger entry created against draft stock entry ([#25540](https://github.com/frappe/erpnext/pull/25540)) +- Incorrect expense account set in pos invoice ([#25543](https://github.com/frappe/erpnext/pull/25543)) +- Stock balance and batch-wise balance history report showing different closing stock ([#25575](https://github.com/frappe/erpnext/pull/25575)) +- Make strings translatable ([#25521](https://github.com/frappe/erpnext/pull/25521)) +- Serial no changed after saving stock reconciliation ([#25541](https://github.com/frappe/erpnext/pull/25541)) +- Ignore fraction difference while making round off gl entry ([#25438](https://github.com/frappe/erpnext/pull/25438)) +- Sync shopify customer addresses ([#25481](https://github.com/frappe/erpnext/pull/25481)) +- Total stock summary report not working ([#25551](https://github.com/frappe/erpnext/pull/25551)) +- Rename field has not updated value of deposit and withdrawal fields ([#25545](https://github.com/frappe/erpnext/pull/25545)) +- Unexpected keyword argument 'merge_logs' ([#25489](https://github.com/frappe/erpnext/pull/25489)) +- Validation message of quality inspection in purchase receipt ([#25667](https://github.com/frappe/erpnext/pull/25667)) +- Added is_stock_item filter ([#25530](https://github.com/frappe/erpnext/pull/25530)) +- Fetch total stock at company in PO ([#25532](https://github.com/frappe/erpnext/pull/25532)) +- Updated filters for process statement of accounts ([#25384](https://github.com/frappe/erpnext/pull/25384)) +- Incorrect expense account set in pos invoice ([#25571](https://github.com/frappe/erpnext/pull/25571)) +- Client script breaking while settings tax labels ([#25653](https://github.com/frappe/erpnext/pull/25653)) +- Empty payment term column in accounts receivable report ([#25556](https://github.com/frappe/erpnext/pull/25556)) +- Designation insufficient permission on lead doctype. ([#25331](https://github.com/frappe/erpnext/pull/25331)) +- Force https for shopify webhook registration ([#25630](https://github.com/frappe/erpnext/pull/25630)) +- Patch regional fields for old companies ([#25673](https://github.com/frappe/erpnext/pull/25673)) +- Woocommerce order sync issue ([#25692](https://github.com/frappe/erpnext/pull/25692)) +- Allow to receive same serial numbers multiple times ([#25471](https://github.com/frappe/erpnext/pull/25471)) +- Update Allocated amount after Paid Amount is changed in PE ([#25515](https://github.com/frappe/erpnext/pull/25515)) +- Updating Standard Notification's channel field ([#25564](https://github.com/frappe/erpnext/pull/25564)) +- Report summary showing inflated values when values are accumulated in Group Company ([#25577](https://github.com/frappe/erpnext/pull/25577)) +- UI fixes related to overflowing payment section ([#25652](https://github.com/frappe/erpnext/pull/25652)) +- List invoices in Payment Reconciliation Payment ([#25524](https://github.com/frappe/erpnext/pull/25524)) +- Ageing errors in PSOA ([#25490](https://github.com/frappe/erpnext/pull/25490)) +- Prevent spurious defaults for items when making prec from dnote ([#25559](https://github.com/frappe/erpnext/pull/25559)) +- Stock reconciliation getting time out error during submission ([#25557](https://github.com/frappe/erpnext/pull/25557)) +- Timesheet filter date exclusive issue ([#25626](https://github.com/frappe/erpnext/pull/25626)) +- Update cost center in the item table fetched from POS Profile ([#25609](https://github.com/frappe/erpnext/pull/25609)) +- Updated modified time in purchase invoice to pull new fields ([#25678](https://github.com/frappe/erpnext/pull/25678)) +- Stock and Accounts Settings form refactor ([#25534](https://github.com/frappe/erpnext/pull/25534)) +- Payment amount showing in foreign currency ([#25292](https://github.com/frappe/erpnext/pull/25292)) \ No newline at end of file From 2f403f1bcd42e9a8991c604f3a05e94d13dc3b52 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 17 May 2021 10:50:26 +0530 Subject: [PATCH 016/951] fix: renamed change log --- erpnext/change_log/v13/v13.0.2.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 erpnext/change_log/v13/v13.0.2.md diff --git a/erpnext/change_log/v13/v13.0.2.md b/erpnext/change_log/v13/v13.0.2.md deleted file mode 100644 index 2bfbdfcc5db..00000000000 --- a/erpnext/change_log/v13/v13.0.2.md +++ /dev/null @@ -1,7 +0,0 @@ -## Version 13.0.2 Release Notes - -### Fixes -- fix: frappe.whitelist for doc methods ([#25231](https://github.com/frappe/erpnext/pull/25231)) -- fix: incorrect incoming rate for the sales return ([#25306](https://github.com/frappe/erpnext/pull/25306)) -- fix(e-invoicing): validations & tax calculation fixes ([#25314](https://github.com/frappe/erpnext/pull/25314)) -- fix: update scheduler check time ([#25295](https://github.com/frappe/erpnext/pull/25295)) \ No newline at end of file From 9ec0f118005732d41e84ad7e7844c72d0a01db9c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 17 May 2021 10:50:42 +0530 Subject: [PATCH 017/951] fix: renamed change log --- erpnext/change_log/v13/v13_0_2.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 erpnext/change_log/v13/v13_0_2.md diff --git a/erpnext/change_log/v13/v13_0_2.md b/erpnext/change_log/v13/v13_0_2.md new file mode 100644 index 00000000000..2bfbdfcc5db --- /dev/null +++ b/erpnext/change_log/v13/v13_0_2.md @@ -0,0 +1,7 @@ +## Version 13.0.2 Release Notes + +### Fixes +- fix: frappe.whitelist for doc methods ([#25231](https://github.com/frappe/erpnext/pull/25231)) +- fix: incorrect incoming rate for the sales return ([#25306](https://github.com/frappe/erpnext/pull/25306)) +- fix(e-invoicing): validations & tax calculation fixes ([#25314](https://github.com/frappe/erpnext/pull/25314)) +- fix: update scheduler check time ([#25295](https://github.com/frappe/erpnext/pull/25295)) \ No newline at end of file From bc92ecb10f36724a967e4bdc17b09fa119f81ec8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 17 May 2021 11:56:29 +0550 Subject: [PATCH 018/951] bumped to version 13.3.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 6775398532b..ad971e2976c 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.2.1' +__version__ = '13.3.0' def get_default_company(user=None): '''Get default company for user''' From 6368c976c7f1825fb2a11b098b0d4fbc6b6a276e Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 18 May 2021 18:39:35 +0530 Subject: [PATCH 019/951] fix: expected amount in pos closing payments table (#25737) --- .../pos_closing_entry/pos_closing_entry.js | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index aa0c53e228b..8c5a34a0d8e 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -101,15 +101,24 @@ frappe.ui.form.on('POS Closing Entry', { }, before_save: function(frm) { + frm.set_value("grand_total", 0); + frm.set_value("net_total", 0); + frm.set_value("total_quantity", 0); + frm.set_value("taxes", []); + + for (let row of frm.doc.payment_reconciliation) { + row.expected_amount = 0; + } + for (let row of frm.doc.pos_transactions) { frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => { - cur_frm.doc.grand_total -= flt(doc.grand_total); - cur_frm.doc.net_total -= flt(doc.net_total); - cur_frm.doc.total_quantity -= flt(doc.total_qty); - refresh_payments(doc, cur_frm, 1); - refresh_taxes(doc, cur_frm, 1); - refresh_fields(cur_frm); - set_html_data(cur_frm); + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); }); } } @@ -118,7 +127,7 @@ frappe.ui.form.on('POS Closing Entry', { frappe.ui.form.on('POS Closing Entry Detail', { closing_amount: (frm, cdt, cdn) => { const row = locals[cdt][cdn]; - frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount)) + frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount)); } }) @@ -142,28 +151,28 @@ function add_to_pos_transaction(d, frm) { }) } -function refresh_payments(d, frm, remove) { +function refresh_payments(d, frm) { d.payments.forEach(p => { const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); if (payment) { - if (!remove) payment.expected_amount += flt(p.amount); - else payment.expected_amount -= flt(p.amount); + payment.expected_amount += flt(p.amount); + payment.difference = payment.closing_amount - payment.expected_amount; } else { frm.add_child("payment_reconciliation", { mode_of_payment: p.mode_of_payment, opening_amount: 0, - expected_amount: p.amount + expected_amount: p.amount, + closing_amount: 0 }) } }) } -function refresh_taxes(d, frm, remove) { +function refresh_taxes(d, frm) { d.taxes.forEach(t => { const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); if (tax) { - if (!remove) tax.amount += flt(t.tax_amount); - else tax.amount -= flt(t.tax_amount); + tax.amount += flt(t.tax_amount); } else { frm.add_child("taxes", { account_head: t.account_head, From 10085580c86f9fcfb4cc96e5265665b422baa987 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 24 May 2021 17:05:27 +0550 Subject: [PATCH 020/951] bumped to version 13.3.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index ad971e2976c..8d1767497f4 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.3.0' +__version__ = '13.3.1' def get_default_company(user=None): '''Get default company for user''' From 753e5894de1be99c4e1b9a9fbc3c567b4ae2a5a1 Mon Sep 17 00:00:00 2001 From: Anuja P Date: Thu, 27 May 2021 17:33:22 +0530 Subject: [PATCH 021/951] fix: ageing error in PSOA --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 2ad455c48ff..0b0ee904ff9 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -94,7 +94,7 @@ def get_report_pdf(doc, consolidated=True): continue html = frappe.render_template(template_path, \ - {"filters": filters, "data": res, "ageing": ageing[0] if doc.include_ageing else None, + {"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None, "letter_head": letter_head if doc.letter_head else None, "terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms') if doc.terms_and_conditions else None}) From b084f1d3203b247041695fd50fbc27770eff97d2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 28 May 2021 11:57:38 +0530 Subject: [PATCH 022/951] fix(India): Show only company addresses for ITC reversal entry --- .../doctype/journal_entry/regional/india.js | 17 +++++++++++++++++ .../doctype/gstr_3b_report/gstr_3b_report.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 erpnext/accounts/doctype/journal_entry/regional/india.js diff --git a/erpnext/accounts/doctype/journal_entry/regional/india.js b/erpnext/accounts/doctype/journal_entry/regional/india.js new file mode 100644 index 00000000000..75a69ac0cf3 --- /dev/null +++ b/erpnext/accounts/doctype/journal_entry/regional/india.js @@ -0,0 +1,17 @@ +frappe.ui.form.on("Journal Entry", { + refresh: function(frm) { + frm.set_query('company_address', function(doc) { + if(!doc.company) { + frappe.throw(__('Please set Company')); + } + + return { + query: 'frappe.contacts.doctype.address.address.address_query', + filters: { + link_doctype: 'Company', + link_name: doc.company + } + }; + }); + } +}); \ No newline at end of file diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 3ddcc58867e..641520437fb 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -310,7 +310,7 @@ class GSTR3BReport(Document): self.report_dict['sup_details']['osup_det']['txval'] += taxable_value gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') - place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply', '00-Other Territory') + place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: From 2df7f474fabab9440336f48de184c6a27d0119c1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 28 May 2021 21:12:10 +0530 Subject: [PATCH 023/951] fix: use dictionary filter instead of list (#25875) Item query doesn't support list filter anymore. --- erpnext/manufacturing/doctype/work_order/work_order.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index a6086fb88da..3e5a72db9a7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -76,9 +76,9 @@ frappe.ui.form.on("Work Order", { frm.set_query("production_item", function() { return { query: "erpnext.controllers.queries.item_query", - filters:[ - ['is_stock_item', '=',1] - ] + filters: { + "is_stock_item": 1, + } }; }); From 4c94ccc8d8f01715a9a2109c4c9142000fe34c5a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 31 May 2021 10:51:57 +0530 Subject: [PATCH 024/951] chore: Added change log --- erpnext/change_log/v13/v13_4_0.md | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 erpnext/change_log/v13/v13_4_0.md diff --git a/erpnext/change_log/v13/v13_4_0.md b/erpnext/change_log/v13/v13_4_0.md new file mode 100644 index 00000000000..eaf4f762d49 --- /dev/null +++ b/erpnext/change_log/v13/v13_4_0.md @@ -0,0 +1,54 @@ +# Version 13.4.0 Release Notes + +### Features & Enhancements + +- Multiple GST enhancement and fixes ([#25249](https://github.com/frappe/erpnext/pull/25249)) +- Linking supplier with an item group for filtering items ([#25683](https://github.com/frappe/erpnext/pull/25683)) +- Leave Policy Assignment Refactor ([#24327](https://github.com/frappe/erpnext/pull/24327)) +- Dimension-wise Accounts Balance Report ([#25260](https://github.com/frappe/erpnext/pull/25260)) +- Show net values in Party Accounts ([#25714](https://github.com/frappe/erpnext/pull/25714)) +- Add pending qty section to batch/serial selector dialog ([#25519](https://github.com/frappe/erpnext/pull/25519)) +- enhancements in Training Event ([#25782](https://github.com/frappe/erpnext/pull/25782)) +- Refactored timesheet ([#25701](https://github.com/frappe/erpnext/pull/25701)) + +### Fixes + +- Process Statement of Accounts formatting ([#25777](https://github.com/frappe/erpnext/pull/25777)) +- Removed serial no validation for sales invoice ([#25817](https://github.com/frappe/erpnext/pull/25817)) +- Fetch email id from dialog box in pos past order summary ([#25808](https://github.com/frappe/erpnext/pull/25808)) +- Don't map set warehouse from delivery note to purchase receipt ([#25672](https://github.com/frappe/erpnext/pull/25672)) +- Apply permission while selecting projects ([#25765](https://github.com/frappe/erpnext/pull/25765)) +- Error on adding bank account to plaid ([#25658](https://github.com/frappe/erpnext/pull/25658)) +- Set disable rounded total if it is globally enabled ([#25789](https://github.com/frappe/erpnext/pull/25789)) +- Wrong amount on CR side in general ledger report for customer when different account currencies are involved ([#25654](https://github.com/frappe/erpnext/pull/25654)) +- Stock move dialog duplicate submit actions (V13) ([#25486](https://github.com/frappe/erpnext/pull/25486)) +- Cashflow mapper not showing data ([#25815](https://github.com/frappe/erpnext/pull/25815)) +- Ignore rounding diff while importing JV using data import ([#25816](https://github.com/frappe/erpnext/pull/25816)) +- Woocommerce order sync issue ([#25688](https://github.com/frappe/erpnext/pull/25688)) +- Expected amount in pos closing payments table ([#25737](https://github.com/frappe/erpnext/pull/25737)) +- Show only company addresses for ITC reversal entry ([#25867](https://github.com/frappe/erpnext/pull/25867)) +- Timeout error while loading warehouse tree ([#25694](https://github.com/frappe/erpnext/pull/25694)) +- Plaid Withdrawals and Deposits are recorded incorrectly ([#25784](https://github.com/frappe/erpnext/pull/25784)) +- Return case for item with available qty equal to one ([#25760](https://github.com/frappe/erpnext/pull/25760)) +- The status of repost item valuation showing In Progress since long time ([#25754](https://github.com/frappe/erpnext/pull/25754)) +- Updated applicable charges form in landed cost voucher ([#25732](https://github.com/frappe/erpnext/pull/25732)) +- Rearrange buttons for Company DocType ([#25617](https://github.com/frappe/erpnext/pull/25617)) +- Show uom for item in selector dialog ([#25697](https://github.com/frappe/erpnext/pull/25697)) +- Warehouse not found in stock entry ([#25776](https://github.com/frappe/erpnext/pull/25776)) +- Use dictionary filter instead of list (bp #25874 pre-release) ([#25875](https://github.com/frappe/erpnext/pull/25875)) +- Send emails on rfq submit ([#25695](https://github.com/frappe/erpnext/pull/25695)) +- Cannot bypass e-invoicing for non gst item invoices ([#25759](https://github.com/frappe/erpnext/pull/25759)) +- Validation message of quality inspection in purchase receipt ([#25666](https://github.com/frappe/erpnext/pull/25666)) +- Dialog variable assignment after definition in POS ([#25681](https://github.com/frappe/erpnext/pull/25681)) +- Wrong quantity after transaction for parallel stock transactions ([#25779](https://github.com/frappe/erpnext/pull/25779)) +- Item Variant Details Report ([#25797](https://github.com/frappe/erpnext/pull/25797)) +- Duplicate stock entry on multiple click ([#25742](https://github.com/frappe/erpnext/pull/25742)) +- Bank statement import via google sheet ([#25676](https://github.com/frappe/erpnext/pull/25676)) +- Change today to now to get data for reposting ([#25702](https://github.com/frappe/erpnext/pull/25702)) +- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25698](https://github.com/frappe/erpnext/pull/25698)) +- Ageing error in PSOA ([#25857](https://github.com/frappe/erpnext/pull/25857)) +- Breaking cost center validation ([#25660](https://github.com/frappe/erpnext/pull/25660)) +- Project filter for Kanban Board ([#25744](https://github.com/frappe/erpnext/pull/25744)) +- Show allow zero valuation only when auto checked ([#25778](https://github.com/frappe/erpnext/pull/25778)) +- Missing cost center message on creating gl entries ([#25755](https://github.com/frappe/erpnext/pull/25755)) +- Address template with upper filter throws jinja error ([#25756](https://github.com/frappe/erpnext/pull/25756)) From c63e233bc7d60ae71f211ec79747ab39eaa827aa Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 31 May 2021 11:34:33 +0550 Subject: [PATCH 025/951] bumped to version 13.4.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 8d1767497f4..20097ef066e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.3.1' +__version__ = '13.4.0' def get_default_company(user=None): '''Get default company for user''' From 5dd92934aef8dfe89555560ff5b9680596d33bff Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 2 Jun 2021 14:13:09 +0530 Subject: [PATCH 026/951] fix: not able to select the item code in work order --- erpnext/controllers/queries.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 46301b7587d..638503edfa9 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -204,7 +204,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if "description" in searchfields: searchfields.remove("description") - + columns = '' extra_searchfields = [field for field in searchfields if not field in ["name", "item_group", "description"]] @@ -216,9 +216,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if not field in searchfields] searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) - if filters.get('supplier'): - item_group_list = frappe.get_all('Supplier Item Group', filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) - + if filters and isinstance(filters, dict) and filters.get('supplier'): + item_group_list = frappe.get_all('Supplier Item Group', + filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) + item_groups = [] for i in item_group_list: item_groups.append(i.item_group) @@ -227,7 +228,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if item_groups: filters['item_group'] = ['in', item_groups] - + description_cond = '' if frappe.db.count('Item', cache=True) < 50000: # scan description only if items are less than 50000 From 8821d71094729ba0ce576414486d0fcf91035f0e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 2 Jun 2021 22:34:19 +0550 Subject: [PATCH 027/951] bumped to version 13.4.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 20097ef066e..5808090db35 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.4.0' +__version__ = '13.4.1' def get_default_company(user=None): '''Get default company for user''' From 4e10ce163263b750f02091f2ad236681e34deffe Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 27 May 2021 17:05:36 +0530 Subject: [PATCH 028/951] fix: timeout error in the repost item valuation --- erpnext/hooks.py | 4 +++- .../repost_item_valuation/repost_item_valuation.py | 13 ++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 55169dffbad..8ad77a1524d 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -332,7 +332,9 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", - "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", + "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders" + ], + "hourly_long": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" ], "daily": [ 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 5b626ea3458..55f2ebb2241 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -37,6 +37,9 @@ class RepostItemValuation(Document): self.db_set('status', status) def on_submit(self): + if not frappe.flags.in_test: + return + frappe.enqueue(repost, timeout=1800, queue='long', job_name='repost_sle', now=frappe.flags.in_test, doc=self) @@ -115,12 +118,6 @@ def notify_error_to_stock_managers(doc, traceback): frappe.sendmail(recipients=recipients, subject=subject, message=message) def repost_entries(): - job_log = frappe.get_all('Scheduled Job Log', fields = ['status', 'creation'], - filters = {'scheduled_job_type': 'repost_item_valuation.repost_entries'}, order_by='creation desc', limit=1) - - if job_log and job_log[0]['status'] == 'Start' and time_diff_in_hours(now(), job_log[0]['creation']) < 2: - return - riv_entries = get_repost_item_valuation_entries() for row in riv_entries: @@ -135,9 +132,7 @@ def repost_entries(): check_if_stock_and_account_balance_synced(today(), d.name) def get_repost_item_valuation_entries(): - date = add_to_date(now(), hours=-3) - return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` WHERE status != 'Completed' and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, date, as_dict=1) + """, now(), as_dict=1) From 8449a4704897d090f4277d895533630ea3bb0c2d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 3 Jun 2021 17:57:01 +0530 Subject: [PATCH 029/951] fix: filter type for item query --- erpnext/controllers/queries.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 638503edfa9..81ac234e700 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe import erpnext +import json from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.utils import nowdate, getdate from collections import defaultdict @@ -198,6 +199,9 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): conditions = [] + if isinstance(filters, str): + filters = json.loads(filters) + #Get searchfields from meta and use in Item Link field query meta = frappe.get_meta("Item", cached=True) searchfields = meta.get_search_fields() From efd7d584b2a50ddc4f95aaf34515e95ab683838b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sat, 12 Jun 2021 13:33:21 +0530 Subject: [PATCH 030/951] feat: add Inactive status to Employee --- erpnext/accounts/party.py | 2 +- erpnext/hr/doctype/employee/employee.json | 4 ++-- erpnext/hr/doctype/employee/employee.py | 4 ++-- erpnext/hr/doctype/employee/employee_list.js | 2 +- erpnext/hr/doctype/employee_promotion/employee_promotion.py | 6 +++--- erpnext/hr/doctype/employee_transfer/employee_transfer.py | 6 +++--- erpnext/payroll/doctype/retention_bonus/retention_bonus.py | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e01cb6e151e..e025fc69054 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -457,7 +457,7 @@ def validate_party_frozen_disabled(party_type, party_name): frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen) elif party_type == "Employee": - if frappe.db.get_value("Employee", party_name, "status") == "Left": + if frappe.db.get_value("Employee", party_name, "status") != "Active": frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True) def get_timeline_data(doctype, name): diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 5123d6a5a78..5442ed56c31 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -207,7 +207,7 @@ "label": "Status", "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Active\nLeft", + "options": "Active\nInactive\nLeft", "reqd": 1, "search_index": 1 }, @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2021-01-02 16:54:33.477439", + "modified": "2021-06-12 11:31:37.730760", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index ed7d5884347..bc5694226ad 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -37,7 +37,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Active", "Temporary Leave", "Left"]) + validate_status(self.status, ["Active", "Inactive", "Left"]) self.employee = self.name self.set_employee_name() @@ -478,7 +478,7 @@ def get_employee_emails(employee_list): @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [['status', '!=', 'Left']] + filters = [['status', '=', 'Active']] if company and company != 'All Companies': filters.append(['company', '=', company]) diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js index 44837030be8..6679e318c24 100644 --- a/erpnext/hr/doctype/employee/employee_list.js +++ b/erpnext/hr/doctype/employee/employee_list.js @@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = { filters: [["status","=", "Active"]], get_indicator: function(doc) { var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; - indicator[1] = {"Active": "green", "Temporary Leave": "red", "Left": "gray"}[doc.status]; + indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; return indicator; } }; diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py index 49949212689..83fb235f92c 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee class EmployeePromotion(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") == "Left": - frappe.throw(_("Cannot promote Employee with status Left")) + if frappe.get_value("Employee", self.employee, "status") != "Active": + frappe.throw(_("Cannot promote Employee with status Left or Inactive")) def before_submit(self): if getdate(self.promotion_date) > getdate(): - frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date "), + frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date"), frappe.DocstatusTransitionError) def on_submit(self): diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py index 3539970a32a..6eec9fa12a9 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee class EmployeeTransfer(Document): def validate(self): - if frappe.get_value("Employee", self.employee, "status") == "Left": - frappe.throw(_("Cannot transfer Employee with status Left")) + if frappe.get_value("Employee", self.employee, "status") != "Active": + frappe.throw(_("Cannot transfer Employee with status Left or Inactive")) def before_submit(self): if getdate(self.transfer_date) > getdate(): - frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date "), + frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), frappe.DocstatusTransitionError) def on_submit(self): diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py index b8e56ae42aa..049ea265cce 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -10,8 +10,8 @@ from frappe.utils import getdate class RetentionBonus(Document): def validate(self): - if frappe.get_value('Employee', self.employee, 'status') == 'Left': - frappe.throw(_('Cannot create Retention Bonus for left Employees')) + if frappe.get_value('Employee', self.employee, 'status') != 'Active': + frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees')) if getdate(self.bonus_payment_date) < getdate(): frappe.throw(_('Bonus Payment Date cannot be a past date')) From 302855e160e60dc7b6de7120380f5f7e4643efb5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 11:16:39 +0530 Subject: [PATCH 031/951] fix: Auto tax calculations in Payment Entry --- .../doctype/payment_entry/payment_entry.js | 86 +++++++++++++++++-- .../doctype/payment_entry/payment_entry.py | 44 +++++----- .../purchase_taxes_and_charges.json | 3 +- .../sales_taxes_and_charges.json | 3 +- erpnext/controllers/accounts_controller.py | 34 ++++---- 5 files changed, 118 insertions(+), 52 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 939f3546ff6..d3ac3a66760 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1087,6 +1087,8 @@ frappe.ui.form.on('Payment Entry', { initialize_taxes: function(frm) { $.each(frm.doc["taxes"] || [], function(i, tax) { + frm.events.validate_taxes_and_charges(tax); + frm.events.validate_inclusive_tax(tax); tax.item_wise_tax_detail = {}; let tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]; @@ -1101,6 +1103,73 @@ frappe.ui.form.on('Payment Entry', { }); }, + validate_taxes_and_charges: function(d) { + let msg = ""; + + if (d.account_head && !d.description) { + // set description from account head + d.description = d.account_head.split(' - ').slice(0, -1).join(' - '); + } + + if (!d.charge_type && (d.row_id || d.rate || d.tax_amount)) { + msg = __("Please select Charge Type first"); + d.row_id = ""; + d.rate = d.tax_amount = 0.0; + } else if ((d.charge_type == 'Actual' || d.charge_type == 'On Net Total' || d.charge_type == 'On Paid Amount') && d.row_id) { + msg = __("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"); + d.row_id = ""; + } else if ((d.charge_type == 'On Previous Row Amount' || d.charge_type == 'On Previous Row Total') && d.row_id) { + if (d.idx == 1) { + msg = __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"); + d.charge_type = ''; + } else if (!d.row_id) { + msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]); + d.row_id = ""; + } else if (d.row_id && d.row_id >= d.idx) { + msg = __("Cannot refer row number greater than or equal to current row number for this Charge type"); + d.row_id = ""; + } + } + if (msg) { + frappe.validated = false; + refresh_field("taxes"); + frappe.throw(msg); + } + + }, + + validate_inclusive_tax: function(tax) { + let actual_type_error = function() { + let msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx]) + frappe.throw(msg); + }; + + let on_previous_row_error = function(row_range) { + let msg = __("For row {0} in {1}. To include {2} in Item rate, rows {3} must also be included", + [tax.idx, __(tax.doctype), tax.charge_type, row_range]) + frappe.throw(msg); + }; + + if(cint(tax.included_in_paid_amount)) { + if(tax.charge_type == "Actual") { + // inclusive tax cannot be of type Actual + actual_type_error(); + } else if(tax.charge_type == "On Previous Row Amount" && + !cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_paid_amount) + ) { + // referred row should also be an inclusive tax + on_previous_row_error(tax.row_id); + } else if(tax.charge_type == "On Previous Row Total") { + let taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id), + function(t) { return cint(t.included_in_paid_amount) ? null : t; }); + if(taxes_not_included.length > 0) { + // all rows above this tax should be inclusive + on_previous_row_error(tax.row_id == 1 ? "1" : "1 - " + tax.row_id); + } + } + } + }, + determine_exclusive_rate: function(frm) { let has_inclusive_tax = false; $.each(frm.doc["taxes"] || [], function(i, row) { @@ -1110,8 +1179,7 @@ frappe.ui.form.on('Payment Entry', { let cumulated_tax_fraction = 0.0; $.each(frm.doc["taxes"] || [], function(i, tax) { - let current_tax_fraction = frm.events.get_current_tax_fraction(frm, tax); - tax.tax_fraction_for_current_item = current_tax_fraction[0]; + tax.tax_fraction_for_current_item = frm.events.get_current_tax_fraction(frm, tax); if(i==0) { tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item; @@ -1132,9 +1200,7 @@ frappe.ui.form.on('Payment Entry', { if(cint(tax.included_in_paid_amount)) { let tax_rate = tax.rate; - if (tax.charge_type == "Actual") { - current_tax_fraction = tax.tax_amount/(frm.doc.paid_amount_after_tax + frm.doc.tax_amount); - } else if(tax.charge_type == "On Paid Amount") { + if(tax.charge_type == "On Paid Amount") { current_tax_fraction = (tax_rate / 100.0); } else if(tax.charge_type == "On Previous Row Amount") { current_tax_fraction = (tax_rate / 100.0) * @@ -1147,7 +1213,6 @@ frappe.ui.form.on('Payment Entry', { if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") { current_tax_fraction *= -1; - inclusive_tax_amount_per_qty *= -1; } return current_tax_fraction; }, @@ -1207,10 +1272,8 @@ frappe.ui.form.on('Payment Entry', { frappe.throw( __("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row")); } - if (!tax.row_id) { - tax.row_id = tax.idx - 1; - } } + if(tax.charge_type == "Actual") { current_tax_amount = flt(tax.tax_amount, precision("tax_amount", tax)) } else if(tax.charge_type == "On Paid Amount") { @@ -1296,6 +1359,11 @@ frappe.ui.form.on('Advance Taxes and Charges', { included_in_paid_amount: function(frm) { frm.events.apply_taxes(frm); frm.events.set_unallocated_amount(frm); + }, + + charge_type: function(frm) { + frm.events.apply_taxes(frm); + frm.events.set_unallocated_amount(frm); } }) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index edca210142a..bd6f84cd75f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -16,9 +16,11 @@ from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_ac from erpnext.controllers.accounts_controller import AccountsController, get_supplier_block_status from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details - from six import string_types, iteritems +from erpnext.controllers.accounts_controller import validate_conversion_rate, \ + validate_taxes_and_charges, validate_inclusive_tax + class InvalidPaymentEntry(ValidationError): pass @@ -407,20 +409,6 @@ class PaymentEntry(AccountsController): net_total = self.paid_amount included_in_paid_amount = 0 - if self.get('references'): - for doc in self.get('references'): - if doc.reference_doctype == 'Purchase Order': - reference_doclist.append(doc.reference_name) - - if reference_doclist: - order_amount = frappe.db.get_all('Purchase Order', fields=['sum(net_total)'], - filters = {'name': ('in', reference_doclist), 'docstatus': 1, - 'apply_tds': 1}, as_list=1) - - if order_amount: - net_total = order_amount[0][0] - included_in_paid_amount = 1 - # Adding args as purchase invoice to get TDS amount args = frappe._dict({ 'company': self.company, @@ -719,9 +707,9 @@ class PaymentEntry(AccountsController): if account_currency != self.company_currency: frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency)) - if (self.payment_type == 'Pay' and self.advance_tax_account) or self.payment_type == 'Receive': + if self.payment_type == 'Pay': dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" - elif (self.payment_type == 'Receive' and self.advance_tax_account) or self.payment_type == 'Pay': + elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" payment_or_advance_account = self.get_party_account_for_taxes() @@ -747,6 +735,8 @@ class PaymentEntry(AccountsController): if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, + "party_type": self.party_type, + "party": self.party }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -770,9 +760,9 @@ class PaymentEntry(AccountsController): def get_party_account_for_taxes(self): if self.advance_tax_account: return self.advance_tax_account - elif self.payment_type == 'Pay': - return self.paid_from elif self.payment_type == 'Receive': + return self.paid_from + elif self.payment_type == 'Pay': return self.paid_to def update_advance_paid(self): @@ -823,6 +813,9 @@ class PaymentEntry(AccountsController): def initialize_taxes(self): for tax in self.get("taxes"): + validate_taxes_and_charges(tax) + validate_inclusive_tax(tax, self) + tax_fields = ["total", "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] if tax.charge_type != "Actual": @@ -918,15 +911,11 @@ class PaymentEntry(AccountsController): if cint(tax.included_in_paid_amount): tax_rate = tax.rate - if tax.charge_type == 'Actual': - current_tax_fraction = tax.tax_amount/ (self.paid_amount_after_tax + tax.tax_amount) - elif tax.charge_type == "On Paid Amount": + if tax.charge_type == "On Paid Amount": current_tax_fraction = tax_rate / 100.0 - elif tax.charge_type == "On Previous Row Amount": current_tax_fraction = (tax_rate / 100.0) * \ self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item - elif tax.charge_type == "On Previous Row Total": current_tax_fraction = (tax_rate / 100.0) * \ self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item @@ -1626,6 +1615,13 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta paid_amount = received_amount * doc.get('conversion_rate', 1) if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) + + if dt == "Purchase Order" and doc.apply_tds: + if party_account_currency == bank.account_currency: + paid_amount = received_amount = doc.base_net_total + else: + paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) + return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 9b07645ccc9..1fa68e0a8a8 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -218,6 +218,7 @@ }, { "default": "0", + "depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry", "fieldname": "included_in_paid_amount", "fieldtype": "Check", @@ -227,7 +228,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:48:25.335733", + "modified": "2021-06-14 01:43:50.750455", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 170d34e651c..1b7a0fe562e 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -195,6 +195,7 @@ }, { "default": "0", + "depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)", "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry", "fieldname": "included_in_paid_amount", "fieldtype": "Check", @@ -205,7 +206,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-09 11:48:04.691596", + "modified": "2021-06-14 01:44:36.899147", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 53ded33b6f8..a86c8e5909d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1192,7 +1192,7 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c def validate_taxes_and_charges(tax): - if tax.charge_type in ['Actual', 'On Net Total'] and tax.row_id: + if tax.charge_type in ['Actual', 'On Net Total', 'On Paid Amount'] and tax.row_id: frappe.throw(_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'")) elif tax.charge_type in ['On Previous Row Amount', 'On Previous Row Total']: if cint(tax.idx) == 1: @@ -1209,23 +1209,23 @@ def validate_taxes_and_charges(tax): def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): - throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, - row_range)) + throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) - if cint(getattr(tax, "included_in_print_rate", None)): - if tax.charge_type == "Actual": - # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate): - # referred row should also be inclusive - _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]): - # all rows about the reffered tax should be inclusive - _on_previous_row_error("1 - %d" % (tax.row_id,)) - elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + for fieldname in ['included_in_print_rate', 'included_in_paid_amount']: + if cint(getattr(tax, fieldname, None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].get(fieldname)): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.get(fieldname) for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): From 5ef9a629175f4a5743b210fa5d7deabf703ef6a6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 14:34:44 +0530 Subject: [PATCH 032/951] fix: Add separate function to validate payment entry taxes --- .../doctype/payment_entry/payment_entry.py | 22 ++++++++++++-- erpnext/controllers/accounts_controller.py | 29 +++++++++---------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bd6f84cd75f..bd95726035f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -18,8 +18,7 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from six import string_types, iteritems -from erpnext.controllers.accounts_controller import validate_conversion_rate, \ - validate_taxes_and_charges, validate_inclusive_tax +from erpnext.controllers.accounts_controller import validate_taxes_and_charges class InvalidPaymentEntry(ValidationError): pass @@ -925,6 +924,25 @@ class PaymentEntry(AccountsController): return current_tax_fraction +def validate_inclusive_tax(tax, doc): + def _on_previous_row_error(row_range): + throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) + + if cint(getattr(tax, "included_in_paid_amount", None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + @frappe.whitelist() def get_outstanding_reference_documents(args): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a86c8e5909d..02d1c7680b0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1211,21 +1211,20 @@ def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range)) - for fieldname in ['included_in_print_rate', 'included_in_paid_amount']: - if cint(getattr(tax, fieldname, None)): - if tax.charge_type == "Actual": - # inclusive tax cannot be of type Actual - throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) - elif tax.charge_type == "On Previous Row Amount" and \ - not cint(doc.get("taxes")[cint(tax.row_id) - 1].get(fieldname)): - # referred row should also be inclusive - _on_previous_row_error(tax.row_id) - elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.get(fieldname) for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): - # all rows about the referred tax should be inclusive - _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) - elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not be marked as Inclusive")) + if cint(getattr(tax, "included_in_print_rate", None)): + if tax.charge_type == "Actual": + # inclusive tax cannot be of type Actual + throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx)) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate): + # referred row should also be inclusive + _on_previous_row_error(tax.row_id) + elif tax.charge_type == "On Previous Row Total" and \ + not all([cint(t.included_in_print_rate for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + # all rows about the referred tax should be inclusive + _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + elif tax.get("category") == "Valuation": + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): From ac52daa14f97d98670ece65444977b239686fe8c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 14:44:19 +0530 Subject: [PATCH 033/951] fix: Import throw --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bd95726035f..b1953329807 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext, json -from frappe import _, scrub, ValidationError +from frappe import _, scrub, ValidationError, throw from frappe.utils import flt, comma_or, nowdate, getdate, cint from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency, get_balance_on from erpnext.accounts.party import get_party_account From bbf6121bb5f251917e1052e60f74ce170f591614 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 14 Jun 2021 20:01:04 +0530 Subject: [PATCH 034/951] fix: Revert unintended changes --- erpnext/controllers/accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 02d1c7680b0..e4559825177 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1220,9 +1220,9 @@ def validate_inclusive_tax(tax, doc): # referred row should also be inclusive _on_previous_row_error(tax.row_id) elif tax.charge_type == "On Previous Row Total" and \ - not all([cint(t.included_in_print_rate for t in doc.get("taxes")[:cint(tax.row_id) - 1])]): + not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]): # all rows about the referred tax should be inclusive - _on_previous_row_error("1 - %d" % (cint(tax.row_id),)) + _on_previous_row_error("1 - %d" % (tax.row_id,)) elif tax.get("category") == "Valuation": frappe.throw(_("Valuation type charges can not be marked as Inclusive")) From 4afda3c89c2fe06f0ab91604fd8e00c39f88101f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 1 Jun 2021 13:13:04 +0530 Subject: [PATCH 035/951] fix(India): Taxable value for invoices with additional discount --- erpnext/regional/india/e_invoice/utils.py | 30 ++++++----------------- erpnext/regional/india/utils.py | 8 ++---- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 0eaf7905381..11ebef724c4 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -38,7 +38,7 @@ def validate_eligibility(doc): einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): return False - + invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') @@ -135,7 +135,7 @@ def validate_address_fields(address, is_shipping_address): def get_party_details(address_name, is_shipping_address=False): addr = frappe.get_doc('Address', address_name) - + validate_address_fields(addr, is_shipping_address) if addr.gst_state_number == 97: @@ -188,11 +188,6 @@ def get_item_list(invoice): item.qty = abs(item.qty) - if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - item.discount_amount = abs(item.base_amount - item.base_net_amount) - else: - item.discount_amount = 0 - item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) item.gross_amount = abs(item.taxable_value) + item.discount_amount item.taxable_value = abs(item.taxable_value) @@ -254,18 +249,8 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - - if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: - # Discount already applied on net total which means on items - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - invoice_value_details.invoice_discount_amt = 0 - elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount: - invoice_value_details.invoice_discount_amt = invoice.base_discount_amount - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - else: - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) - # since tax already considers discount amount - invoice_value_details.invoice_discount_amt = 0 + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.invoice_discount_amt = 0 invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) @@ -287,8 +272,7 @@ def update_invoice_taxes(invoice, invoice_value_details): considered_rows = [] for t in invoice.taxes: - tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \ - else t.base_tax_amount_after_discount_amount + tax_amount = t.base_tax_amount_after_discount_amount if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: # using after discount amt since item also uses after discount amt for cess calc @@ -995,7 +979,7 @@ class GSPConnector(): self.invoice.failure_description = self.get_failure_message(errors) if errors else "" self.update_invoice() frappe.db.commit() - + def get_failure_message(self, errors): if isinstance(errors, list): errors = ', '.join(errors) @@ -1052,7 +1036,7 @@ def generate_einvoices(docnames): _('{} e-invoices generated successfully').format(success), title=_('Bulk E-Invoice Generation Complete') ) - + else: enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 075c698fead..a4466e78f28 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -817,12 +817,8 @@ def update_taxable_values(doc, method): considered_rows.append(prev_row_id) for item in doc.get('items'): - if doc.apply_discount_on == 'Grand Total' and doc.discount_amount: - proportionate_value = item.base_amount if doc.base_total else item.qty - total_value = doc.base_total if doc.base_total else doc.total_qty - else: - proportionate_value = item.base_net_amount if doc.base_net_total else item.qty - total_value = doc.base_net_total if doc.base_net_total else doc.total_qty + proportionate_value = item.base_net_amount if doc.base_net_total else item.qty + total_value = doc.base_net_total if doc.base_net_total else doc.total_qty applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), item.precision('taxable_value'))) From 433815dabaf7b1c7219de663c180c1fa7bd384d8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Jun 2021 12:04:30 +0530 Subject: [PATCH 036/951] fix: Update einvoice json test --- .../sales_invoice/test_sales_invoice.py | 98 ++++++++----------- 1 file changed, 41 insertions(+), 57 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5010fdc2474..114b7d2d352 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1937,69 +1937,53 @@ class TestSalesInvoice(unittest.TestCase): frappe.flags.country = country def test_einvoice_json(self): - from erpnext.regional.india.e_invoice.utils import make_einvoice + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals - si = make_sales_invoice_for_ewaybill() - si.naming_series = 'INV-2020-.#####' - si.items = [] - si.append("items", { - "item_code": "_Test Item", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 2000, - "rate": 12, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) - si.append("items", { - "item_code": "_Test Item 2", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 420, - "rate": 15, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) + si = get_sales_invoice_for_e_invoice() si.discount_amount = 100 si.save() einvoice = make_einvoice(si) - - total_item_ass_value = 0 - total_item_cgst_value = 0 - total_item_sgst_value = 0 - total_item_igst_value = 0 - total_item_value = 0 - - for item in einvoice['ItemList']: - total_item_ass_value += item['AssAmt'] - total_item_cgst_value += item['CgstAmt'] - total_item_sgst_value += item['SgstAmt'] - total_item_igst_value += item['IgstAmt'] - total_item_value += item['TotItemVal'] - - self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount']) - self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt']) - - value_details = einvoice['ValDtls'] - - self.assertEqual(einvoice['Version'], '1.1') - self.assertEqual(value_details['AssVal'], total_item_ass_value) - self.assertEqual(value_details['CgstVal'], total_item_cgst_value) - self.assertEqual(value_details['SgstVal'], total_item_sgst_value) - self.assertEqual(value_details['IgstVal'], total_item_igst_value) - - calculated_invoice_value = \ - value_details['AssVal'] + value_details['CgstVal'] \ - + value_details['SgstVal'] + value_details['IgstVal'] \ - + value_details['OthChrg'] - value_details['Discount'] - - self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1) - - self.assertEqual(value_details['TotInvVal'], si.base_grand_total) self.assertTrue(einvoice['EwbDtls']) + validate_totals(einvoice) + + si.apply_discount_on = 'Net Total' + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + + [d.set('included_in_print_rate', 1) for d in si.taxes] + si.save() + einvoice = make_einvoice(si) + validate_totals(einvoice) + +def get_sales_invoice_for_e_invoice(): + si = make_sales_invoice_for_ewaybill() + si.naming_series = 'INV-2020-.#####' + si.items = [] + si.append("items", { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2000, + "rate": 12, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + + si.append("items", { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 420, + "rate": 15, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + + return si def test_item_tax_net_range(self): item = create_item("T Shirt") From b2f2d0e749dfd5ba95f8e4119ef89e1b203f6dba Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 15 Jun 2021 19:43:54 +0530 Subject: [PATCH 037/951] chore: Added change log for v13.5.0 --- erpnext/change_log/v13/v13_5_0.md | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 erpnext/change_log/v13/v13_5_0.md diff --git a/erpnext/change_log/v13/v13_5_0.md b/erpnext/change_log/v13/v13_5_0.md new file mode 100644 index 00000000000..64c323a23e5 --- /dev/null +++ b/erpnext/change_log/v13/v13_5_0.md @@ -0,0 +1,54 @@ +# Version 13.5.0 Release Notes + +### Features & Enhancements + +- Tax deduction against advance payments ([#25831](https://github.com/frappe/erpnext/pull/25831)) +- Cost-center wise period closing entry ([#25766](https://github.com/frappe/erpnext/pull/25766)) +- Create Quality Inspections from account and stock documents ([#25221](https://github.com/frappe/erpnext/pull/25221)) +- Item Taxes based on net rate ([#25961](https://github.com/frappe/erpnext/pull/25961)) +- Enable/disable gl entry posting for change given in pos ([#25822](https://github.com/frappe/erpnext/pull/25822)) +- Add Inactive status to Employee ([#26029](https://github.com/frappe/erpnext/pull/26029)) +- Added check box to combine items with same BOM ([#25478](https://github.com/frappe/erpnext/pull/25478)) +- Item Tax Templates for Germany ([#25858](https://github.com/frappe/erpnext/pull/25858)) +- Refactored leave balance report ([#25771](https://github.com/frappe/erpnext/pull/25771)) +- Refactored Vehicle Expenses Report ([#25727](https://github.com/frappe/erpnext/pull/25727)) +- Refactored maintenance schedule and visit document ([#25358](https://github.com/frappe/erpnext/pull/25358)) + +### Fixes + +- Cannot add same item with different rates ([#25849](https://github.com/frappe/erpnext/pull/25849)) +- Show only company addresses for ITC reversal entry ([#25866](https://github.com/frappe/erpnext/pull/25866)) +- Hiding Rounding Adjustment field ([#25380](https://github.com/frappe/erpnext/pull/25380)) +- Auto tax calculations in Payment Entry ([#26055](https://github.com/frappe/erpnext/pull/26055)) +- Not able to select the item code in work order ([#25915](https://github.com/frappe/erpnext/pull/25915)) +- Cannot reset plaid link for a bank account ([#25869](https://github.com/frappe/erpnext/pull/25869)) +- Student invalid password reset link ([#25826](https://github.com/frappe/erpnext/pull/25826)) +- Multiple pos issues ([#25928](https://github.com/frappe/erpnext/pull/25928)) +- Add Product Bundles to POS ([#25860](https://github.com/frappe/erpnext/pull/25860)) +- Enable Parallel tests ([#25862](https://github.com/frappe/erpnext/pull/25862)) +- Service item check on e-Invoicing ([#25986](https://github.com/frappe/erpnext/pull/25986)) +- Choose correct Salary Structure Assignment when getting data for formula eval ([#25981](https://github.com/frappe/erpnext/pull/25981)) +- Ignore internal transfer invoices from GST Reports ([#25969](https://github.com/frappe/erpnext/pull/25969)) +- Taxable value for invoices with additional discount ([#26056](https://github.com/frappe/erpnext/pull/26056)) +- Validate negative allocated amount in Payment Entry ([#25799](https://github.com/frappe/erpnext/pull/25799)) +- Allow all System Managers to delete company transactions ([#25834](https://github.com/frappe/erpnext/pull/25834)) +- Wrong round off gl entry posted in case of purchase invoice ([#25775](https://github.com/frappe/erpnext/pull/25775)) +- Use dictionary filter instead of list ([#25874](https://github.com/frappe/erpnext/pull/25874)) +- Ageing error in PSOA ([#25855](https://github.com/frappe/erpnext/pull/25855)) +- On click of duplicate button system has not copied the difference account ([#25988](https://github.com/frappe/erpnext/pull/25988)) +- Assign Product Bundle's conversion_factor to Pack… ([#25840](https://github.com/frappe/erpnext/pull/25840)) +- Rename Loan Management workspace to Loans ([#25856](https://github.com/frappe/erpnext/pull/25856)) +- Fix stock quantity calculation when negative_stock_allowe… ([#25859](https://github.com/frappe/erpnext/pull/25859)) +- Update cost center from pos profile ([#25971](https://github.com/frappe/erpnext/pull/25971)) +- Ensure website theme is applied correctly ([#25863](https://github.com/frappe/erpnext/pull/25863)) +- Only display GST card in Accounting Workspace if it's in India ([#26000](https://github.com/frappe/erpnext/pull/26000)) +- Incorrect gstin fetched incase of branch company address ([#25841](https://github.com/frappe/erpnext/pull/25841)) +- Sort account balances by account name ([#26009](https://github.com/frappe/erpnext/pull/26009)) +- Custom conversion factor field not mapped from job card to stock entry ([#25956](https://github.com/frappe/erpnext/pull/25956)) +- Chart of accounts importer always error ([#25882](https://github.com/frappe/erpnext/pull/25882)) +- Create POS Invoice for Product Bundles ([#25847](https://github.com/frappe/erpnext/pull/25847)) +- Wrap dates in getdate for leave application ([#25899](https://github.com/frappe/erpnext/pull/25899)) +- Closing entry shows incorrect expected amount ([#25868](https://github.com/frappe/erpnext/pull/25868)) +- Add Hold status column in the Issue Summary Report ([#25828](https://github.com/frappe/erpnext/pull/25828)) +- Rendering of broken image on pos ([#25872](https://github.com/frappe/erpnext/pull/25872)) +- Timeout error in the repost item valuation ([#25854](https://github.com/frappe/erpnext/pull/25854)) \ No newline at end of file From 1e2df2c1093e5fa418933e6425cf97c23372ca6f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 15 Jun 2021 19:53:57 +0530 Subject: [PATCH 038/951] fix(pos): 'NoneType' object is not iterable --- erpnext/selling/page/point_of_sale/point_of_sale.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 7742f243852..8d1f112dc28 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -9,7 +9,7 @@ from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability def search_by_term(search_term, warehouse, price_list): - result = search_for_serial_or_batch_or_barcode_number(search_term) + result = search_for_serial_or_batch_or_barcode_number(search_term) or {} item_code = result.get("item_code") or search_term serial_no = result.get("serial_no") or "" @@ -25,7 +25,7 @@ def search_by_term(search_term, warehouse, price_list): price_list_rate, currency = frappe.db.get_value('Item Price', { 'price_list': price_list, 'item_code': item_code - }, ["price_list_rate", "currency"]) + }, ["price_list_rate", "currency"]) or [None, None] item_info.update({ 'serial_no': serial_no, @@ -46,7 +46,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te result = [] if search_term: - result = search_by_term(search_term, warehouse, price_list) + result = search_by_term(search_term, warehouse, price_list) or [] if result: return result From 24a88f6cf642603e2d5ee17e68853de4676979ed Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 15 Jun 2021 20:18:40 +0550 Subject: [PATCH 039/951] bumped to version 13.5.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 5808090db35..76e8a514d50 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.4.1' +__version__ = '13.5.0' def get_default_company(user=None): '''Get default company for user''' From 99531a35e00c53943a2f613530ff23d19dc6efeb Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Jun 2021 10:47:23 +0530 Subject: [PATCH 040/951] fix(pos): unsupported operand type -= for 'float' and 'NoneType' --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/public/js/controllers/transaction.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e14f305fc55..55a5b99907b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -989,7 +989,7 @@ class SalesInvoice(SellingController): for payment_mode in self.payments: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: - payment_mode.base_amount -= self.change_amount + payment_mode.base_amount -= flt(self.change_amount) if payment_mode.amount: # POS, make payment entries diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 89fed3bf0dc..0f44ad71d8a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -387,7 +387,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(this.frm.doc.scan_barcode) { frappe.call({ - method: "erpnext.selling.page.point_of_sale.point_of_sale.search_serial_or_batch_or_barcode_number", + method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number", args: { search_value: this.frm.doc.scan_barcode } }).then(r => { const data = r && r.message; From 94484d7766d69a882cba5ee179f35505a5dc78e6 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 21 Jun 2021 11:48:57 +0550 Subject: [PATCH 041/951] bumped to version 13.5.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 76e8a514d50..60c614f6f59 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.5.0' +__version__ = '13.5.1' def get_default_company(user=None): '''Get default company for user''' From 26bec9d7b4e3c6b360f38202b541b4c921c18244 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 11:03:32 +0530 Subject: [PATCH 042/951] fix: Country Link field in 'Add address' website modal auto-clears --- erpnext/templates/includes/cart/cart_address.html | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index 84a9430956e..4482bc10cf7 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -99,6 +99,7 @@ frappe.ready(() => { fieldname: 'country', fieldtype: 'Link', options: 'Country', + only_select: true, reqd: 1 }, { From 5884f1aeb02411200b150604d37419eee15c67d8 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 24 Jun 2021 12:01:12 +0530 Subject: [PATCH 043/951] fix: (style) Address card buttons hover state --- erpnext/public/scss/shopping_cart.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 9402cf9ea48..5962859be5a 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -467,11 +467,15 @@ body.product-page { .btn-change-address { color: var(--blue-500); - box-shadow: none; - border: 1px solid var(--blue-500); } } +.btn-new-address:hover, .btn-change-address:hover { + box-shadow: none; + color: var(--blue-500) !important; + border: 1px solid var(--blue-500); +} + .modal .address-card { .card-body { padding: var(--padding-sm); From ea2408744a9a79cd4865a31fa41c0f8cc19a5c86 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 14:08:07 +0530 Subject: [PATCH 044/951] fix: Consider Website Item Groups in Item group page product listing - Passed an argument to query engine to know when query is for item group page - If for item group page, get data with regards to website item group table - This query should be fast since there's one filter and that shortens the table beforehand - This data is merged with the results from the Item master (results only considering item attributes and field filters) - The combined data is then sorted as per weightage Co-authored-by: Gavin D'souza --- .../setup/doctype/item_group/item_group.py | 2 +- erpnext/shopping_cart/product_query.py | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index bff806d5472..668714314f6 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -91,7 +91,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): field_filters['item_group'] = self.name engine = ProductQuery() - context.items = engine.query(attribute_filters, field_filters, search, start) + context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) filter_engine = ProductFiltersBuilder(self.name) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 36d446ed0fd..bb31220447a 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -22,13 +22,14 @@ class ProductQuery: self.settings = frappe.get_doc("Products Settings") self.cart_settings = frappe.get_doc("Shopping Cart Settings") self.page_length = self.settings.products_per_page or 20 - self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route'] + self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', + 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage'] self.filters = [] self.or_filters = [['show_in_website', '=', 1]] if not self.settings.get('hide_variants'): self.or_filters.append(['show_variant_in_website', '=', 1]) - def query(self, attributes=None, fields=None, search_term=None, start=0): + def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None): """Summary Args: @@ -44,6 +45,15 @@ class ProductQuery: if search_term: self.build_search_filters(search_term) result = [] + website_item_groups = [] + + # if from item group page consider website item group table + if item_group: + website_item_groups = frappe.db.get_all( + "Item", + fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], + filters=[["Website Item Group", "item_group", "=", item_group]] + ) if attributes: all_items = [] @@ -65,18 +75,33 @@ class ProductQuery: ) items_dict = {item.name: item for item in items} - # TODO: Replace Variants by their parent templates all_items.append(set(items_dict.keys())) result = [items_dict.get(item) for item in list(set.intersection(*all_items))] else: - result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length) + result = frappe.get_all( + "Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + start=start, + limit=self.page_length + ) + + # Combine results having context of website item groups into item results + if item_group and website_item_groups: + items_list = {row.name for row in result} + for row in website_item_groups: + if row.wig_parent not in items_list: + result.append(row) + + result = sorted(result, key=lambda x: x.get("weightage"), reverse=True) for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None + item.formatted_price = product_info.get('price', {}).get('formatted_price') return result From 9f305e983cc301aa628648f7efe66e138b271607 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 16:03:24 +0530 Subject: [PATCH 045/951] fix: Filters did not consider Website Item Group --- erpnext/shopping_cart/filters.py | 21 +++++++++++++++------ erpnext/shopping_cart/product_query.py | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 6c63d8759b4..979afd3c13d 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -22,12 +22,15 @@ class ProductFiltersBuilder: filter_data = [] for df in fields: - filters = {} + filters, or_filters = {}, [] if df.fieldtype == "Link": if self.item_group: - filters['item_group'] = self.item_group + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() @@ -44,7 +47,9 @@ class ProductFiltersBuilder: values = [d.name for d in frappe.get_all(doctype, filters)] # Remove None - values = values.remove(None) if None in values else values + if None in values: + values.remove(None) + if values: filter_data.append([df, values]) @@ -61,14 +66,18 @@ class ProductFiltersBuilder: for attr_doc in attribute_docs: selected_attributes = [] for attr in attr_doc.item_attribute_values: + or_filters = [] filters= [ ["Item Variant Attribute", "attribute", "=", attr.parent], ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value] ] if self.item_group: - filters.append(["item_group", "=", self.item_group]) + or_filters.extend([ + ["item_group", "=", self.item_group], + ["Website Item Group", "item_group", "=", self.item_group] + ]) - if frappe.db.get_all("Item", filters, limit=1): + if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1): selected_attributes.append(attr) if selected_attributes: diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index bb31220447a..0b05f68ae9b 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -101,7 +101,7 @@ class ProductQuery: for item in result: product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') if product_info: - item.formatted_price = product_info.get('price', {}).get('formatted_price') + item.formatted_price = (product_info.get('price') or {}).get('formatted_price') return result From f91383837329e1d82f2a8fdb4a2119fd56efa74d Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:06:11 +0530 Subject: [PATCH 046/951] fix: Consider Table Multiselect fields in Query engine - Since table multiselect fields were not handled, the query tried searching for this child field in item master - This broke the query - On trying to reload or go back to all-products page with field filters that are table mutiselect, page breaks --- erpnext/shopping_cart/product_query.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index 0b05f68ae9b..cd4a1769212 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -115,6 +115,17 @@ class ProductQuery: if not values: continue + # handle multiselect fields in filter addition + meta = frappe.get_meta('Item', cached=True) + df = meta.get_field(field) + if df.fieldtype == 'Table MultiSelect': + child_doctype = df.options + child_meta = frappe.get_meta(child_doctype, cached=True) + fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + if fields: + self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) + continue + if isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) From 4f0e6cd911bf0eb71107f0a4bcbf4dec2642b5ee Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 20:12:59 +0530 Subject: [PATCH 047/951] fix: Sider --- erpnext/shopping_cart/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 979afd3c13d..9f06d20bde5 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) else: doctype = df.get_link_doctype() From 820a579051d26857ae52cc0a30c7aea0db79190e Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 23 Jun 2021 22:38:10 +0530 Subject: [PATCH 048/951] chore: Test for Item visibility in multiple item group pages --- .../test_product_configurator.py | 63 +++++++++++++++++++ erpnext/shopping_cart/filters.py | 2 +- erpnext/shopping_cart/product_query.py | 6 +- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index 3521e7e8bf0..daaba671736 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -43,6 +43,30 @@ class TestProductConfigurator(unittest.TestCase): "show_variant_in_website": 1 }).insert() + def create_regular_web_item(self, name, item_group=None): + if not frappe.db.exists('Item', name): + doc = frappe.get_doc({ + "description": name, + "item_code": name, + "item_name": name, + "doctype": "Item", + "is_stock_item": 1, + "item_group": item_group or "_Test Item Group", + "stock_uom": "_Test UOM", + "item_defaults": [{ + "company": "_Test Company", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + "income_account": "Sales - _TC" + }], + "show_in_website": 1 + }).insert() + else: + doc = frappe.get_doc("Item", name) + return doc + def test_product_list(self): template_items = frappe.get_all('Item', {'show_in_website': 1}) variant_items = frappe.get_all('Item', {'show_variant_in_website': 1}) @@ -79,3 +103,42 @@ class TestProductConfigurator(unittest.TestCase): 'Test Size': ['2XL'] }) self.assertEqual(len(items), 1) + + def test_products_in_multiple_item_groups(self): + """Check if product is visible on multiple item group pages barring its own.""" + from erpnext.shopping_cart.product_query import ProductQuery + + if not frappe.db.exists("Item Group", {"name": "Tech Items"}): + item_group_doc = frappe.get_doc({ + "doctype": "Item Group", + "item_group_name": "Tech Items", + "parent_item_group": "All Item Groups", + "show_in_website": 1 + }).insert() + else: + item_group_doc = frappe.get_doc("Item Group", "Tech Items") + + doc = self.create_regular_web_item("Portal Item", item_group="Tech Items") + if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}): + doc.append("website_item_groups", { + "item_group": "_Test Item Group Desktops" + }) + doc.save() + + # check if item is visible in its own Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items") + self.assertEqual(len(items), 1) + self.assertEqual(items[0].item_code, "Portal Item") + + # check if item is visible in configured foreign Item Group's page + engine = ProductQuery() + items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops") + item_codes = [row.item_code for row in items] + + self.assertIn(len(items), [2, 3]) + self.assertIn("Portal Item", item_codes) + + # teardown + doc.delete() + item_group_doc.delete() \ No newline at end of file diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py index 9f06d20bde5..7dfa09e2d62 100644 --- a/erpnext/shopping_cart/filters.py +++ b/erpnext/shopping_cart/filters.py @@ -30,7 +30,7 @@ class ProductFiltersBuilder: ["Website Item Group", "item_group", "=", self.item_group] ]) - values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname, debug=1) + values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname) else: doctype = df.get_link_doctype() diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py index cd4a1769212..d96d803416c 100644 --- a/erpnext/shopping_cart/product_query.py +++ b/erpnext/shopping_cart/product_query.py @@ -121,12 +121,10 @@ class ProductQuery: if df.fieldtype == 'Table MultiSelect': child_doctype = df.options child_meta = frappe.get_meta(child_doctype, cached=True) - fields = child_meta.get("fields", { "fieldtype": "Link", "in_list_view": 1 }) + fields = child_meta.get("fields") if fields: self.filters.append([child_doctype, fields[0].fieldname, 'IN', values]) - continue - - if isinstance(values, list): + elif isinstance(values, list): # If value is a list use `IN` query self.filters.append([field, 'IN', values]) else: From f79a72dbf33bbc035570751dea04cf447c90bfe2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Jun 2021 14:14:46 +0530 Subject: [PATCH 049/951] fix: Error while booking deferred revenue --- erpnext/accounts/deferred_revenue.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index dd346bc2408..2f86c6c1de2 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -263,6 +263,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): amount, base_amount = calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency) + if not amount: + return + if via_journal_entry: book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) From b5d1a7731cf2c7eb1e77475a4cbd3fdda5c5db47 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 24 Jun 2021 15:55:50 +0550 Subject: [PATCH 050/951] bumped to version 13.5.2 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 60c614f6f59..39d9a27615e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.5.1' +__version__ = '13.5.2' def get_default_company(user=None): '''Get default company for user''' From 532a224c4456f0fe8bb9805d76d4211d2af79613 Mon Sep 17 00:00:00 2001 From: Ankush Date: Fri, 25 Jun 2021 13:28:01 +0530 Subject: [PATCH 051/951] fix: precision rate for packed items (#26046) (#26217) Co-authored-by: Noah Jacob --- erpnext/controllers/selling_controller.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7f28289760c..da2765deded 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -330,9 +330,15 @@ class SellingController(StockController): # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): - rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) - if d.rate != rate: - d.rate = rate + if d.doctype == "Packed Item": + incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate')) + if d.incoming_rate != incoming_rate: + d.incoming_rate = incoming_rate + else: + rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate')) + if d.rate != rate: + d.rate = rate + d.discount_percentage = 0 d.discount_amount = 0 frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") From cd36ba7e64343c6997a5aa710196af63fea573fc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:34:00 +0530 Subject: [PATCH 052/951] fix: Error while fetching item taxes --- erpnext/stock/get_item_details.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c64084fe340..e0a0c4a472e 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -438,6 +438,10 @@ def get_barcode_data(items_list): @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} + + if not item_tax_templates: + item_tax_templates = {} + if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) From 6eb8d19cc9e3e263a90cde4fb1cf7f0abae21f21 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Jun 2021 13:38:06 +0530 Subject: [PATCH 053/951] fix: Check for is None --- erpnext/stock/get_item_details.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e0a0c4a472e..ca174a3f63c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -439,8 +439,11 @@ def get_barcode_data(items_list): def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} - if not item_tax_templates: + if item_tax_templates is None: item_tax_templates = {} + + if item_rates is None: + item_rates = {} if isinstance(item_codes, (str,)): item_codes = json.loads(item_codes) @@ -457,7 +460,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]} + args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) From b4e7ee0e45010bac5a783845cb15b46aec4d73c9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 25 May 2021 17:40:59 +0530 Subject: [PATCH 054/951] chore: remove dead and py2 compatibility code form_grid_template doesn't exist --- erpnext/manufacturing/doctype/work_order/work_order.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index e343ed2dd38..302753214b3 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1,7 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe import json import math @@ -30,9 +29,6 @@ class ItemHasVariantError(frappe.ValidationError): pass class SerialNoQtyError(frappe.ValidationError): pass -form_grid_templates = { - "operations": "templates/form_grid/work_order_grid.html" -} class WorkOrder(Document): def onload(self): From 9af3f12411cbdadb0611a10c2bfb4edef0b876ab Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 14:47:44 +0530 Subject: [PATCH 055/951] fix(ux): show bom in operations child table --- .../work_order_operation/work_order_operation.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 6d8fb80e319..f7b8787a0b3 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -2,7 +2,6 @@ "actions": [], "creation": "2014-10-16 14:35:41.950175", "doctype": "DocType", - "editable_grid": 1, "engine": "InnoDB", "field_order": [ "details", @@ -49,6 +48,7 @@ { "fieldname": "bom", "fieldtype": "Link", + "in_list_view": 1, "label": "BOM", "no_copy": 1, "options": "BOM", @@ -68,6 +68,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "Operation completed for how many finished goods?", "fieldname": "completed_qty", "fieldtype": "Float", @@ -77,6 +78,7 @@ "read_only": 1 }, { + "columns": 1, "default": "Pending", "fieldname": "status", "fieldtype": "Select", @@ -119,6 +121,7 @@ "fieldtype": "Column Break" }, { + "columns": 1, "description": "in Minutes", "fieldname": "time_in_mins", "fieldtype": "Float", @@ -205,7 +208,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-12 14:48:31.061286", + "modified": "2021-06-24 14:36:12.835543", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -214,4 +217,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 6588a936d5dd96f434ca3590ff8eb01ae3e594fa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 May 2021 12:22:50 +0530 Subject: [PATCH 056/951] fix: order and time of operations for multilevel bom - Order of operations was being sorted by idx of individual operations in BOM table, which made the ordering useless. - This adds ordering that's sorted from lowest level item to top level item. - chore: remove dead functionality. There's no `items` table. Required item level operations get overwritten on fetching of items / operations e.g. when clicking on multi-level BOM checkbox. - test: add test for tree representation - feat: BOMTree class to get complete representation of a tree --- erpnext/manufacturing/doctype/bom/bom.py | 85 ++++++++++++++++++- erpnext/manufacturing/doctype/bom/test_bom.py | 82 +++++++++++++++++- .../doctype/work_order/work_order.py | 57 +++++++------ 3 files changed, 189 insertions(+), 35 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3e855603b48..c58f017258e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1,7 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from typing import List +from collections import deque import frappe, erpnext from frappe.utils import cint, cstr, flt, today from frappe import _ @@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc import functools -from six import string_types - from operator import itemgetter form_grid_templates = { "items": "templates/form_grid/item_grid.html" } + +class BOMTree: + """Full tree representation of a BOM""" + + # specifying the attributes to save resources + # ref: https://docs.python.org/3/reference/datamodel.html#slots + __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] + + def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + self.name = name # name of node, BOM number if is_bom else item_code + self.child_items: List["BOMTree"] = [] # list of child items + self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.item_code: str = None # item_code associated with node + self.qty = qty # required unit quantity to make one unit of parent item. + self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. + if not self.is_bom: + self.item_code = self.name + else: + self.__create_tree() + + def __create_tree(self): + bom = frappe.get_cached_doc("BOM", self.name) + self.item_code = bom.item + + for item in bom.get("items", []): + qty = item.qty / bom.quantity # quantity per unit + exploded_qty = self.exploded_qty * qty + if item.bom_no: + child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) + self.child_items.append(child) + else: + self.child_items.append( + BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty) + ) + + def level_order_traversal(self) -> List["BOMTree"]: + """Get level order traversal of tree. + E.g. for following tree the traversal will return list of nodes in order from top to bottom. + BOM: + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 + + returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] + """ + traversal = [] + q = deque() + q.append(self) + + while q: + node = q.popleft() + + for child in node.child_items: + traversal.append(child) + q.append(child) + + return traversal + + def __str__(self) -> str: + return ( + f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}" + f" exploded_qty: {self.exploded_qty}" + ) + + def __repr__(self, level: int = 0) -> str: + rep = "┃ " * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n" + for child in self.child_items: + rep += child.__repr__(level=level + 1) + return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", @@ -152,7 +224,7 @@ class BOM(WebsiteGenerator): if not args: args = frappe.form_dict.get('args') - if isinstance(args, string_types): + if isinstance(args, str): import json args = json.loads(args) @@ -600,6 +672,11 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + def get_tree_representation(self) -> BOMTree: + """Get a complete tree representation preserving order of child items.""" + return BOMTree(self.name) + + def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 1f443fb95ae..57a54587269 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -2,14 +2,13 @@ # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from collections import deque import unittest import frappe from frappe.utils import cstr, flt from frappe.test_runner import make_test_records from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost -from six import string_types from erpnext.stock.doctype.item.test_item import make_item from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.tests.test_subcontracting import set_backflush_based_on @@ -227,11 +226,88 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + def test_bom_tree_representation(self): + bom_tree = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + } + } + parent_bom = create_nested_bom(bom_tree, prefix="") + created_tree = parent_bom.get_tree_representation() + + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + created_order = created_tree.level_order_traversal() + + self.assertEqual(len(reqd_order), len(created_order)) + + for reqd_item, created_item in zip(reqd_order, created_order): + self.assertEqual(reqd_item, created_item.item_code) + + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + + + +def level_order_traversal(node): + traversal = [] + q = deque() + q.append(node) + + while q: + node = q.popleft() + + for node_name, subtree in node.items(): + traversal.append(node_name) + q.append(subtree) + + return traversal + +def create_nested_bom(tree, prefix="_Test bom "): + """ Helper function to create a simple nested bom from tree describing item names. (along with required items) + """ + + def create_items(bom_tree): + for item_code, subtree in bom_tree.items(): + bom_item_code = prefix + item_code + if not frappe.db.exists("Item", bom_item_code): + frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() + create_items(subtree) + create_items(tree) + + def dfs(tree, node): + """naive implementation for searching right subtree""" + for node_name, subtree in tree.items(): + if node_name == node: + return subtree + else: + result = dfs(subtree, node) + if result is not None: + return result + + order_of_creating_bom = reversed(level_order_traversal(tree)) + + for item in order_of_creating_bom: + child_items = dfs(tree, item) + if child_items: + bom_item_code = prefix + item + bom = frappe.get_doc(doctype="BOM", item=bom_item_code) + for child_item in child_items.keys(): + bom.append("items", {"item_code": prefix + child_item}) + bom.insert() + bom.submit() + + return bom # parent bom is last bom + + def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=None): - if warehouse_list and isinstance(warehouse_list, string_types): + if warehouse_list and isinstance(warehouse_list, str): warehouse_list = [warehouse_list] if not warehouse_list: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 302753214b3..180815d80e4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -468,46 +468,47 @@ class WorkOrder(Document): def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" - self.set('operations', []) + def _get_operations(bom_no, qty=1): + return frappe.db.sql( + f"""select + operation, description, workstation, idx, + base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, + "Pending" as status, parent as bom, batch_size, sequence_id + from + `tabBOM Operation` + where + parent = %s order by idx + """, bom_no, as_dict=1) + + + self.set('operations', []) if not self.bom_no: return - if self.use_multi_level_bom: - bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() + operations = [] + if not self.use_multi_level_bom: + bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") + operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) else: - bom_list = [self.bom_no] + bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() + bom_traversal = list(reversed(bom_tree.level_order_traversal())) + bom_traversal.append(bom_tree) # add operation on top level item last + + for d in bom_traversal: + if d.is_bom: + operations.extend(_get_operations(d.name, qty=d.exploded_qty)) + + for correct_index, operation in enumerate(operations, start=1): + operation.idx = correct_index - operations = frappe.db.sql(""" - select - operation, description, workstation, idx, - base_hour_rate as hour_rate, time_in_mins, - "Pending" as status, parent as bom, batch_size, sequence_id - from - `tabBOM Operation` - where - parent in (%s) order by idx - """ % ", ".join(["%s"]*len(bom_list)), tuple(bom_list), as_dict=1) self.set('operations', operations) - - if self.use_multi_level_bom and self.get('operations') and self.get('items'): - raw_material_operations = [d.operation for d in self.get('items')] - operations = [d.operation for d in self.get('operations')] - - for operation in raw_material_operations: - if operation not in operations: - self.append('operations', { - 'operation': operation - }) - self.calculate_time() def calculate_time(self): - bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") - for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() From 5d5dc56f94a00cf501dc3df0839020216d521cfd Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 22 Jun 2021 15:23:04 +0530 Subject: [PATCH 057/951] fix: removed values out of sync validation from stock transactions --- erpnext/controllers/stock_controller.py | 5 +- .../incorrect_stock_value_report/__init__.py | 0 .../incorrect_stock_value_report.js | 36 +++++ .../incorrect_stock_value_report.json | 29 ++++ .../incorrect_stock_value_report.py | 141 ++++++++++++++++++ 5 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 erpnext/stock/report/incorrect_stock_value_report/__init__.py create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json create mode 100644 erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 35097b97b99..8196cff849d 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map -from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map from erpnext.stock.stock_ledger import get_valuation_rate @@ -497,9 +497,6 @@ class StockController(AccountsController): }) if future_sle_exists(args): create_repost_item_valuation_entry(args) - elif not is_reposting_pending(): - check_if_stock_and_account_balance_synced(self.posting_date, - self.company, self.doctype, self.name) @frappe.whitelist() def make_quality_inspections(doctype, docname, items): diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js new file mode 100644 index 00000000000..ff424807e3e --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js @@ -0,0 +1,36 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Incorrect Stock Value Report"] = { + "filters": [ + { + "label": __("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, + { + "label": __("Account"), + "fieldname": "account", + "fieldtype": "Link", + "options": "Account", + get_query: function() { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "account_type": "Stock", + "company": company + } + } + } + }, + { + "label": __("From Date"), + "fieldname": "from_date", + "fieldtype": "Date" + } + ] +}; diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json new file mode 100644 index 00000000000..a7e9f203f7b --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-06-22 15:35:05.148177", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-06-22 15:35:05.148177", + "modified_by": "Administrator", + "module": "Stock", + "name": "Incorrect Stock Value Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Incorrect Stock Value Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py new file mode 100644 index 00000000000..a7243878eb8 --- /dev/null +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -0,0 +1,141 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import erpnext +from frappe import _ +from six import iteritems +from frappe.utils import add_days, today, getdate +from erpnext.stock.utils import get_stock_value_on +from erpnext.accounts.utils import get_stock_and_account_balance + +def execute(filters=None): + if not erpnext.is_perpetual_inventory_enabled(filters.company): + frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") + .format(filters.company)) + + data = get_data(filters) + columns = get_columns(filters) + + return columns, data + +def get_unsync_date(filters): + date = filters.from_date + if not date: + date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") + date = date[0][0] + + if not date: + return + + while getdate(date) < getdate(today()): + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date, + company=filters.company, account = filters.account) + + if abs(account_bal - stock_bal) > 0.1: + return date + + date = add_days(date, 1) + +def get_data(report_filters): + from_date = get_unsync_date(report_filters) + + if not from_date: + return [] + + result = [] + + voucher_wise_dict = {} + data = frappe.db.sql(''' + SELECT + name, posting_date, posting_time, voucher_type, voucher_no, + stock_value_difference, stock_value, warehouse, item_code + FROM + `tabStock Ledger Entry` + WHERE + posting_date + = %s and company = %s + and is_cancelled = 0 + ORDER BY timestamp(posting_date, posting_time) asc, creation asc + ''', (from_date, report_filters.company), as_dict=1) + + for d in data: + voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) + + closing_date = add_days(from_date, -1) + for key, stock_data in iteritems(voucher_wise_dict): + prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1]) + for data in stock_data: + expected_stock_value = prev_stock_value + data.stock_value_difference + if abs(data.stock_value - expected_stock_value) > 0.1: + data.difference_value = abs(data.stock_value - expected_stock_value) + data.expected_stock_value = expected_stock_value + result.append(data) + + return result + +def get_columns(filters): + return [ + { + "label": _("Stock Ledger ID"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Stock Ledger Entry", + "width": "80" + }, + { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date" + }, + { + "label": _("Posting Time"), + "fieldname": "posting_time", + "fieldtype": "Time" + }, + { + "label": _("Voucher Type"), + "fieldname": "voucher_type", + "width": "110" + }, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": "110" + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": "110" + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": "110" + }, + { + "label": _("Expected Stock Value"), + "fieldname": "expected_stock_value", + "fieldtype": "Currency", + "width": "150" + }, + { + "label": _("Stock Value"), + "fieldname": "stock_value", + "fieldtype": "Currency", + "width": "120" + }, + { + "label": _("Difference Value"), + "fieldname": "difference_value", + "fieldtype": "Currency", + "width": "150" + } + ] \ No newline at end of file From 478360397d903bcb374d5cb7fd862337cedc59ea Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 24 Jun 2021 23:13:54 +0530 Subject: [PATCH 058/951] fix: fetch batch items in stock reco --- .../doctype/work_order/test_work_order.py | 9 +- .../stock_reconciliation.js | 67 +++++++---- .../stock_reconciliation.py | 108 +++++++++++++----- 3 files changed, 125 insertions(+), 59 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index cb1ee92196f..68de0b29d3e 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -389,17 +389,12 @@ class TestWorkOrder(unittest.TestCase): ste.submit() stock_entries.append(ste) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) + job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') self.assertEqual(len(job_cards), len(bom.operations)) for i, job_card in enumerate(job_cards): doc = frappe.get_doc("Job Card", job_card) - doc.append("time_logs", { - "from_time": add_to_date(None, i), - "hours": 1, - "to_time": add_to_date(None, i + 1), - "completed_qty": doc.for_quantity - }) + doc.time_logs[0].completed_qty = 1 doc.submit() ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index ac4ed5e75d9..a01db80da4a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -48,37 +48,54 @@ frappe.ui.form.on("Stock Reconciliation", { }, get_items: function(frm) { - frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1, + let fields = [{ + label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1, "get_query": function() { return { "filters": { "company": frm.doc.company, } - } - }}, - function(data) { - frappe.call({ - method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", - args: { - warehouse: data.warehouse, - posting_date: frm.doc.posting_date, - posting_time: frm.doc.posting_time, - company:frm.doc.company - }, - callback: function(r) { - var items = []; - frm.clear_table("items"); - for(var i=0; i< r.message.length; i++) { - var d = frm.add_child("items"); - $.extend(d, r.message[i]); - if(!d.qty) d.qty = null; - if(!d.valuation_rate) d.valuation_rate = null; - } - frm.refresh_field("items"); - } - }); + }; } - , __("Get Items"), __("Update")); + }, { + label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item", + "get_query": function() { + return { + "filters": { + "disabled": 0, + } + }; + } + }]; + + frappe.prompt(fields, function(data) { + frappe.call({ + method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", + args: { + warehouse: data.warehouse, + posting_date: frm.doc.posting_date, + posting_time: frm.doc.posting_time, + company: frm.doc.company, + item_code: data.item_code + }, + callback: function(r) { + frm.clear_table("items"); + for (var i=0; i= %s and rgt <= %s and name=bin.warehouse) - """, (lft, rgt)) + where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1 + and i.has_variants = 0 and exists( + select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse + ) + """, (lft, rgt), as_dict=1) items += frappe.db.sql(""" - select i.name, i.item_name, id.default_warehouse, i.has_serial_no + select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no from tabItem i, `tabItem Default` id where i.name = id.parent and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse) - and i.is_stock_item = 1 and i.has_batch_no = 0 - and i.has_variants = 0 and i.disabled = 0 and id.company=%s + and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s group by i.name - """, (lft, rgt, company)) + """, (lft, rgt, company), as_dict=1) - res = [] - for d in set(items): - stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time, - with_valuation_rate=True , with_serial_no=cint(d[3])) + return items - if frappe.db.get_value("Item", d[0], "disabled") == 0: - res.append({ - "item_code": d[0], - "warehouse": d[2], - "qty": stock_bal[0], - "item_name": d[1], - "valuation_rate": stock_bal[1], - "current_qty": stock_bal[0], - "current_valuation_rate": stock_bal[1], - "current_serial_no": stock_bal[2] if cint(d[3]) else '', - "serial_no": stock_bal[2] if cint(d[3]) else '' - }) +def get_item_data(row, qty, valuation_rate, serial_no=None): + return { + 'item_code': row.item_code, + 'warehouse': row.warehouse, + 'qty': qty, + 'item_name': row.item_name, + 'valuation_rate': valuation_rate, + 'current_qty': qty, + 'current_valuation_rate': valuation_rate, + 'current_serial_no': serial_no, + 'serial_no': serial_no, + 'batch_no': row.get('batch_no') + } - return res +def get_itemwise_batch(warehouse, posting_date, company, item_code=None): + from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute + itemwise_batch_data = {} + + filters = frappe._dict({ + 'warehouse': warehouse, + 'from_date': posting_date, + 'to_date': posting_date, + 'company': company + }) + + if item_code: + filters.item_code = item_code + + columns, data = execute(filters) + + for row in data: + itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({ + 'item_code': row[0], + 'warehouse': warehouse, + 'qty': row[8], + 'item_name': row[1], + 'batch_no': row[4] + })) + + return itemwise_batch_data @frappe.whitelist() def get_stock_balance_for(item_code, warehouse, From 1f10a99910e26c0aca4ac9ba30e3d5f985b992ef Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 29 Jun 2021 15:58:56 +0530 Subject: [PATCH 059/951] fix: Employee Inactive status implications (#26245) --- erpnext/hr/doctype/attendance/attendance.js | 2 +- erpnext/hr/doctype/attendance/attendance.py | 5 +++++ erpnext/hr/doctype/attendance/attendance_list.js | 3 +++ erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/attendance/attendance.js b/erpnext/hr/doctype/attendance/attendance.js index c3c3cb82f94..7964078c7f0 100644 --- a/erpnext/hr/doctype/attendance/attendance.js +++ b/erpnext/hr/doctype/attendance/attendance.js @@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) { cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) { return{ query: "erpnext.controllers.queries.employee_query" - } + } } diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index f3b8a799b3c..3412675d811 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -15,6 +15,7 @@ class Attendance(Document): validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) self.validate_attendance_date() self.validate_duplicate_record() + self.validate_employee_status() self.check_leave_record() def validate_attendance_date(self): @@ -38,6 +39,10 @@ class Attendance(Document): frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( frappe.bold(self.employee), frappe.bold(self.attendance_date))) + def validate_employee_status(self): + if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": + frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) + def check_leave_record(self): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 0c7eafe9c61..9a3bac0eb23 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = { label: __('For Employee'), fieldtype: 'Link', options: 'Employee', + get_query: () => { + return {query: "erpnext.controllers.queries.employee_query"} + }, reqd: 1, onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index e71d81f323a..5c7c0a3b092 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -459,6 +459,7 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): where t1.name = t2.employee and t2.docstatus = 1 + and t1.status != 'Inactive' %s order by t2.from_date desc """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) From 8492bf040dc5e309c74e9c51121ed6eff519367b Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 30 Jun 2021 17:17:43 +0530 Subject: [PATCH 060/951] fix: feating employee in payroll entry --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 5c7c0a3b092..36e728fc992 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -680,6 +680,10 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] include_employees = [] emp_cond = '' + + if not filters.payroll_frequency: + frappe.throw(_('Select Payroll Frequency.')) + if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) emp = filters.get('employees') From cf4e29a604c819f0673876592c2c9219a1830d0b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 30 Jun 2021 20:27:32 +0530 Subject: [PATCH 061/951] chore: Added change log for v13.6.0 --- erpnext/change_log/v13/v13_6_0.md | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 erpnext/change_log/v13/v13_6_0.md diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md new file mode 100644 index 00000000000..d881b279e3f --- /dev/null +++ b/erpnext/change_log/v13/v13_6_0.md @@ -0,0 +1,72 @@ +# Version 13.6.0 Release Notes + +### Features & Enhancements + +- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523)) +- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044)) +- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184)) +- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878)) +- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705)) +- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030)) +- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696)) +- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891)) + +### Fixes + +- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) +- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176)) +- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092)) +- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978)) +- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073)) +- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245)) +- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230)) +- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125)) +- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134)) +- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196)) +- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083)) +- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941)) +- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945)) +- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011)) +- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070)) +- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071)) +- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122)) +- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220)) +- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003)) +- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229)) +- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269)) +- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045)) +- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170)) +- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032)) +- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095)) +- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023)) +- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191)) +- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188)) +- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217)) +- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152)) +- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108)) +- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202)) +- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906)) +- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894)) +- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997)) +- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051)) +- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043)) +- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143)) +- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211)) +- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126)) +- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192)) +- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081)) +- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187)) +- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195)) +- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947)) +- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951)) +- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968)) +- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037)) +- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198)) +- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100)) +- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098)) +- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062)) +- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031)) +- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203)) +- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185)) +- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934)) +- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201)) From 87b4e6ea323bf242e0661a8735c38d5cc5d4bea8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Jun 2021 23:27:24 +0530 Subject: [PATCH 062/951] fix: employee selection not working in payroll entry --- .../doctype/payroll_entry/payroll_entry.js | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index f2892600d12..496c37b2fad 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -135,10 +135,26 @@ frappe.ui.form.on('Payroll Entry', { }); frm.set_query('employee', 'employees', () => { - if (!frm.doc.company) { - frappe.msgprint(__("Please set a Company")); - return []; + let error_fields = []; + let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date']; + + let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]); + + mandatory_fields.forEach(field => { + if (!frm.doc[field]) { + error_fields.push(frappe.unscrub(field)); + } + }); + + if (error_fields && error_fields.length) { + message = message + '

  • ' + error_fields.join('
  • ') + "
"; + frappe.throw({ + message: message, + indicator: 'red', + title: __('Missing Fields') + }); } + return { query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query", filters: frm.events.get_employee_filters(frm) @@ -148,25 +164,22 @@ frappe.ui.form.on('Payroll Entry', { get_employee_filters: function (frm) { let filters = {}; - filters['company'] = frm.doc.company; - filters['start_date'] = frm.doc.start_date; - filters['end_date'] = frm.doc.end_date; filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet; - filters['payroll_frequency'] = frm.doc.payroll_frequency; - filters['payroll_payable_account'] = frm.doc.payroll_payable_account; - filters['currency'] = frm.doc.currency; - if (frm.doc.department) { - filters['department'] = frm.doc.department; - } - if (frm.doc.branch) { - filters['branch'] = frm.doc.branch; - } - if (frm.doc.designation) { - filters['designation'] = frm.doc.designation; - } + let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account', + 'currency', 'department', 'branch', 'designation']; + + fields.forEach(field => { + if (frm.doc[field]) { + filters[field] = frm.doc[field]; + } + }); + if (frm.doc.employees) { - filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee); + if (employees && employees.length) { + filters['employees'] = employees; + } } return filters; }, From f99f872946f178d76a823ac667927555fbdedf03 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 1 Jul 2021 11:50:48 +0530 Subject: [PATCH 063/951] fix: update cost not working in the draft bom --- erpnext/manufacturing/doctype/bom/bom.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 27019dbbae2..15a7c316c91 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -325,8 +325,7 @@ frappe.ui.form.on("BOM", { freeze: true, args: { update_parent: true, - from_child_bom:false, - save: frm.doc.docstatus === 1 ? true : false + from_child_bom:false }, callback: function(r) { refresh_field("items"); From eee03fcbabd1974ddbbefa12e1f5c34a128b371e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 1 Jul 2021 12:57:13 +0550 Subject: [PATCH 064/951] bumped to version 13.6.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 39d9a27615e..0c96d325c2e 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.5.2' +__version__ = '13.6.0' def get_default_company(user=None): '''Get default company for user''' From a01264dae77f6f54bb4a099dfda295afca2b47de Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 6 Jul 2021 18:00:35 +0530 Subject: [PATCH 065/951] fix: stock_rbnb not defined (#26354) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e488b695b5f..82c87a83a50 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -386,6 +386,7 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked + stock_rbnb = self.get_company_default("stock_received_but_not_billed") i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): From ae41b53ceed0f07fd03960151b5fd48b2aa66a2f Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Tue, 6 Jul 2021 18:00:35 +0530 Subject: [PATCH 066/951] fix: stock_rbnb not defined (#26354) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e488b695b5f..82c87a83a50 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -386,6 +386,7 @@ class PurchaseReceipt(BuyingController): against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked + stock_rbnb = self.get_company_default("stock_received_but_not_billed") i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): From 01aada6c904e37ce5ef89980ae97b68fd4fbe257 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Jul 2021 13:24:43 +0530 Subject: [PATCH 067/951] refactor: Optimized code for reposting item valuation --- .../stock/doctype/stock_entry/stock_entry.py | 2 +- erpnext/stock/stock_ledger.py | 61 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8f27ef4356c..90b81ddb1dc 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -529,7 +529,7 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"): + if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): bom_items = self.get_bom_raw_materials(finished_item_qty) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4e9c7689ae4..c15d1eda7dc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -6,13 +6,14 @@ import frappe import erpnext import copy from frappe import _ -from frappe.utils import cint, flt, cstr, now, get_link_to_form +from frappe.utils import cint, flt, cstr, now, get_link_to_form, getdate from frappe.model.meta import get_field_precision from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel from erpnext.stock.utils import get_bin import json from six import iteritems + # future reposting class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): @@ -130,7 +131,13 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat if not args and voucher_type and voucher_no: args = get_args_for_voucher(voucher_type, voucher_no) - distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args] + distinct_item_warehouses = {} + for i, d in enumerate(args): + distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ + "reposting_status": False, + "sle": d, + "args_idx": i + })) i = 0 while i < len(args): @@ -139,13 +146,21 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat "warehouse": args[i].warehouse, "posting_date": args[i].posting_date, "posting_time": args[i].posting_time, - "creation": args[i].get("creation") + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - for item_wh, new_sle in iteritems(obj.new_items): - if item_wh not in distinct_item_warehouses: - args.append(new_sle) + distinct_item_warehouses[(args[i].item_code, args[i].warehouse)].reposting_status = True + if obj.new_items_found: + for item_wh, data in iteritems(distinct_item_warehouses): + if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + data.args_idx = len(args) + args.append(data.sle) + elif data.sle_changed and not data.reposting_status: + args[data.args_idx] = data.sle + + data.sle_changed = False i += 1 def get_args_for_voucher(voucher_type, voucher_no): @@ -186,11 +201,12 @@ class update_entries_after(object): self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() self.valuation_method = get_valuation_method(self.item_code) - self.new_items = {} + + self.new_items_found = False + self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.data = frappe._dict() self.initialize_previous_data(self.args) - self.build() def get_precision(self): @@ -296,11 +312,29 @@ class update_entries_after(object): elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: return entries_to_fix elif dependant_sle.item_code != self.item_code: - if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items: - self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle + self.update_distinct_item_warehouses(dependant_sle) return entries_to_fix elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix + else: + return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + + def update_distinct_item_warehouses(self, dependant_sle): + key = (dependant_sle.item_code, dependant_sle.warehouse) + val = frappe._dict({ + "sle": dependant_sle + }) + if key not in self.distinct_item_warehouses: + self.distinct_item_warehouses[key] = val + self.new_items_found = True + else: + existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): + val.sle_changed = True + self.distinct_item_warehouses[key] = val + self.new_items_found = True + + def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) args = self.data[dependant_sle.warehouse].previous_sle \ @@ -393,6 +427,7 @@ class update_entries_after(object): rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": + self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): @@ -442,7 +477,11 @@ class update_entries_after(object): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True) + if not sle.dependant_sle_voucher_detail_no: + self.recalculate_amounts_in_stock_entry(sle.voucher_no) + + def recalculate_amounts_in_stock_entry(self, voucher_no): + stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: From 0003938f2bba56ab1fad31d3b16ad87178194f19 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Jul 2021 13:24:43 +0530 Subject: [PATCH 068/951] refactor: Optimized code for reposting item valuation --- erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py | 1 + 1 file changed, 1 insertion(+) 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 0febcb68910..cb939e63c28 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -178,3 +178,4 @@ def on_doctype_update(): frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) + frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"]) From 9965af166e5e03899fc0629ec8d6835f5f7b6cdd Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 16 Jun 2021 19:03:27 +0530 Subject: [PATCH 069/951] feat: details fetched from supplier group in supplier --- erpnext/buying/doctype/supplier/supplier.js | 13 +++++++++++++ erpnext/buying/doctype/supplier/supplier.py | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 4ddc458175b..af6401b3fe6 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -60,10 +60,23 @@ frappe.ui.form.on("Supplier", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Supplier Group Details'), function () { + frm.trigger("get_supplier_group_details"); + }, __('Actions')); + // indicators erpnext.utils.set_party_dashboard_indicators(frm); } }, + get_supplier_group_details: function(frm) { + frappe.call({ + method: "get_supplier_group_details", + doc: frm.doc, + callback: function(r){ + frm.refresh() + } + }); + }, is_internal_supplier: function(frm) { if (frm.doc.is_internal_supplier == 1) { diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index edeb135d951..791f71ed3b0 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -51,6 +51,23 @@ class Supplier(TransactionBase): validate_party_accounts(self) self.validate_internal_supplier() + @frappe.whitelist() + def get_supplier_group_details(self): + doc = frappe.get_doc('Supplier Group', self.supplier_group) + self.payment_terms = "" + self.accounts = [] + + if not self.accounts and doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + self.save() + + if not self.payment_terms and doc.payment_terms: + self.payment_terms = doc.payment_terms + + def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") @@ -86,4 +103,4 @@ class Supplier(TransactionBase): create_contact(supplier, 'Supplier', doc.name, args.get('supplier_email_' + str(i))) except frappe.NameError: - pass \ No newline at end of file + pass From 1cb2af00a84534983cf086fef7a9118e0ecb10b6 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 17 Jun 2021 15:48:55 +0530 Subject: [PATCH 070/951] feat: details fetched from customer group in customer --- erpnext/selling/doctype/customer/customer.js | 17 ++++++++++++- erpnext/selling/doctype/customer/customer.py | 26 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 825b170a901..91944adef3b 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -130,6 +130,10 @@ frappe.ui.form.on("Customer", { erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); }, __('Create')); + frm.add_custom_button(__('Get Customer Group Details'), function () { + frm.trigger("get_customer_group_details"); + }, __('Actions')); + // indicator erpnext.utils.set_party_dashboard_indicators(frm); @@ -145,4 +149,15 @@ frappe.ui.form.on("Customer", { if(frm.doc.lead_name) frappe.model.clear_doc("Lead", frm.doc.lead_name); }, -}); \ No newline at end of file + get_customer_group_details: function(frm) { + frappe.call({ + method: "get_customer_group_details", + doc: frm.doc, + callback: function(r){ + frm.refresh() + } + }); + + } +}); + diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 818888c0c12..cdeb0896189 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -78,6 +78,32 @@ class Customer(TransactionBase): if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) + @frappe.whitelist() + def get_customer_group_details(self): + doc = frappe.get_doc('Customer Group', self.customer_group) + self.accounts = self.credit_limits = [] + self.payment_terms = self.default_price_list = "" + + if not self.accounts and doc.accounts: + for account in doc.accounts: + child = self.append('accounts') + child.company = account.company + child.account = account.account + self.save() + + if not self.credit_limits and doc.credit_limits: + for credit in doc.credit_limits: + child = self.append('credit_limits') + child.company = credit.company + child.credit_limit = credit.credit_limit + self.save() + + if not self.payment_terms and doc.payment_terms: + self.payment_terms = doc.payment_terms + + if not self.default_price_list and doc.default_price_list: + self.default_price_list = doc.default_price_list + def check_customer_group_change(self): frappe.flags.customer_group_changed = False From f07f7e9305d70a02a931cde8a720e8c8682ed2b4 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 18 Jun 2021 18:53:28 +0530 Subject: [PATCH 071/951] test: test case for fetching supplier group details --- .../buying/doctype/supplier/test_supplier.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index f9c8d35518d..faa813aa4c4 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -13,6 +13,26 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): + def test_get_supplier_group_details(self): + doc = frappe.get_doc("Supplier Group", "Local") + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + doc.append("accounts", test_account_details) + doc.save() + doc = frappe.get_doc("Supplier", "_Test Supplier") + doc.supplier_group = "Local" + doc.payment_terms = "" + doc.accounts = [] + doc.save() + doc.get_supplier_group_details() + self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(doc.accounts[0].company, "_Test Company") + self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date frappe.db.set_value( @@ -136,4 +156,4 @@ def create_supplier(**args): return doc except frappe.DuplicateEntryError: - return frappe.get_doc("Supplier", args.supplier_name) \ No newline at end of file + return frappe.get_doc("Supplier", args.supplier_name) From fedee0e8da2f0a4f60bbd06fbb22ff4d46e7def6 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 18 Jun 2021 19:13:18 +0530 Subject: [PATCH 072/951] test: test cases for fetching customer group details --- .../selling/doctype/customer/test_customer.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7761aa70fb2..8cb07aaa8ab 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -27,6 +27,38 @@ class TestCustomer(unittest.TestCase): def tearDown(self): set_credit_limit('_Test Customer', '_Test Company', 0) + def test_get_customer_group_details(self): + doc = frappe.get_doc("Customer Group", "Commercial") + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + doc.default_price_list = "Standard Buying" + doc.credit_limits = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + test_credit_limits = { + "company": "_Test Company", + "credit_limit": 350000 + } + doc.append("accounts", test_account_details) + doc.append("credit_limits", test_credit_limits) + doc.save() + + doc = frappe.get_doc("Customer", "_Test Customer") + doc.customer_group = "Commercial" + doc.payment_terms = doc.default_price_list = "" + doc.accounts = doc.credit_limits= [] + doc.save() + doc.get_customer_group_details() + self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + + self.assertEqual(doc.accounts[0].company, "_Test Company") + self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + + self.assertEqual(doc.credit_limits[0].company, "_Test Company") + self.assertEqual(doc.credit_limits[0].credit_limit, 350000 ) + def test_party_details(self): from erpnext.accounts.party import get_party_details From dd0a8f20e27555d59c52c25f87dbae450b29321c Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 2 Jul 2021 19:35:50 +0530 Subject: [PATCH 073/951] test: updated test cases --- .../buying/doctype/supplier/test_supplier.py | 24 ++++++++------- .../selling/doctype/customer/test_customer.py | 30 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index faa813aa4c4..89804662700 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -14,7 +14,8 @@ test_records = frappe.get_test_records('Supplier') class TestSupplier(unittest.TestCase): def test_get_supplier_group_details(self): - doc = frappe.get_doc("Supplier Group", "Local") + doc = frappe.new_doc("Supplier Group") + doc.supplier_group_name = "_Testing Supplier Group" doc.payment_terms = "_Test Payment Term Template 3" doc.accounts = [] test_account_details = { @@ -23,15 +24,18 @@ class TestSupplier(unittest.TestCase): } doc.append("accounts", test_account_details) doc.save() - doc = frappe.get_doc("Supplier", "_Test Supplier") - doc.supplier_group = "Local" - doc.payment_terms = "" - doc.accounts = [] - doc.save() - doc.get_supplier_group_details() - self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(doc.accounts[0].company, "_Test Company") - self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + s_doc = frappe.new_doc("Supplier") + s_doc.supplier_name = "Testing Supplier" + s_doc.supplier_group = "_Testing Supplier Group" + s_doc.payment_terms = "" + s_doc.accounts = [] + s_doc.insert() + s_doc.get_supplier_group_details() + self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(s_doc.accounts[0].company, "_Test Company") + self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") + s_doc.delete() + doc.delete() def test_supplier_default_payment_terms(self): # Payment Term based on Days after invoice date diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 8cb07aaa8ab..b1a5b52f963 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -28,7 +28,8 @@ class TestCustomer(unittest.TestCase): set_credit_limit('_Test Customer', '_Test Company', 0) def test_get_customer_group_details(self): - doc = frappe.get_doc("Customer Group", "Commercial") + doc = frappe.new_doc("Customer Group") + doc.customer_group_name = "_Testing Customer Group" doc.payment_terms = "_Test Payment Term Template 3" doc.accounts = [] doc.default_price_list = "Standard Buying" @@ -43,21 +44,24 @@ class TestCustomer(unittest.TestCase): } doc.append("accounts", test_account_details) doc.append("credit_limits", test_credit_limits) - doc.save() + doc.insert() - doc = frappe.get_doc("Customer", "_Test Customer") - doc.customer_group = "Commercial" - doc.payment_terms = doc.default_price_list = "" - doc.accounts = doc.credit_limits= [] - doc.save() - doc.get_customer_group_details() - self.assertEqual(doc.payment_terms, "_Test Payment Term Template 3") + c_doc = frappe.new_doc("Customer") + c_doc.customer_name = "Testing Customer" + c_doc.customer_group = "_Testing Customer Group" + c_doc.payment_terms = c_doc.default_price_list = "" + c_doc.accounts = c_doc.credit_limits= [] + c_doc.insert() + c_doc.get_customer_group_details() + self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(doc.accounts[0].company, "_Test Company") - self.assertEqual(doc.accounts[0].account, "Creditors - _TC") + self.assertEqual(c_doc.accounts[0].company, "_Test Company") + self.assertEqual(c_doc.accounts[0].account, "Creditors - _TC") - self.assertEqual(doc.credit_limits[0].company, "_Test Company") - self.assertEqual(doc.credit_limits[0].credit_limit, 350000 ) + self.assertEqual(c_doc.credit_limits[0].company, "_Test Company") + self.assertEqual(c_doc.credit_limits[0].credit_limit, 350000) + c_doc.delete() + doc.delete() def test_party_details(self): from erpnext.accounts.party import get_party_details From 1e8f598ba5afdf1efce64de40f058c337ce580b5 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Fri, 2 Jul 2021 22:02:07 +0530 Subject: [PATCH 074/951] fix: Sider --- erpnext/buying/doctype/supplier/supplier.js | 4 ++-- erpnext/selling/doctype/customer/customer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index af6401b3fe6..1766c2c80cc 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -72,8 +72,8 @@ frappe.ui.form.on("Supplier", { frappe.call({ method: "get_supplier_group_details", doc: frm.doc, - callback: function(r){ - frm.refresh() + callback: function() { + frm.refresh(); } }); }, diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 91944adef3b..28494662673 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -153,8 +153,8 @@ frappe.ui.form.on("Customer", { frappe.call({ method: "get_customer_group_details", doc: frm.doc, - callback: function(r){ - frm.refresh() + callback: function() { + frm.refresh(); } }); From 74b3fc1e1cba75e61eb6e3d97051f3be29f1370c Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 12 Jul 2021 09:18:19 +0530 Subject: [PATCH 075/951] refactor: suggested changes --- erpnext/buying/doctype/supplier/supplier.py | 6 ++-- erpnext/selling/doctype/customer/customer.py | 29 +++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 791f71ed3b0..fd16b23c220 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -57,16 +57,16 @@ class Supplier(TransactionBase): self.payment_terms = "" self.accounts = [] - if not self.accounts and doc.accounts: + if doc.accounts: for account in doc.accounts: child = self.append('accounts') child.company = account.company child.account = account.account - self.save() - if not self.payment_terms and doc.payment_terms: + if doc.payment_terms: self.payment_terms = doc.payment_terms + self.save() def validate_internal_supplier(self): internal_supplier = frappe.db.get_value("Supplier", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index cdeb0896189..3b62081e24c 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -84,25 +84,22 @@ class Customer(TransactionBase): self.accounts = self.credit_limits = [] self.payment_terms = self.default_price_list = "" - if not self.accounts and doc.accounts: - for account in doc.accounts: - child = self.append('accounts') - child.company = account.company - child.account = account.account - self.save() + tables = [["accounts", "account"], ["credit_limits", "credit_limit"]] + fields = ["payment_terms", "default_price_list"] - if not self.credit_limits and doc.credit_limits: - for credit in doc.credit_limits: - child = self.append('credit_limits') - child.company = credit.company - child.credit_limit = credit.credit_limit - self.save() + for row in tables: + table, field = row[0], row[1] + if not doc.get(table): continue - if not self.payment_terms and doc.payment_terms: - self.payment_terms = doc.payment_terms + for entry in doc.get(table): + child = self.append(table) + child.update({"company": entry.company, field: entry.get(field)}) - if not self.default_price_list and doc.default_price_list: - self.default_price_list = doc.default_price_list + for field in fields: + if not doc.get(field): continue + self.update({field: doc.get(field)}) + + self.save() def check_customer_group_change(self): frappe.flags.customer_group_changed = False From 621927d9f94a7c080c24b6b194f9b58f17cee6d4 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 13 Jul 2021 14:13:39 +0530 Subject: [PATCH 076/951] fix: move the rename abbreviation job to long queue (#26462) --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 915e6a4f316..36a7d20a8ff 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -395,7 +395,7 @@ class Company(NestedSet): @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue="long", company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) From 4acbeecbbe631764d12bf1e92f4f0379133c58d4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 13 Jul 2021 11:45:41 +0530 Subject: [PATCH 077/951] fix: multi-currency issue --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++- erpnext/stock/get_item_details.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c32a8a95a17..9da461f4971 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -713,7 +713,8 @@ def get_bom_item_rate(args, bom_doc): "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function "conversion_factor": args.get("conversion_factor") or 1, "plc_conversion_rate": 1, - "ignore_party": True + "ignore_party": True, + "ignore_conversion_rate": True }) item_doc = frappe.get_cached_doc("Item", args.get("item_code")) out = frappe._dict() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index ca174a3f63c..4657700dbb4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -441,7 +441,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t if item_tax_templates is None: item_tax_templates = {} - + if item_rates is None: item_rates = {} @@ -807,10 +807,14 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate - if (not args.conversion_rate - and args.currency==frappe.get_cached_value('Company', args.company, "default_currency")): + company_currency = frappe.get_cached_value('Company', args.company, "default_currency") + if (not args.conversion_rate and args.currency==company_currency): args.conversion_rate = 1.0 + if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency): + args.conversion_rate = get_exchange_rate(args.currency, + company_currency, args.transaction_date, "for_buying") or 1.0 + # validate currency conversion rate validate_conversion_rate(args.currency, args.conversion_rate, meta.get_label("conversion_rate"), args.company) From 07d9f3f74ba83ec6d7851c78f7881af0429fe960 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 1 Jul 2021 21:17:17 +0530 Subject: [PATCH 078/951] fix: Incorrect discount amount on amended document --- erpnext/public/js/controllers/taxes_and_totals.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1de9ec1a7df..52efbb5f6cd 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -67,6 +67,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ calculate_discount_amount: function(){ if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { + this.calculate_item_values(); + this.calculate_net_total(); this.set_discount_amount(); this.apply_discount_amount(); } From 0bad696faf6e20bfcd8809cb3db56c4f503a438f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Jul 2021 10:06:38 +0530 Subject: [PATCH 079/951] fix: Unable to download GSTR-1 json --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 10961593e1c..cfcb8c3444f 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -584,7 +584,7 @@ class Gstr1Report(object): def get_json(filters, report_name, data): filters = json.loads(filters) report_data = json.loads(data) - gstin = get_company_gstin_number(filters["company"], filters["company_address"]) + gstin = get_company_gstin_number(filters.get("company"), filters.get("company_address")) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) From 7be9f8dab16a54a020d1e37541eb0392457c3bc9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Jul 2021 20:23:52 +0530 Subject: [PATCH 080/951] fix: Error on creation of company for India --- erpnext/regional/india/setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5f9d5ed0d61..5ef04b66c7d 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -122,10 +122,12 @@ def add_print_formats(): def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] + sales_invoice_series = frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") + ['SINV-.YY.-', 'SRET-.YY.-', ''] + purchase_invoice_series = frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") + ['PINV-.YY.-', 'PRET-.YY.-', ''] if not patch: - make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') - make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') + make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') + make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '') make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') def make_custom_fields(update=True): From fea29ae8cb8190bf1f043dc14e997e2d20c02356 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 12 Jul 2021 18:29:52 +0530 Subject: [PATCH 081/951] fix: Use update flag for company dependant fixtures --- erpnext/regional/india/setup.py | 11 +++++++---- erpnext/setup/doctype/company/company.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5ef04b66c7d..92654608da5 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -12,7 +12,10 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError from frappe.utils import today def setup(company=None, patch=True): - setup_company_independent_fixtures(patch=patch) + # Company independent fixtures should be called only once at the first company setup + if frappe.db.count('Company', {'country': 'India'}) <=1: + setup_company_independent_fixtures(patch=patch) + if not patch: make_fixtures(company) @@ -122,8 +125,8 @@ def add_print_formats(): def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] - sales_invoice_series = frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") + ['SINV-.YY.-', 'SRET-.YY.-', ''] - purchase_invoice_series = frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") + ['PINV-.YY.-', 'PRET-.YY.-', ''] + sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") + purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") if not patch: make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') @@ -788,7 +791,7 @@ def set_tax_withholding_category(company): doc.flags.ignore_mandatory = True doc.insert() else: - doc = frappe.get_doc("Tax Withholding Category", d.get("name")) + doc = frappe.get_doc("Tax Withholding Category", d.get("name"), for_update=True) if accounts: doc.append("accounts", accounts[0]) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 36a7d20a8ff..8755125c810 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -291,7 +291,7 @@ class Company(NestedSet): cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') if cash and self.default_cash_account \ and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): - mode_of_payment = frappe.get_doc('Mode of Payment', cash) + mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True) mode_of_payment.append('accounts', { 'company': self.name, 'default_account': self.default_cash_account From 51ae46f0de7f41c3693de885e0bc6b472249b031 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 18:04:24 +0530 Subject: [PATCH 082/951] fix: Unallocated amount in Payment Entry after taxes --- .../doctype/payment_entry/payment_entry.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0c21aae944c..20c97cf251d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -404,9 +404,15 @@ class PaymentEntry(AccountsController): if not self.advance_tax_account: frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction")) - reference_doclist = [] net_total = self.paid_amount - included_in_paid_amount = 0 + + for reference in self.get("references"): + net_total_for_tds = 0 + if reference.reference_doctype == 'Purchase Order': + net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total')) + + if net_total_for_tds: + net_total = net_total_for_tds # Adding args as purchase invoice to get TDS amount args = frappe._dict({ @@ -423,7 +429,6 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ - 'included_in_paid_amount': included_in_paid_amount, 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -509,18 +514,17 @@ class PaymentEntry(AccountsController): self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): - self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ - and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \ - and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate): - self.unallocated_amount = (self.received_amount_after_tax + total_deductions - + and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ + and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): + self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate elif self.payment_type == "Pay" \ - and self.base_total_allocated_amount < (self.base_paid_amount_after_tax - total_deductions) \ - and self.total_allocated_amount < self.received_amount_after_tax + (total_deductions / self.target_exchange_rate): - self.unallocated_amount = (self.base_paid_amount_after_tax - (total_deductions + + and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ + and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): + self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate def set_difference_amount(self): @@ -530,11 +534,11 @@ class PaymentEntry(AccountsController): base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount_after_tax + self.difference_amount = base_party_amount - self.base_received_amount elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount_after_tax - base_party_amount + self.difference_amount = self.base_paid_amount - base_party_amount else: - self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax) + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) @@ -683,8 +687,8 @@ class PaymentEntry(AccountsController): "account": self.paid_from, "account_currency": self.paid_from_account_currency, "against": self.party if self.payment_type=="Pay" else self.paid_to, - "credit_in_account_currency": self.paid_amount_after_tax, - "credit": self.base_paid_amount_after_tax, + "credit_in_account_currency": self.paid_amount, + "credit": self.base_paid_amount, "cost_center": self.cost_center }, item=self) ) @@ -694,8 +698,8 @@ class PaymentEntry(AccountsController): "account": self.paid_to, "account_currency": self.paid_to_account_currency, "against": self.party if self.payment_type=="Receive" else self.paid_from, - "debit_in_account_currency": self.received_amount_after_tax, - "debit": self.base_received_amount_after_tax, + "debit_in_account_currency": self.received_amount, + "debit": self.base_received_amount, "cost_center": self.cost_center }, item=self) ) @@ -708,15 +712,17 @@ class PaymentEntry(AccountsController): if self.payment_type in ('Pay', 'Internal Transfer'): dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit" + against = self.party or self.paid_from elif self.payment_type == 'Receive': dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit" + against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() gl_entries.append( self.get_gl_dict({ "account": d.account_head, - "against": self.party if self.payment_type=="Receive" else self.paid_from, + "against": against, dr_or_cr: d.base_tax_amount, dr_or_cr + "_in_account_currency": d.base_tax_amount if account_currency==self.company_currency @@ -728,14 +734,12 @@ class PaymentEntry(AccountsController): gl_entries.append( self.get_gl_dict({ "account": payment_or_advance_account, - "against": self.party if self.payment_type=="Receive" else self.paid_from, + "against": against, dr_or_cr: -1 * d.base_tax_amount, dr_or_cr + "_in_account_currency": -1*d.base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, - "party_type": self.party_type, - "party": self.party }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): @@ -760,9 +764,9 @@ class PaymentEntry(AccountsController): if self.advance_tax_account: return self.advance_tax_account elif self.payment_type == 'Receive': - return self.paid_from - elif self.payment_type in ('Pay', 'Internal Transfer'): return self.paid_to + elif self.payment_type in ('Pay', 'Internal Transfer'): + return self.paid_from def update_advance_paid(self): if self.payment_type in ("Receive", "Pay") and self.party: @@ -1634,12 +1638,6 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta if dt == "Employee Advance": paid_amount = received_amount * doc.get('exchange_rate', 1) - if dt == "Purchase Order" and doc.apply_tds: - if party_account_currency == bank.account_currency: - paid_amount = received_amount = doc.base_net_total - else: - paid_amount = received_amount = doc.base_net_total * doc.get('exchange_rate', 1) - return paid_amount, received_amount def apply_early_payment_discount(paid_amount, received_amount, doc): From 9513d61a50ea3792d627f76fd92a0fb87f00a793 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 18:52:12 +0530 Subject: [PATCH 083/951] fix: Hide amount after tax fields --- .../accounts/doctype/payment_entry/payment_entry.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 51f18a5a4e3..6f362c1fbb9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -667,6 +667,7 @@ { "fieldname": "base_paid_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Paid Amount After Tax (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 @@ -693,21 +694,25 @@ "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "fieldname": "received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax", - "options": "paid_to_account_currency" + "options": "paid_to_account_currency", + "read_only": 1 }, { "depends_on": "doc.received_amount", "fieldname": "base_received_amount_after_tax", "fieldtype": "Currency", + "hidden": 1, "label": "Received Amount After Tax (Company Currency)", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-22 20:37:06.154206", + "modified": "2021-07-09 08:58:15.008761", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", From 2f350bf4507eafb5d7dc25ee26f4a68f2214ac1a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 20:00:55 +0530 Subject: [PATCH 084/951] fix: Remove unintentional changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 20c97cf251d..e7feebacf80 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -521,6 +521,7 @@ class PaymentEntry(AccountsController): and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + print(self.unallocated_amount, "#@#@#@#@#") elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): From 01c89eaad9b94559ec4e9e200d6a5a7901e66a46 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 9 Jul 2021 20:08:29 +0530 Subject: [PATCH 085/951] fix: Remove unintentional changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e7feebacf80..7f53ca91d5e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -514,6 +514,7 @@ class PaymentEntry(AccountsController): self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): + self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) if self.payment_type == "Receive" \ @@ -521,7 +522,6 @@ class PaymentEntry(AccountsController): and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate - print(self.unallocated_amount, "#@#@#@#@#") elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): From 740d5c6c5309bc19003b159c0848157f78d0bcf7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 12 Jul 2021 22:11:57 +0530 Subject: [PATCH 086/951] fix: Deduct included taxes from unallocated amount --- .../doctype/payment_entry/payment_entry.py | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7f53ca91d5e..835601e5349 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -517,16 +517,19 @@ class PaymentEntry(AccountsController): self.unallocated_amount = 0 if self.party: total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() if self.payment_type == "Receive" \ and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): self.unallocated_amount = (self.received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate + self.unallocated_amount -= included_taxes elif self.payment_type == "Pay" \ and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) \ and self.total_allocated_amount < self.received_amount + (total_deductions / self.target_exchange_rate): self.unallocated_amount = (self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)) / self.target_exchange_rate + self.unallocated_amount -= included_taxes def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) @@ -542,10 +545,22 @@ class PaymentEntry(AccountsController): self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) total_deductions = sum(flt(d.amount) for d in self.get("deductions")) + included_taxes = self.get_included_taxes() - self.difference_amount = flt(self.difference_amount - total_deductions, + self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")) + def get_included_taxes(self): + included_taxes = 0 + for tax in self.get('taxes'): + if tax.included_in_paid_amount: + if tax.add_deduct_tax == 'Add': + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount + + return included_taxes + # Paid amount is auto allocated in the reference document by default. # Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast def clear_unallocated_reference_document_rows(self): @@ -719,6 +734,10 @@ class PaymentEntry(AccountsController): against = self.party or self.paid_to payment_or_advance_account = self.get_party_account_for_taxes() + tax_amount = d.tax_amount + + if self.advance_tax_account: + tax_amount = -1* tax_amount gl_entries.append( self.get_gl_dict({ @@ -732,16 +751,17 @@ class PaymentEntry(AccountsController): }, account_currency, item=d)) #Intentionally use -1 to get net values in party account - gl_entries.append( - self.get_gl_dict({ - "account": payment_or_advance_account, - "against": against, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount - if account_currency==self.company_currency - else d.tax_amount, - "cost_center": self.cost_center, - }, account_currency, item=d)) + if not d.included_in_paid_amount or self.advance_tax_account: + gl_entries.append( + self.get_gl_dict({ + "account": payment_or_advance_account, + "against": against, + dr_or_cr: -1 * d.base_tax_amount, + dr_or_cr + "_in_account_currency": -1*d.base_tax_amount + if account_currency==self.company_currency + else d.tax_amount, + "cost_center": self.cost_center, + }, account_currency, item=d)) def add_deductions_gl_entries(self, gl_entries): for d in self.get("deductions"): From c00d851a88d8dd0f326de765c031e5790a341fbd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Jul 2021 11:22:55 +0530 Subject: [PATCH 087/951] fix: Unallocated amount for inclusive charges --- .../accounts/doctype/payment_entry/payment_entry.py | 13 ++++++++----- erpnext/controllers/accounts_controller.py | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 835601e5349..7f665db2f9b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -429,6 +429,7 @@ class PaymentEntry(AccountsController): return tax_withholding_details.update({ + 'add_deduct_tax': 'Add', 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) }) @@ -735,16 +736,18 @@ class PaymentEntry(AccountsController): payment_or_advance_account = self.get_party_account_for_taxes() tax_amount = d.tax_amount + base_tax_amount = d.base_tax_amount if self.advance_tax_account: - tax_amount = -1* tax_amount + tax_amount = -1 * tax_amount + base_tax_amount = -1 * base_tax_amount gl_entries.append( self.get_gl_dict({ "account": d.account_head, "against": against, - dr_or_cr: d.base_tax_amount, - dr_or_cr + "_in_account_currency": d.base_tax_amount + dr_or_cr: tax_amount, + dr_or_cr + "_in_account_currency": base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": d.cost_center @@ -756,8 +759,8 @@ class PaymentEntry(AccountsController): self.get_gl_dict({ "account": payment_or_advance_account, "against": against, - dr_or_cr: -1 * d.base_tax_amount, - dr_or_cr + "_in_account_currency": -1*d.base_tax_amount + dr_or_cr: -1 * tax_amount, + dr_or_cr + "_in_account_currency": -1 * base_tax_amount if account_currency==self.company_currency else d.tax_amount, "cost_center": self.cost_center, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1c086e9edcd..5d30b65a1ec 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -751,11 +751,11 @@ class AccountsController(TransactionBase): account_currency = get_account_currency(tax.account_head) if self.doctype == "Purchase Invoice": - dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" - else: dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + else: + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" + rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" party = self.supplier if self.doctype == "Purchase Invoice" else self.customer unallocated_amount = tax.tax_amount - tax.allocated_amount From 59bf2df28e95259fd0046df24a2d33789b850f06 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 14 Jul 2021 11:40:58 +0530 Subject: [PATCH 088/951] fix: pos item cart dom updates (#26461) --- .../selling/page/point_of_sale/pos_item_cart.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 38508c219b3..f7b2c1d93c3 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -965,8 +965,23 @@ erpnext.PointOfSale.ItemCart = class { }); } + attach_refresh_field_event(frm) { + $(frm.wrapper).off('refresh-fields'); + $(frm.wrapper).on('refresh-fields', () => { + if (frm.doc.items.length) { + frm.doc.items.forEach(item => { + this.update_item_html(item); + }); + } + this.update_totals_section(frm); + }); + } + load_invoice() { const frm = this.events.get_frm(); + + this.attach_refresh_field_event(frm); + this.fetch_customer_details(frm.doc.customer).then(() => { this.events.customer_details_updated(this.customer_info); this.update_customer_section(); From 72715956f125c45ff3916506a0505f08d4645812 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 14 Jul 2021 15:56:20 +0530 Subject: [PATCH 089/951] fix: tds computation summary shows cancelled invoices (#26486) --- .../report/tds_computation_summary/tds_computation_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index e15715dccd8..6b9df41f54e 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -75,7 +75,8 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f select voucher_no, credit from `tabGL Entry` where party in (%s) and credit > 0 - and company=%s and posting_date between %s and %s + and company=%s and is_cancelled = 0 + and posting_date between %s and %s """, (supplier, company, from_date, to_date), as_dict=1) supplier_credit_amount = flt(sum(d.credit for d in entries)) From 03f4db0606cc307c47a253eb4dc91fa230f96d93 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Wed, 14 Jul 2021 16:28:54 +0530 Subject: [PATCH 090/951] fix: validation check for batch for stock reconciliation type in stock entry(bp #26370 ) (#26488) * fix(ux): added filter for valid batch nos. * fix: not validating batch no if entry type stock reconciliation * test: validate batch_no --- .../stock_ledger_entry/stock_ledger_entry.py | 19 ++++++++--------- .../stock_reconciliation.js | 8 +++++++ .../test_stock_reconciliation.py | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) 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 cb939e63c28..93482e8beab 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -89,17 +89,16 @@ class StockLedgerEntry(Document): if item_det.is_stock_item != 1: frappe.throw(_("Item {0} must be a stock Item").format(self.item_code)) - # check if batch number is required - if self.voucher_type != 'Stock Reconciliation': - if item_det.has_batch_no == 1: - batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name - if not self.batch_no: - frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) - elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): - frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) + # check if batch number is valid + if item_det.has_batch_no == 1: + batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name + if not self.batch_no: + frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) + elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): + frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) - elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: - frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) + elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: + frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index a01db80da4a..349e59f31d1 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -17,6 +17,14 @@ frappe.ui.form.on("Stock Reconciliation", { } } }); + frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + filters: { + 'item': item.item_code + } + }; + }); if (frm.doc.company) { erpnext.queries.setup_queries(frm, "Warehouse", function() { diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 84cdc491282..c192582531a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -16,6 +16,7 @@ from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valua from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + class TestStockReconciliation(unittest.TestCase): @classmethod def setUpClass(self): @@ -352,6 +353,26 @@ class TestStockReconciliation(unittest.TestCase): dn2.cancel() pr1.cancel() + def test_valid_batch(self): + create_batch_item_with_batch("Testing Batch Item 1", "001") + create_batch_item_with_batch("Testing Batch Item 2", "002") + sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" + , do_not_submit=True) + self.assertRaises(frappe.ValidationError, sr.submit) + +def create_batch_item_with_batch(item_name, batch_id): + batch_item_doc = create_item(item_name, is_stock_item=1) + if not batch_item_doc.has_batch_no: + batch_item_doc.has_batch_no = 1 + batch_item_doc.create_new_batch = 1 + batch_item_doc.save(ignore_permissions=True) + + if not frappe.db.exists('Batch', batch_id): + b = frappe.new_doc('Batch') + b.item = item_name + b.batch_id = batch_id + b.save() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry From b24c0bfbf933799811cfdc31c0658506261af76d Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 13 Jul 2021 15:34:25 +0530 Subject: [PATCH 091/951] fix: show child item group items on portal --- erpnext/setup/doctype/item_group/item_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 1c72cebfa9d..5fcad00af16 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -87,8 +87,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): if not field_filters: field_filters = {} - # Ensure the query remains within current item group - field_filters['item_group'] = self.name + # Ensure the query remains within current item group & sub group + field_filters['item_group'] = [ig[0] for ig in get_child_groups(self.name)] engine = ProductQuery() context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name) From 9ea5072c527b2aed4206c0e747ec5f0474992165 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 13 Jul 2021 17:27:55 +0530 Subject: [PATCH 092/951] fix: set item group as a persistent filter --- erpnext/portal/product_configurator/utils.py | 6 ++++++ erpnext/templates/generators/item_group.html | 2 +- erpnext/www/all-products/index.js | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index d77eb2c3966..211b94a9cfd 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -2,6 +2,7 @@ import frappe from frappe.utils import cint from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.shopping_cart.product_info import get_product_info_for_website +from erpnext.setup.doctype.item_group.item_group import get_child_groups def get_field_filter_data(): product_settings = get_product_settings() @@ -89,6 +90,7 @@ def get_products_for_website(field_filters=None, attribute_filters=None, search= def get_products_html_for_website(field_filters=None, attribute_filters=None): field_filters = frappe.parse_json(field_filters) attribute_filters = frappe.parse_json(attribute_filters) + set_item_group_filters(field_filters) items = get_products_for_website(field_filters, attribute_filters) html = ''.join(get_html_for_items(items)) @@ -98,6 +100,10 @@ def get_products_html_for_website(field_filters=None, attribute_filters=None): return html +def set_item_group_filters(field_filters): + if 'item_group' in field_filters: + field_filters['item_group'] = [ig[0] for ig in get_child_groups(field_filters['item_group'])] + def get_item_codes_by_attributes(attribute_filters, template_item_code=None): items = [] diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 393c3a43afb..95eb8f493f6 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -9,7 +9,7 @@ {% endblock %} {% block page_content %} -
+
{% if slideshow %} {{ web_block( diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index 0721056816b..1c641b59ad1 100644 --- a/erpnext/www/all-products/index.js +++ b/erpnext/www/all-products/index.js @@ -124,6 +124,10 @@ $(() => { attribute_filters: if_key_exists(attribute_filters) }; + const item_group = $(".item-group-content").data('item-group'); + if (item_group) { + Object.assign(field_filters, { item_group }); + } return new Promise((resolve, reject) => { frappe.call('erpnext.portal.product_configurator.utils.get_products_html_for_website', args) .then(r => { From f40da4ac4c7d011eefef9b7189330217078c9b98 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 14 Jul 2021 20:01:36 +0530 Subject: [PATCH 093/951] fix: Paging buttons not working on item group portal page --- erpnext/templates/generators/item_group.html | 29 +++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 95eb8f493f6..9050cc388ae 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -127,15 +127,36 @@
-
-
+
+
+
+
{% if frappe.form_dict.start|int > 0 %} - + {% endif %} {% if items|length >= page_length %} - + {% endif %}
+ + {% endblock %} \ No newline at end of file From 9997cce1eaa6e17ce0738a1adf78e99e07b817da Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Jul 2021 14:08:58 +0530 Subject: [PATCH 094/951] fix: FG item not fetched in manufacture entry --- .../doctype/work_order/test_work_order.py | 54 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 22 +++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 68de0b29d3e..bf1ccb71594 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -513,6 +513,60 @@ class TestWorkOrder(unittest.TestCase): work_order1.save() self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def test_batch_size_for_fg_item(self): + fg_item = "Test Batch Size Item For BOM 3" + rm1 = "Test Batch Size Item RM 1 For BOM 3" + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]: + item_args = { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + } + + if item == fg_item: + item_args['has_batch_no'] = 1 + item_args['create_new_batch'] = 1 + item_args['batch_number_series'] = 'TBSI3.#####' + + make_item(item, item_args) + + bom_name = frappe.db.get_value("BOM", + {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + + if not bom_name: + bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom.save() + bom.submit() + bom_name = bom.name + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + + work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), + qty=30, do_not_save = True) + work_order.batch_size = 10 + work_order.insert() + work_order.submit() + self.assertEqual(work_order.has_batch_no, 1) + ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30)) + for row in ste1.get('items'): + if row.is_finished_item: + self.assertEqual(row.item_code, fg_item) + self.assertEqual(row.qty, 10) + + frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + def test_partial_material_consumption(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 90b81ddb1dc..c9838d75f1d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1090,13 +1090,13 @@ class StockEntry(StockController): "is_finished_item": 1 } - if self.work_order and self.pro_doc.has_batch_no: + if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', + 'make_serial_no_batch_from_work_order', cache=True)): self.set_batchwise_finished_goods(args, item) else: - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) def set_batchwise_finished_goods(self, args, item): - qty = flt(self.fg_completed_qty) filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, @@ -1105,7 +1105,17 @@ class StockEntry(StockController): fields = ["qty_to_produce as qty", "produced_qty", "name"] - for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"): + data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + + if not data: + self.add_finished_goods(args, item) + else: + self.add_batchwise_finished_good(data, args, item) + + def add_batchwise_finished_good(self, data, args, item): + qty = flt(self.fg_completed_qty) + + for row in data: batch_qty = flt(row.qty) - flt(row.produced_qty) if not batch_qty: continue @@ -1121,9 +1131,9 @@ class StockEntry(StockController): args["qty"] = fg_qty args["batch_no"] = row.name - self.add_finisged_goods(args, item) + self.add_finished_goods(args, item) - def add_finisged_goods(self, args, item): + def add_finished_goods(self, args, item): self.add_to_stock_entry_detail({ item.name: args }, bom_no = self.bom_no) From 2f775ee53cb115511cd3be5d9d09f505bf46e656 Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Thu, 15 Jul 2021 16:29:28 +0530 Subject: [PATCH 095/951] fix: WIP needs to be set before submit on skip_transfer (#26507) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 779ae42d653..0a8e5329c15 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -239,7 +239,7 @@ class WorkOrder(Document): self.create_serial_no_batch_no() def on_submit(self): - if not self.wip_warehouse: + if not self.wip_warehouse and not self.skip_transfer: frappe.throw(_("Work-in-Progress Warehouse is required before Submit")) if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) From a0df79ee8c1e7d251e9f5f24c11bbb21b22621da Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 16 Jul 2021 13:07:39 +0530 Subject: [PATCH 096/951] chore: Added change log for v13.7.0 --- erpnext/change_log/v13/v13_7_0.md | 69 +++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 erpnext/change_log/v13/v13_7_0.md diff --git a/erpnext/change_log/v13/v13_7_0.md b/erpnext/change_log/v13/v13_7_0.md new file mode 100644 index 00000000000..589f610b939 --- /dev/null +++ b/erpnext/change_log/v13/v13_7_0.md @@ -0,0 +1,69 @@ +# Version 13.7.0 Release Notes + +### Features & Enhancements +- Optionally allow rejected quality inspection on submission ([#26133](https://github.com/frappe/erpnext/pull/26133)) +- Bootstrapped GST Setup for India ([#25415](https://github.com/frappe/erpnext/pull/25415)) +- Fetching details from supplier/customer groups ([#26454](https://github.com/frappe/erpnext/pull/26454)) +- Provision to make subcontracted purchase order from the production plan ([#26240](https://github.com/frappe/erpnext/pull/26240)) +- Optimized code for reposting item valuation ([#26432](https://github.com/frappe/erpnext/pull/26432)) + +### Fixes +- Auto process deferred accounting for multi-company setup ([#26277](https://github.com/frappe/erpnext/pull/26277)) +- Error while fetching item taxes ([#26218](https://github.com/frappe/erpnext/pull/26218)) +- Validation check for batch for stock reconciliation type in stock entry(bp #26370 ) ([#26488](https://github.com/frappe/erpnext/pull/26488)) +- Error popup for COA errors ([#26358](https://github.com/frappe/erpnext/pull/26358)) +- Precision for expected values in payment entry test ([#26394](https://github.com/frappe/erpnext/pull/26394)) +- Bank statement import ([#26287](https://github.com/frappe/erpnext/pull/26287)) +- LMS progress issue ([#26253](https://github.com/frappe/erpnext/pull/26253)) +- Paging buttons not working on item group portal page ([#26497](https://github.com/frappe/erpnext/pull/26497)) +- Omit item discount amount for e-invoicing ([#26353](https://github.com/frappe/erpnext/pull/26353)) +- Validate LCV for Invoices without Update Stock ([#26333](https://github.com/frappe/erpnext/pull/26333)) +- Remove cancelled entries in consolidated financial statements ([#26331](https://github.com/frappe/erpnext/pull/26331)) +- Fetching employee in payroll entry ([#26271](https://github.com/frappe/erpnext/pull/26271)) +- To fetch the correct field in Tax Rule ([#25927](https://github.com/frappe/erpnext/pull/25927)) +- Order and time of operations in multilevel BOM work order ([#25886](https://github.com/frappe/erpnext/pull/25886)) +- Fixed Budget Variance Graph color from all black to default ([#26368](https://github.com/frappe/erpnext/pull/26368)) +- TDS computation summary shows cancelled invoices (#26456) ([#26486](https://github.com/frappe/erpnext/pull/26486)) +- Do not consider cancelled entries in party dashboard ([#26231](https://github.com/frappe/erpnext/pull/26231)) +- Add validation for 'for_qty' else throws errors ([#25829](https://github.com/frappe/erpnext/pull/25829)) +- Move the rename abbreviation job to long queue (#26434) ([#26462](https://github.com/frappe/erpnext/pull/26462)) +- Query for Training Event ([#26388](https://github.com/frappe/erpnext/pull/26388)) +- Item group portal issues (backport) ([#26493](https://github.com/frappe/erpnext/pull/26493)) +- When lead is created with mobile_no, mobile_no value gets lost ([#26298](https://github.com/frappe/erpnext/pull/26298)) +- WIP needs to be set before submit on skip_transfer (bp #26499) ([#26507](https://github.com/frappe/erpnext/pull/26507)) +- Incorrect valuation rate in stock reconciliation ([#26259](https://github.com/frappe/erpnext/pull/26259)) +- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046)) +- Changed profitability analysis report width ([#26165](https://github.com/frappe/erpnext/pull/26165)) +- Unable to download GSTR-1 json ([#26468](https://github.com/frappe/erpnext/pull/26468)) +- Unallocated amount in Payment Entry after taxes ([#26472](https://github.com/frappe/erpnext/pull/26472)) +- Include Stock Reco logic in `update_qty_in_future_sle` ([#26158](https://github.com/frappe/erpnext/pull/26158)) +- Update cost not working in the draft BOM ([#26279](https://github.com/frappe/erpnext/pull/26279)) +- Cancellation of Loan Security Pledges ([#26252](https://github.com/frappe/erpnext/pull/26252)) +- fix(e-invoicing): allow export invoice even if no taxes applied (#26363) ([#26405](https://github.com/frappe/erpnext/pull/26405)) +- Delete accounts (an empty file) ([#25323](https://github.com/frappe/erpnext/pull/25323)) +- Errors on parallel requests creation of company for India ([#26470](https://github.com/frappe/erpnext/pull/26470)) +- Incorrect bom no added for non-variant items on variant boms ([#26320](https://github.com/frappe/erpnext/pull/26320)) +- Incorrect discount amount on amended document ([#26466](https://github.com/frappe/erpnext/pull/26466)) +- Added a message to enable appointment booking if disabled ([#26334](https://github.com/frappe/erpnext/pull/26334)) +- fix(pos): taxes amount in pos item cart ([#26411](https://github.com/frappe/erpnext/pull/26411)) +- Track changes on batch ([#26382](https://github.com/frappe/erpnext/pull/26382)) +- Stock entry with putaway rule not working ([#26350](https://github.com/frappe/erpnext/pull/26350)) +- Only "Tax" type accounts should be shown for selection in GST Settings ([#26300](https://github.com/frappe/erpnext/pull/26300)) +- Added permission for employee to book appointment ([#26255](https://github.com/frappe/erpnext/pull/26255)) +- Allow to make job card without employee ([#26312](https://github.com/frappe/erpnext/pull/26312)) +- Project Portal Enhancements ([#26290](https://github.com/frappe/erpnext/pull/26290)) +- BOM stock report not working ([#26332](https://github.com/frappe/erpnext/pull/26332)) +- Order Items by weightage in the web items query ([#26284](https://github.com/frappe/erpnext/pull/26284)) +- Removed values out of sync validation from stock transactions ([#26226](https://github.com/frappe/erpnext/pull/26226)) +- Payroll-entry minor fix ([#26349](https://github.com/frappe/erpnext/pull/26349)) +- Allow user to change the To Date in the blanket order even after submit of order ([#26241](https://github.com/frappe/erpnext/pull/26241)) +- Value fetching for custom field in POS ([#26367](https://github.com/frappe/erpnext/pull/26367)) +- Iteration through accounts only when accounts exist ([#26391](https://github.com/frappe/erpnext/pull/26391)) +- Employee Inactive status implications ([#26244](https://github.com/frappe/erpnext/pull/26244)) +- Multi-currency issue ([#26458](https://github.com/frappe/erpnext/pull/26458)) +- FG item not fetched in manufacture entry ([#26509](https://github.com/frappe/erpnext/pull/26509)) +- Set query for training events ([#26303](https://github.com/frappe/erpnext/pull/26303)) +- Fetch batch items in stock reconciliation ([#26213](https://github.com/frappe/erpnext/pull/26213)) +- Employee selection not working in payroll entry ([#26278](https://github.com/frappe/erpnext/pull/26278)) +- POS item cart dom updates (#26459) ([#26461](https://github.com/frappe/erpnext/pull/26461)) +- dunning calculation of grand total when rate of interest is 0% ([#26285](https://github.com/frappe/erpnext/pull/26285)) \ No newline at end of file From 0c6ca09e06faf53da276af64b8a16a441bf7e6ef Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Jul 2021 16:32:23 +0530 Subject: [PATCH 097/951] fix: added patch to fix missing FG item --- erpnext/patches.txt | 1 + .../add_missing_fg_item_for_stock_entry.py | 110 ++++++++++++++++++ .../repost_item_valuation.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 4 + 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 29376f00a1c..f63c7edea2f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -291,3 +291,4 @@ 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.add_missing_fg_item_for_stock_entry diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py new file mode 100644 index 00000000000..48999e6f993 --- /dev/null +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -0,0 +1,110 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.utils import cstr, flt, cint +from erpnext.stock.stock_ledger import make_sl_entries +from erpnext.controllers.stock_controller import create_repost_item_valuation_entry + +def execute(): + if not frappe.db.has_column('Work Order', 'has_batch_no'): + return + + if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): + return + + frappe.reload_doc('manufacturing', 'doctype', 'work_order') + filters = { + 'docstatus': 1, + 'produced_qty': ('>', 0), + 'creation': ('>=', '2021-06-29 00:00:00'), + 'has_batch_no': 1 + } + + fields = ['name', 'production_item'] + + work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)] + + if not work_orders: + return + + repost_stock_entries = [] + stock_entries = frappe.db.sql_list(''' + SELECT + se.name + FROM + `tabStock Entry` se + WHERE + se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders} + and not exists( + select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 + ) + Order BY + se.posting_date, se.posting_time + '''.format(work_orders=tuple(work_orders))) + + if stock_entries: + print('Length of stock entries', len(stock_entries)) + + for stock_entry in stock_entries: + doc = frappe.get_doc('Stock Entry', stock_entry) + doc.set_work_order_details() + doc.load_items_from_bom() + doc.calculate_rate_and_amount() + set_expense_account(doc) + doc.make_batches('t_warehouse') + + if doc.docstatus == 0: + doc.save() + else: + repost_stock_entry(doc) + repost_stock_entries.append(doc) + + for repost_doc in repost_stock_entries: + repost_future_sle_and_gle(repost_doc) + +def set_expense_account(doc): + for row in doc.items: + if row.is_finished_item and not row.expense_account: + row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account') + +def repost_stock_entry(doc): + doc.db_update() + for child_row in doc.items: + if child_row.is_finished_item: + child_row.db_update() + + sl_entries = [] + finished_item_row = doc.get_finished_item_row() + get_sle_for_target_warehouse(doc, sl_entries, finished_item_row) + + if sl_entries: + try: + make_sl_entries(sl_entries, True) + except Exception: + print(f'SLE entries not posted for the stock entry {doc.name}') + traceback = frappe.get_traceback() + frappe.log_error(traceback) + +def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row): + for d in doc.get('items'): + if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name: + sle = doc.get_sl_entries(d, { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate) + }) + + sle.recalculate_rate = 1 + sl_entries.append(sle) + +def repost_future_sle_and_gle(doc): + args = frappe._dict({ + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "company": doc.company + }) + + create_repost_item_valuation_entry(args) \ No newline at end of file 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 55f2ebb2241..5f31d9caf0d 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -133,6 +133,6 @@ def repost_entries(): def get_repost_item_valuation_entries(): return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` - WHERE status != 'Completed' and creation <= %s and docstatus = 1 + WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc """, now(), as_dict=1) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index c9838d75f1d..872b1d05169 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -719,6 +719,10 @@ class StockEntry(StockController): frappe.throw(_("Multiple items cannot be marked as finished item")) if self.purpose == "Manufacture": + if not finished_items: + frappe.throw(_('Finished Good has not set in the stock entry {0}') + .format(self.name)) + allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) From e079a1bb33cb1eb322cbd42635800c9eb0832836 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 16 Jul 2021 15:41:33 +0550 Subject: [PATCH 098/951] bumped to version 13.7.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 0c96d325c2e..11665496289 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.6.0' +__version__ = '13.7.0' def get_default_company(user=None): '''Get default company for user''' From f9da88cb15c831e50dc823acaeacbaa822a7fabf Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 19 Jul 2021 21:45:33 +0530 Subject: [PATCH 099/951] fix: Additional discount calculations in Invoices --- .../public/js/controllers/taxes_and_totals.js | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1de9ec1a7df..b5aa6265be2 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -34,9 +34,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.set_value(item.doctype, item.name, "rate", item_rate); }, - calculate_taxes_and_totals: function(update_paid_amount) { + calculate_taxes_and_totals: async function(update_paid_amount) { this.discount_amount_applied = false; - this._calculate_taxes_and_totals(); + await this._calculate_taxes_and_totals(); this.calculate_discount_amount(); // Advance calculation applicable to Sales /Purchase Invoice @@ -72,19 +72,20 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }, - _calculate_taxes_and_totals: function() { - frappe.run_serially([ - () => this.validate_conversion_rate(), - () => this.calculate_item_values(), - () => this.update_item_tax_map(), - () => this.initialize_taxes(), - () => this.determine_exclusive_rate(), - () => this.calculate_net_total(), - () => this.calculate_taxes(), - () => this.manipulate_grand_total_for_inclusive_tax(), - () => this.calculate_totals(), - () => this._cleanup() - ]); + _calculate_taxes_and_totals: async function() { + this.validate_conversion_rate(); + this.calculate_item_values(); + await this.update_item_tax_map(); + }, + + _calculate_tax_values : function() { + this.initialize_taxes(); + this.determine_exclusive_rate(); + this.calculate_net_total(); + this.calculate_taxes(); + this.manipulate_grand_total_for_inclusive_tax(); + this.calculate_totals(); + this._cleanup(); }, validate_conversion_rate: function() { @@ -105,7 +106,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, calculate_item_values: function() { - var me = this; + let me = this; if (!this.discount_amount_applied) { $.each(this.frm.doc["items"] || [], function(i, item) { frappe.model.round_floats_in(item); @@ -266,7 +267,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); }, - update_item_tax_map: function() { + update_item_tax_map: async function() { let me = this; let item_codes = []; let item_rates = {}; @@ -301,6 +302,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } }); } + + me._calculate_tax_values(); } }); } From 50b188214d03dc8d2113e8567f8a11dd16a221fa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Jul 2021 12:41:48 +0530 Subject: [PATCH 100/951] revert: Client side handling for Dynamic GST Rates --- .../public/js/controllers/taxes_and_totals.js | 54 ++----------------- 1 file changed, 4 insertions(+), 50 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index b5aa6265be2..263570cb667 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -34,9 +34,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.set_value(item.doctype, item.name, "rate", item_rate); }, - calculate_taxes_and_totals: async function(update_paid_amount) { + calculate_taxes_and_totals: function(update_paid_amount) { this.discount_amount_applied = false; - await this._calculate_taxes_and_totals(); + this._calculate_taxes_and_totals(); this.calculate_discount_amount(); // Advance calculation applicable to Sales /Purchase Invoice @@ -65,20 +65,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.refresh_fields(); }, - calculate_discount_amount: function(){ + calculate_discount_amount: function() { if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { this.set_discount_amount(); this.apply_discount_amount(); } }, - _calculate_taxes_and_totals: async function() { + _calculate_taxes_and_totals: function() { this.validate_conversion_rate(); this.calculate_item_values(); - await this.update_item_tax_map(); - }, - - _calculate_tax_values : function() { this.initialize_taxes(); this.determine_exclusive_rate(); this.calculate_net_total(); @@ -267,48 +263,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); }, - update_item_tax_map: async function() { - let me = this; - let item_codes = []; - let item_rates = {}; - let item_tax_templates = {}; - - $.each(this.frm.doc.items || [], function(i, item) { - if (item.item_code) { - // Use combination of name and item code in case same item is added multiple times - item_codes.push([item.item_code, item.name]); - item_rates[item.name] = item.net_rate; - item_tax_templates[item.name] = item.item_tax_template; - } - }); - - if (item_codes.length) { - return this.frm.call({ - method: "erpnext.stock.get_item_details.get_item_tax_info", - args: { - company: me.frm.doc.company, - tax_category: cstr(me.frm.doc.tax_category), - item_codes: item_codes, - item_rates: item_rates, - item_tax_templates: item_tax_templates - }, - callback: function(r) { - if (!r.exc) { - $.each(me.frm.doc.items || [], function(i, item) { - if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { - item.item_tax_template = r.message[item.name].item_tax_template; - item.item_tax_rate = r.message[item.name].item_tax_rate; - me.add_taxes_from_item_tax_template(item.item_tax_rate); - } - }); - } - - me._calculate_tax_values(); - } - }); - } - }, - add_taxes_from_item_tax_template: function(item_tax_map) { let me = this; From 72eb72f66f64518f859de681adbb4de63a235d28 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Jul 2021 15:45:04 +0530 Subject: [PATCH 101/951] fix: Add update item tax template method back --- erpnext/public/js/controllers/transaction.js | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b3af3d67eaa..6eb6775b285 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1787,6 +1787,46 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ ]); }, + update_item_tax_map: function() { + let me = this; + let item_codes = []; + let item_rates = {}; + let item_tax_templates = {}; + + $.each(this.frm.doc.items || [], function(i, item) { + if (item.item_code) { + // Use combination of name and item code in case same item is added multiple times + item_codes.push([item.item_code, item.name]); + item_rates[item.name] = item.net_rate; + item_tax_templates[item.name] = item.item_tax_template; + } + }); + + if (item_codes.length) { + return this.frm.call({ + method: "erpnext.stock.get_item_details.get_item_tax_info", + args: { + company: me.frm.doc.company, + tax_category: cstr(me.frm.doc.tax_category), + item_codes: item_codes, + item_rates: item_rates, + item_tax_templates: item_tax_templates + }, + callback: function(r) { + if (!r.exc) { + $.each(me.frm.doc.items || [], function(i, item) { + if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) { + item.item_tax_template = r.message[item.name].item_tax_template; + item.item_tax_rate = r.message[item.name].item_tax_rate; + me.add_taxes_from_item_tax_template(item.item_tax_rate); + } + }); + } + } + }); + } + }, + item_tax_template: function(doc, cdt, cdn) { var me = this; if(me.frm.updating_party_details) return; From 9fa92c912b484c82b49e77b9c00527ecd165d6df Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 20 Jul 2021 17:02:05 +0530 Subject: [PATCH 102/951] fix: Revert refresh field --- erpnext/public/js/controllers/taxes_and_totals.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 263570cb667..53d5278bbf4 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -587,8 +587,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail); }); } - - this.frm.refresh_fields(); }, set_discount_amount: function() { From 9ab18b534141c4a4bdf2a4c48541febaa55cbe23 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Jul 2021 23:15:15 +0530 Subject: [PATCH 103/951] fix: add company change trigger --- erpnext/public/js/controllers/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 6eb6775b285..5475383759f 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -826,9 +826,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.run_serially([ () => me.frm.script_manager.trigger("currency"), + () => me.update_item_tax_map(), () => me.apply_default_taxes(), - () => me.apply_pricing_rule(), - () => me.calculate_taxes_and_totals() + () => me.apply_pricing_rule() ]); } } From 7551bcf421f134e3510ded387326d273ef6add82 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 22 Jul 2021 17:25:51 +0550 Subject: [PATCH 104/951] bumped to version 13.7.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 11665496289..a181c2d42cb 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.7.0' +__version__ = '13.7.1' def get_default_company(user=None): '''Get default company for user''' From 64454a5dc8510c6a9b76c9a53ef8a00900a62f4d Mon Sep 17 00:00:00 2001 From: Noah Jacob Date: Mon, 26 Jul 2021 12:54:35 +0530 Subject: [PATCH 105/951] fix: included company in Link Document Type filters for contact (#26576) (cherry picked from commit cbddedab7bf2fc7637b861214c3373a742da830b) --- erpnext/hooks.py | 3 ++- erpnext/public/js/contact.js | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 erpnext/public/js/contact.js diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 52daec91805..1ba752a1464 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -24,7 +24,8 @@ doctype_js = { "Address": "public/js/address.js", "Communication": "public/js/communication.js", "Event": "public/js/event.js", - "Newsletter": "public/js/newsletter.js" + "Newsletter": "public/js/newsletter.js", + "Contact": "public/js/contact.js" } override_doctype_class = { diff --git a/erpnext/public/js/contact.js b/erpnext/public/js/contact.js new file mode 100644 index 00000000000..41a0e8a9f99 --- /dev/null +++ b/erpnext/public/js/contact.js @@ -0,0 +1,16 @@ + + +frappe.ui.form.on("Contact", { + refresh(frm) { + frm.set_query('link_doctype', "links", function() { + return { + query: "frappe.contacts.address_and_contact.filter_dynamic_link_doctypes", + filters: { + fieldtype: ["in", ["HTML", "Text Editor"]], + fieldname: ["in", ["contact_html", "company_description"]], + } + }; + }); + frm.refresh_field("links"); + } +}); From ed68f11a4670f0ee9d080dbf293818b026e36f50 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 26 Jul 2021 16:47:36 +0530 Subject: [PATCH 106/951] fix: Supplier invoice importer fix pre release (#26636) * fix: Supplier Invoice Importer fix Co-authored-by: Subin Tom --- erpnext/controllers/accounts_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4c313c43a72..cdd865ac4ac 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1112,8 +1112,11 @@ class AccountsController(TransactionBase): for d in self.get("payment_schedule"): if d.invoice_portion: d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) - d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) + d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount')) d.outstanding = d.payment_amount + elif not d.invoice_portion: + d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount')) + def set_due_date(self): due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] From 50b76d04bfae7a03472c0d08b104744fc8b941ac Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 19 Jul 2021 20:09:37 +0530 Subject: [PATCH 107/951] fix:Ignore mandatory fields while creating payment reconciliation Journal Entry --- .../doctype/payment_reconciliation/payment_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 6635128f9ef..d788d91855e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company): } ] }) - + jv.flags.ignore_mandatory = True jv.submit() \ No newline at end of file From c468e4a93d991000ce3e4e86b30567c99b1a5fbc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 19 Jul 2021 14:36:54 +0530 Subject: [PATCH 108/951] fix: Add missing cess amount in GSTR-3B report --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 641520437fb..6de228fbc7e 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -322,6 +322,9 @@ class GSTR3BReport(Document): inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100) + if self.invoice_cess.get(inv): + self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2) + self.set_inter_state_supply(inter_state_supply_details) def set_supplies_liable_to_reverse_charge(self): From 5fe7d80a6453b4dd7d33be059eeee62bc88814e8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Jul 2021 13:25:53 +0530 Subject: [PATCH 109/951] fix: GST Reports timeout issue --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 5 ++--- erpnext/regional/report/gstr_1/gstr_1.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 641520437fb..6a61ae2b422 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -214,9 +214,8 @@ class GSTR3BReport(Document): for d in item_details: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, - sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details - if i.item_code == d.item_code and i.parent == d.parent)) + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) + self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) if d.is_nil_exempt and d.item_code not in self.is_nil_exempt: self.is_nil_exempt.append(d.item_code) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index cfcb8c3444f..b81fa810fe1 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -217,9 +217,8 @@ class Gstr1Report(object): for d in items: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, - sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items - if i.item_code == d.item_code and i.parent == d.parent)) + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) + self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) item_tax_rate = {} From a661667e2a340afdc495cd78cb4dfb59fda5546e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 23 Jul 2021 20:28:02 +0530 Subject: [PATCH 110/951] fix(India): Default value for export type --- erpnext/patches.txt | 1 + .../v13_0/update_export_type_for_gst.py | 24 +++++++++++++++++++ erpnext/regional/india/setup.py | 2 -- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v13_0/update_export_type_for_gst.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2a836351177..b891719b02d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -293,3 +293,4 @@ erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships +erpnext.patches.v13_0.update_export_type_for_gst diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py new file mode 100644 index 00000000000..478a2a6c806 --- /dev/null +++ b/erpnext/patches/v13_0/update_export_type_for_gst.py @@ -0,0 +1,24 @@ +import frappe + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + # Update custom fields + fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'}) + if fieldname: + frappe.db.set_value('Custom Field', fieldname, 'default', '') + + fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'}) + if fieldname: + frappe.db.set_value('Custom Field', fieldname, 'default', '') + + # Update Customer/Supplier Masters + frappe.db.sql(""" + UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export') + """) + + frappe.db.sql(""" + UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas') + """) \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 92654608da5..e9372f9b8fc 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -641,7 +641,6 @@ def make_custom_fields(update=True): 'label': 'Export Type', 'fieldtype': 'Select', 'insert_after': 'gst_category', - 'default': 'Without Payment of Tax', 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } @@ -660,7 +659,6 @@ def make_custom_fields(update=True): 'label': 'Export Type', 'fieldtype': 'Select', 'insert_after': 'gst_category', - 'default': 'Without Payment of Tax', 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } From cba847b0517b65fdf415da4224f8c55ea69d6b43 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Jul 2021 18:38:50 +0530 Subject: [PATCH 111/951] fix: Test case for GSTR-3b report --- .../regional/doctype/gstr_3b_report/gstr_3b_report.py | 10 ++++++++-- erpnext/regional/report/gstr_1/gstr_1.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 641520437fb..6fd135d5606 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -281,9 +281,15 @@ class GSTR3BReport(Document): if self.get('invoice_items'): # Build itemised tax for export invoices, nil and exempted where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type') - == "Without Payment of Tax"): + if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \ + == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) + else: + for item in items.keys(): + if item in self.is_nil_exempt + self.is_non_gst and \ + item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, []) + self.items_based_on_tax_rate[invoice][0].append(item) def set_outward_taxable_supplies(self): inter_state_supply_details = {} diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index cfcb8c3444f..f9de2d527ba 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -287,7 +287,8 @@ class Gstr1Report(object): # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ - and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": + and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ + and self.invoices.get(invoice, {}).get('gst_category') == "Overseas": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): From 356a55258ee4248deea931db7a7ee1b6252116d3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 25 Jul 2021 19:46:20 +0530 Subject: [PATCH 112/951] fix: Exchange rate revaluation posting date and precision fixes --- .../exchange_rate_revaluation.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 56193216a22..e94875f2d72 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -99,10 +99,12 @@ class ExchangeRateRevaluation(Document): sum(debit) - sum(credit) as balance from `tabGL Entry` where account in (%s) + and posting_date <= %s + and is_cancelled = 0 group by account, party_type, party having sum(debit) != sum(credit) order by account - """ % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) + """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1) return account_details @@ -143,9 +145,9 @@ class ExchangeRateRevaluation(Document): "party_type": d.get("party_type"), "party": d.get("party"), "account_currency": d.get("account_currency"), - "balance": d.get("balance_in_account_currency"), - dr_or_cr: abs(d.get("balance_in_account_currency")), - "exchange_rate":d.get("new_exchange_rate"), + "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), + dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), + "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, }) @@ -154,9 +156,9 @@ class ExchangeRateRevaluation(Document): "party_type": d.get("party_type"), "party": d.get("party"), "account_currency": d.get("account_currency"), - "balance": d.get("balance_in_account_currency"), - reverse_dr_or_cr: abs(d.get("balance_in_account_currency")), - "exchange_rate": d.get("current_exchange_rate"), + "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")), + reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")), + "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name }) @@ -185,9 +187,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N account_details = {} company_currency = erpnext.get_company_currency(company) - balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False) + balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False) if balance: - balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party) + balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party) current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate From 19a0ca1980b69d04310cbbe27e05f117622c3894 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 25 Jul 2021 19:46:50 +0530 Subject: [PATCH 113/951] fix: Ignore GL Entry on cancel --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index e94875f2d72..c8d5737d753 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document): if not (self.company and self.posting_date): frappe.throw(_("Please select Company and Posting Date to getting entries")) + def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry') + @frappe.whitelist() def check_journal_entry_condition(self): total_debit = frappe.db.get_value("Journal Entry Account", { From 3fcc5e3134f0aa2d29d6baabbc38050437fa42e1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 25 Jul 2021 21:26:22 +0530 Subject: [PATCH 114/951] fix: Convert null values to empty string on grouping --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index c8d5737d753..f2b0a8c08a6 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -104,7 +104,7 @@ class ExchangeRateRevaluation(Document): where account in (%s) and posting_date <= %s and is_cancelled = 0 - group by account, party_type, party + group by account, NULLIF(party_type,''), NULLIF(party,'') having sum(debit) != sum(credit) order by account """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1) From 7a97b6d6a831e5bf77f2b5d28264f998aa5a933f Mon Sep 17 00:00:00 2001 From: Ankush Date: Tue, 27 Jul 2021 16:39:38 +0530 Subject: [PATCH 115/951] fix: reload manufacturing setting before patch (#26641) (cherry picked from commit c8d7a8c781f6c448fd872427d611ffab70c136db) --- erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py index 48999e6f993..d7ad1fc6962 100644 --- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -10,6 +10,7 @@ def execute(): if not frappe.db.has_column('Work Order', 'has_batch_no'): return + frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): return @@ -107,4 +108,4 @@ def repost_future_sle_and_gle(doc): "company": doc.company }) - create_repost_item_valuation_entry(args) \ No newline at end of file + create_repost_item_valuation_entry(args) From 940356d28a0073f139d3b0dbdc9b0b4dee974a16 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 27 Jul 2021 18:43:20 +0530 Subject: [PATCH 116/951] fix: not able to add employee in the job card --- erpnext/manufacturing/doctype/job_card/job_card.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 420bb008039..69c7f5c614b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -192,11 +192,11 @@ class JobCard(Document): "completed_qty": args.get("completed_qty") or 0.0 }) elif args.get("start_time"): - new_args = { + new_args = frappe._dict({ "from_time": get_datetime(args.get("start_time")), "operation": args.get("sub_operation"), "completed_qty": 0.0 - } + }) if employees: for name in employees: From 5a7fad8a6ab5ac16d6ad3ded583c092cc6f49c37 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 27 Jul 2021 10:59:25 +0530 Subject: [PATCH 117/951] feat: Enhancements in TDS --- .../tax_withholding_category.json | 350 ++++++------------ .../tax_withholding_category.py | 24 +- .../test_tax_withholding_category.py | 47 ++- 3 files changed, 184 insertions(+), 237 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json index f9160e281da..331770fbe84 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json @@ -1,263 +1,151 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 18:42:06.431683", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-04-13 18:42:06.431683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "category_details_section", + "category_name", + "round_off_tax_amount", + "column_break_2", + "consider_party_ledger_amount", + "tax_on_excess_amount", + "section_break_8", + "rates", + "section_break_7", + "accounts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "category_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Category Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_8", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Tax Withholding Rates", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "show_days": 1, + "show_seconds": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "rates", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Rates", - "length": 0, - "no_copy": 0, "options": "Tax Withholding Rate", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "show_days": 1, + "show_seconds": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, + "fieldname": "section_break_7", + "fieldtype": "Section Break", "label": "Account Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Tax Withholding Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Tax Withholding Account", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "category_details_section", + "fieldtype": "Section Break", + "label": "Category Details", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach", + "fieldname": "consider_party_ledger_amount", + "fieldtype": "Check", + "label": "Consider Entire Party Ledger Amount", + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Tax will be withheld only for amount exceeding the cumulative threshold", + "fieldname": "tax_on_excess_amount", + "fieldtype": "Check", + "label": "Only Deduct Tax On Excess Amount ", + "show_days": 1, + "show_seconds": 1 + }, + { + "description": "Checking this will round off the tax amount to the nearest integer", + "fieldname": "round_off_tax_amount", + "fieldtype": "Data", + "label": "Round Off Tax Amount", + "show_days": 1, + "show_seconds": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-07-17 22:53:26.193179", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Tax Withholding Category", - "name_case": "", - "owner": "Administrator", + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-07-26 21:47:34.396071", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withholding Category", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index b9ee4a0963f..45c8e1b49fc 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt, getdate +from frappe.utils import flt, getdate, cint from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): @@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "rate": tax_rate_detail.tax_withholding_rate, "threshold": tax_rate_detail.single_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category + "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category, + "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount, + "tax_on_excess_amount": tax_withholding.tax_on_excess_amount, + "round_off_tax_amount": tax_withholding.round_off_tax_amount }) def get_tax_withholding_rates(tax_withholding, fiscal_year): @@ -235,10 +238,15 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): tds_amount = 0 + invoice_filters = { + 'name': ('in', vouchers), + 'docstatus': 1 + } - supp_credit_amt = frappe.db.get_value('Purchase Invoice', { - 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1 - }, 'sum(net_total)') or 0.0 + if not cint(tax_details.consider_party_ledger_amount): + invoice_filters.update({'apply_tds': 1}) + + supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0 supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { 'parent': ('in', vouchers), 'docstatus': 1, @@ -255,6 +263,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu cumulative_threshold = tax_details.get('cumulative_threshold', 0) if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): + if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount): + supp_credit_amt -= cumulative_threshold + if ldc and is_valid_certificate( ldc.valid_from, ldc.valid_upto, inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, @@ -263,6 +274,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) else: tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 + + if cint(tax_details.round_off_tax_amount): + tds_amount = round(tds_amount) return tds_amount diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index dd26be7c992..2ba22ca4353 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_tax_withholding_category_checks(self): + invoices = [] + frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category") + + # First Invoice with no tds check + pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True) + pi.apply_tds = 0 + pi.save() + pi.submit() + invoices.append(pi) + + # Second Invoice will apply TDS checked + pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000) + pi1.submit() + invoices.append(pi1) + + # Cumulative threshold is 30000 + # Threshold calculation should be on both the invoices + # TDS should be applied only on 1000 + self.assertEqual(pi1.taxes[0].tax_amount, 1000) + + for d in invoices: + d.cancel() + + def test_cumulative_threshold_tcs(self): frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") invoices = [] @@ -195,7 +220,7 @@ def create_sales_invoice(**args): def create_records(): # create a new suppliers - for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: + for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']: if frappe.db.exists('Supplier', name): continue @@ -311,3 +336,23 @@ def create_tax_with_holding_category(): 'account': 'TDS - _TC' }] }).insert() + + if not frappe.db.exists("Tax Withholding Category", "New TDS Category"): + frappe.get_doc({ + "doctype": "Tax Withholding Category", + "name": "New TDS Category", + "category_name": "New TDS Category", + "round_off_tax_amount": 1, + "consider_party_ledger_amount": 1, + "tax_on_excess_amount": 1, + "rates": [{ + 'fiscal_year': fiscal_year, + 'tax_withholding_rate': 10, + 'single_threshold': 0, + 'cumulative_threshold': 30000 + }], + "accounts": [{ + 'company': '_Test Company', + 'account': 'TDS - _TC' + }] + }).insert() From 441adf763f23385d1e8f6a2db092721956ea8187 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 28 Jul 2021 10:43:02 +0530 Subject: [PATCH 118/951] fix(minor): Consider grand total for threshold check --- .../tax_withholding_category/tax_withholding_category.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 45c8e1b49fc..020de3c3f34 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -243,10 +243,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu 'docstatus': 1 } + field = 'sum(net_total)' + if not cint(tax_details.consider_party_ledger_amount): invoice_filters.update({'apply_tds': 1}) + field = 'sum(grand_total)' - supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0 + supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { 'parent': ('in', vouchers), 'docstatus': 1, From 868a6cb26dc513708c36957d7d2537b3b0d14d4f Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 28 Jul 2021 13:42:13 +0530 Subject: [PATCH 119/951] fix: documentation link for E Invoicing (#26686) --- .../regional/doctype/e_invoice_settings/e_invoice_settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js index cc2d9f06d2d..54e488610df 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -3,7 +3,7 @@ frappe.ui.form.on('E Invoice Settings', { refresh(frm) { - const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing'; + const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing'; frm.dashboard.set_headline( __("Read {0} for more information on E Invoicing features.", [`documentation`]) ); From 8ed7a21cd515ef6e9e109a62195088b13d0a06af Mon Sep 17 00:00:00 2001 From: Ankush Date: Wed, 28 Jul 2021 16:38:59 +0530 Subject: [PATCH 120/951] fix(bom): remove manual permission checking (#26689) get_list does the permission checking. (cherry picked from commit d95f16ac8fb084e33ab936545fc60acd6a4ff618) --- erpnext/manufacturing/doctype/bom/bom.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index af081c449c6..ebd9ae2dc54 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1069,13 +1069,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if barcodes: or_cond_filters["name"] = ("in", barcodes) - for cond in get_match_cond(doctype, as_condition=False): - for key, value in cond.items(): - if key == doctype: - key = "name" - - query_filters[key] = ("in", value) - if filters and filters.get("item_code"): has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") if not has_variants: @@ -1084,7 +1077,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - return frappe.get_all("Item", + return frappe.get_list("Item", fields = fields, filters=query_filters, or_filters = or_cond_filters, order_by=order_by, limit_start=start, limit_page_length=page_len, as_list=1) From 8c7d9efa9d93ee42306ada4de58bfcb560b77038 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 28 Jul 2021 12:57:59 +0530 Subject: [PATCH 121/951] fix: Chnage fieldtype from data to check --- .../tax_withholding_category.json | 4 ++-- erpnext/patches.txt | 1 + erpnext/patches/v13_0/update_tds_check_field.py | 8 ++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v13_0/update_tds_check_field.py diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json index 331770fbe84..153906ffe97 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.json @@ -94,7 +94,7 @@ { "description": "Checking this will round off the tax amount to the nearest integer", "fieldname": "round_off_tax_amount", - "fieldtype": "Data", + "fieldtype": "Check", "label": "Round Off Tax Amount", "show_days": 1, "show_seconds": 1 @@ -102,7 +102,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-07-26 21:47:34.396071", + "modified": "2021-07-27 21:47:34.396071", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Category", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b891719b02d..32763754d22 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -294,3 +294,4 @@ erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_export_type_for_gst +erpnext.patches.v13_0.update_tds_check_field #3 diff --git a/erpnext/patches/v13_0/update_tds_check_field.py b/erpnext/patches/v13_0/update_tds_check_field.py new file mode 100644 index 00000000000..16bf76d530c --- /dev/null +++ b/erpnext/patches/v13_0/update_tds_check_field.py @@ -0,0 +1,8 @@ +import frappe + +def execute(): + if frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"): + frappe.db.sql(""" + UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0 + WHERE round_off_tax_amount IS NULL + """) \ No newline at end of file From 6ac68f3bc74843e7f703a86063238221534267fd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 28 Jul 2021 15:30:05 +0530 Subject: [PATCH 122/951] fix: Patch --- erpnext/patches/v13_0/update_tds_check_field.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/update_tds_check_field.py b/erpnext/patches/v13_0/update_tds_check_field.py index 16bf76d530c..3d149586a04 100644 --- a/erpnext/patches/v13_0/update_tds_check_field.py +++ b/erpnext/patches/v13_0/update_tds_check_field.py @@ -1,7 +1,8 @@ import frappe def execute(): - if frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"): + if frappe.db.has_table("Tax Withholding Category") \ + and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"): frappe.db.sql(""" UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0 WHERE round_off_tax_amount IS NULL From f8343890b9e065ced4ba87a065624d01f388b86e Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 29 Jul 2021 11:09:34 +0530 Subject: [PATCH 123/951] feat: don't recompute taxes (#26695) --- .../sales_taxes_and_charges.json | 14 ++++++++++++-- erpnext/controllers/taxes_and_totals.py | 7 ++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 1b7a0fe562e..cfdb167bbca 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -27,7 +27,8 @@ "base_tax_amount", "base_total", "base_tax_amount_after_discount_amount", - "item_wise_tax_detail" + "item_wise_tax_detail", + "dont_recompute_tax" ], "fields": [ { @@ -200,13 +201,22 @@ "fieldname": "included_in_paid_amount", "fieldtype": "Check", "label": "Considered In Paid Amount" + }, + { + "default": "0", + "fieldname": "dont_recompute_tax", + "fieldtype": "Check", + "hidden": 1, + "label": "Dont Recompute tax", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-14 01:44:36.899147", + "modified": "2021-07-27 12:40:59.051803", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 56da5b71da0..099c7d43463 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -152,7 +152,7 @@ class calculate_taxes_and_totals(object): validate_taxes_and_charges(tax) validate_inclusive_tax(tax, self.doc) - if not self.doc.get('is_consolidated'): + if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")): tax.item_wise_tax_detail = {} tax_fields = ["total", "tax_amount_after_discount_amount", @@ -347,7 +347,7 @@ class calculate_taxes_and_totals(object): elif tax.charge_type == "On Item Quantity": current_tax_amount = tax_rate * item.qty - if not self.doc.get("is_consolidated"): + if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")): self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) return current_tax_amount @@ -455,7 +455,8 @@ class calculate_taxes_and_totals(object): def _cleanup(self): if not self.doc.get('is_consolidated'): for tax in self.doc.get("taxes"): - tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) + if not tax.get("dont_recompute_tax"): + tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) def set_discount_amount(self): if self.doc.additional_discount_percentage: From a6d276a06fe02fa4a16bdbad2ce31432701a9f3f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 29 Jul 2021 17:02:06 +0530 Subject: [PATCH 124/951] fix: remove cancelled entries from Stock and Account Value comparison report --- .../stock_and_account_value_comparison.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 14d543b1740..bfc4471b9af 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -22,6 +22,7 @@ def get_data(report_filters): data = [] filters = { + "is_cancelled": 0, "company": report_filters.company, "posting_date": ("<=", report_filters.as_on_date) } @@ -34,7 +35,7 @@ def get_data(report_filters): key = (d.voucher_type, d.voucher_no) gl_data = voucher_wise_gl_data.get(key) or {} d.account_value = gl_data.get("account_value", 0) - d.difference_value = (d.stock_value - d.account_value) + d.difference_value = abs(d.stock_value - d.account_value) if abs(d.difference_value) > 0.1: data.append(d) From 57cd273f7c20510985162cde175ae4aa424f56b0 Mon Sep 17 00:00:00 2001 From: Ankush Date: Thu, 29 Jul 2021 19:49:36 +0530 Subject: [PATCH 125/951] fix: empty "against account" in Purchase Receipt GLE bp #26712 (#26719) * fix: correct field for GLE against account in PR * fix: remove incorrect field check from reposting --- erpnext/accounts/utils.py | 2 +- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 1cdbd8d38a6..9afe365f747 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -966,7 +966,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): for e in existing_gle: if entry.account == e.account: account_existed = True - if (entry.account == e.account and entry.against_account == e.against_account + if (entry.account == e.account and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) and ( flt(entry.debit, precision) != flt(e.debit, precision) or flt(entry.credit, precision) != flt(e.credit, precision))): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 82c87a83a50..26ea11e01d9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -415,7 +415,7 @@ class PurchaseReceipt(BuyingController): "cost_center": cost_center, "debit": debit, "credit": credit, - "against_account": against_account, + "against": against_account, "remarks": remarks, } From 9c7a9f3a1374bbba0693295f444db3158c06e6bd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 29 Jul 2021 18:47:16 +0530 Subject: [PATCH 126/951] fix: Parent condition in pricing rules --- erpnext/accounts/doctype/pricing_rule/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index b54d0e73a8c..94abf3b3c06 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -168,7 +168,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True): frappe.throw(_("Invalid {0}").format(args.get(field))) parent_groups = frappe.db.sql_list("""select name from `tab%s` - where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) + where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) if parenttype in ["Customer Group", "Item Group", "Territory"]: parent_field = "parent_{0}".format(frappe.scrub(parenttype)) From d4ae1febe3b174469c8f86f281523974fff02ca3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 30 Jul 2021 11:21:49 +0530 Subject: [PATCH 127/951] fix: gl entries for exchange gain loss (#26734) --- .../purchase_invoice/test_purchase_invoice.py | 8 ++++---- erpnext/controllers/accounts_controller.py | 17 +++++++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ca4d009956d..4bc22a544d1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -997,8 +997,8 @@ class TestPurchaseInvoice(unittest.TestCase): expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 37500.0], - ["_Test Payable USD - _TC", -40000.0], - ["Exchange Gain/Loss - _TC", 2500.0] + ["_Test Payable USD - _TC", -35000.0], + ["Exchange Gain/Loss - _TC", -2500.0] ] gl_entries = frappe.db.sql(""" @@ -1028,8 +1028,8 @@ class TestPurchaseInvoice(unittest.TestCase): expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 36500.0], - ["_Test Payable USD - _TC", -38000.0], - ["Exchange Gain/Loss - _TC", 1500.0] + ["_Test Payable USD - _TC", -35000.0], + ["Exchange Gain/Loss - _TC", -1500.0] ] gl_entries = frappe.db.sql(""" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cdd865ac4ac..a9b7efbe980 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -674,19 +674,24 @@ class AccountsController(TransactionBase): if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: for d in self.get("advances"): if d.exchange_gain_loss: - party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer - party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to - party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer" + is_purchase_invoice = self.get('doctype') == 'Purchase Invoice' + party = self.supplier if is_purchase_invoice else self.customer + party_account = self.credit_to if is_purchase_invoice else self.debit_to + party_type = "Supplier" if is_purchase_invoice else "Customer" gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') + if not gain_loss_account: + frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}") + .format(self.get('company'))) account_currency = get_account_currency(gain_loss_account) if account_currency != self.company_currency: - frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) + frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency)) # for purchase dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' - # just reverse for sales? - dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' + if not is_purchase_invoice: + # just reverse for sales? + dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' gl_entries.append( self.get_gl_dict({ From 6eded547f5e6935c01ec24caf96ba03e70a860d9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 29 Jul 2021 15:26:19 +0530 Subject: [PATCH 128/951] fix: TDS calculation for first threshold breach for TDS category 194Q --- .../tax_withholding_category/tax_withholding_category.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 020de3c3f34..481ef285e72 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -148,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no): def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): fiscal_year = fiscal_year_details[0] + vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers @@ -267,7 +268,11 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount): - supp_credit_amt -= cumulative_threshold + # Get net total again as TDS is calculated on net total + # Grand is used to just check for threshold breach + net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0 + net_total += inv.net_total + supp_credit_amt = net_total - cumulative_threshold if ldc and is_valid_certificate( ldc.valid_from, ldc.valid_upto, From 6fffc90b46b9b0dd59b78837e5bab7b0d5c2651c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 31 Jul 2021 16:05:41 +0530 Subject: [PATCH 129/951] chore: Added change log for v13.8.0 --- erpnext/change_log/v13/v13_8_0.md | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 erpnext/change_log/v13/v13_8_0.md diff --git a/erpnext/change_log/v13/v13_8_0.md b/erpnext/change_log/v13/v13_8_0.md new file mode 100644 index 00000000000..98ed95ae04a --- /dev/null +++ b/erpnext/change_log/v13/v13_8_0.md @@ -0,0 +1,39 @@ +# Version 13.8.0 Release Notes + +### Features & Enhancements +- Report to show COGS by item groups ([#26222](https://github.com/frappe/erpnext/pull/26222)) +- Enhancements in TDS ([#26677](https://github.com/frappe/erpnext/pull/26677)) +- API Endpoint to update halted Razorpay subscriptions ([#26564](https://github.com/frappe/erpnext/pull/26564)) + +### Fixes +- Incorrect bom name ([#26600](https://github.com/frappe/erpnext/pull/26600)) +- Exchange rate revaluation posting date and precision fixes ([#26651](https://github.com/frappe/erpnext/pull/26651)) +- POS item cart dom updates ([#26460](https://github.com/frappe/erpnext/pull/26460)) +- General Ledger report not working with filter group by ([#26439](https://github.com/frappe/erpnext/pull/26438)) +- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206)) +- Validation check for batch for stock reconciliation type in stock entry ([#26487](https://github.com/frappe/erpnext/pull/26487)) +- Improved UX for additional discount field ([#26502](https://github.com/frappe/erpnext/pull/26502)) +- Add missing cess amount in GSTR-3B report ([#26644](https://github.com/frappe/erpnext/pull/26644)) +- Optimized code for reposting item valuation ([#26431](https://github.com/frappe/erpnext/pull/26431)) +- FG item not fetched in manufacture entry ([#26508](https://github.com/frappe/erpnext/pull/26508)) +- Errors on parallel requests creation of company for India ([#26420](https://github.com/frappe/erpnext/pull/26420)) +- Incorrect valuation rate calculation in gross profit report ([#26558](https://github.com/frappe/erpnext/pull/26558)) +- Empty "against account" in Purchase Receipt GLE ([#26712](https://github.com/frappe/erpnext/pull/26712)) +- Remove cancelled entries from Stock and Account Value comparison report ([#26721](https://github.com/frappe/erpnext/pull/26721)) +- Remove manual permission checking ([#26691](https://github.com/frappe/erpnext/pull/26691)) +- Delete child docs when parent doc is deleted ([#26518](https://github.com/frappe/erpnext/pull/26518)) +- GST Reports timeout issue ([#26646](https://github.com/frappe/erpnext/pull/26646)) +- Parent condition in pricing rules ([#26727](https://github.com/frappe/erpnext/pull/26727)) +- Added Company filters for Loan ([#26294](https://github.com/frappe/erpnext/pull/26294)) +- Incorrect discount amount on amended document ([#26292](https://github.com/frappe/erpnext/pull/26292)) +- Exchange gain loss not set for advances linked with invoices ([#26436](https://github.com/frappe/erpnext/pull/26436)) +- Unallocated amount in Payment Entry after taxes ([#26412](https://github.com/frappe/erpnext/pull/26412)) +- Wrong operation time in Work Order ([#26613](https://github.com/frappe/erpnext/pull/26613)) +- Serial No and Batch validation ([#26614](https://github.com/frappe/erpnext/pull/26614)) +- Gl Entries for exchange gain loss ([#26734](https://github.com/frappe/erpnext/pull/26734)) +- TDS computation summary shows cancelled invoices ([#26485](https://github.com/frappe/erpnext/pull/26485)) +- Price List rate not fetched for return sales invoice fixed ([#26560](https://github.com/frappe/erpnext/pull/26560)) +- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576)) +- Ignore mandatory fields while creating payment reconciliation Journal Entry ([#26643](https://github.com/frappe/erpnext/pull/26643)) +- Unable to download GSTR-1 json ([#26418](https://github.com/frappe/erpnext/pull/26418)) +- Paging buttons not working on item group portal page ([#26498](https://github.com/frappe/erpnext/pull/26498)) From 8cb560c7534b9dc2f47333f4dcbea078b9090740 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 1 Aug 2021 14:55:09 +0550 Subject: [PATCH 130/951] bumped to version 13.8.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a181c2d42cb..c90e01cfbd6 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.7.1' +__version__ = '13.8.0' def get_default_company(user=None): '''Get default company for user''' From b7b111c3edd3dd42470b61061e866dfa1850d46c Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 10 Aug 2021 22:52:37 +0530 Subject: [PATCH 131/951] fix: unseting of payment if no pos profile found (#26884) (#26890) (cherry picked from commit b614834efedbef572e0567828f0d9d82e81331ee) Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- erpnext/controllers/taxes_and_totals.py | 6 +----- erpnext/public/js/controllers/taxes_and_totals.js | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 099c7d43463..05edb2530c2 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -679,17 +679,13 @@ class calculate_taxes_and_totals(object): default_mode_of_payment = frappe.db.get_value('POS Payment Method', {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) - self.doc.payments = [] - if default_mode_of_payment: + self.doc.payments = [] self.doc.append('payments', { 'mode_of_payment': default_mode_of_payment.mode_of_payment, 'amount': total_amount_to_pay, 'default': 1 }) - else: - self.doc.is_pos = 0 - self.doc.pos_profile = '' self.calculate_paid_amount() diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 53d5278bbf4..891ec6edc18 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -747,8 +747,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ this.frm.doc.payments.find(pay => { if (pay.default) { pay.amount = total_amount_to_pay; - } else { - pay.amount = 0.0 } }); this.frm.refresh_fields(); From 9855bbb95e2dbcc4140753309abbee283db91a3b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 10 Aug 2021 23:49:56 +0530 Subject: [PATCH 132/951] fix(style): apply svg container margin only in desktop view (#26894) --- erpnext/public/scss/hierarchy_chart.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index 7f1077dbbd2..8a1ec4992b0 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -188,6 +188,10 @@ // horizontal hierarchy tree view #hierarchy-chart-wrapper { padding-top: 30px; + + #arrows { + margin-top: -80px; + } } .hierarchy { @@ -211,7 +215,6 @@ #arrows { position: absolute; overflow: visible; - margin-top: -80px; } .active-connector { From 99658ceb4e743c4758a306f70f5497a8818738e0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 11 Aug 2021 14:19:07 +0530 Subject: [PATCH 133/951] fix: Nest .level class style under .hierarchy class - To avoid style overrides in list view --- erpnext/public/scss/hierarchy_chart.scss | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/public/scss/hierarchy_chart.scss b/erpnext/public/scss/hierarchy_chart.scss index 8a1ec4992b0..a66d6474e0d 100644 --- a/erpnext/public/scss/hierarchy_chart.scss +++ b/erpnext/public/scss/hierarchy_chart.scss @@ -206,10 +206,12 @@ margin: 0px 0px 16px 0px; } -.level { - margin-right: 8px; - align-items: flex-start; - flex-direction: column; +.hierarchy, .hierarchy-mobile { + .level { + margin-right: 8px; + align-items: flex-start; + flex-direction: column; + } } #arrows { From 08e4026456a698e4a41e1919795a03366e0ae2cb Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 12 Aug 2021 10:31:01 +0530 Subject: [PATCH 134/951] fix: Stock Analytics Report must consider warehouse during calculation (#26908) * fix: Stock Analytics Report must consider warehouse during calculation * fix: Brand filter in Stock Analytics (cherry picked from commit 703b081172981833bcf9a1fc5c86517817c3ff32) --- .../report/stock_analytics/stock_analytics.py | 44 +++++++++++++++---- .../report/stock_balance/stock_balance.py | 3 ++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index 0cc8ca48aac..d44685060c7 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -114,14 +114,41 @@ def get_period(posting_date, filters): def get_periodic_data(entry, filters): + """Structured as: + Item 1 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + Item 2 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + """ periodic_data = {} for d in entry: period = get_period(d.posting_date, filters) bal_qty = 0 + # if period against item does not exist yet, instantiate it + # insert existing balance dict against period, and add/subtract to it + if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period): + periodic_data[d.item_code][period] = periodic_data[d.item_code]['balance'] + if d.voucher_type == "Stock Reconciliation": - if periodic_data.get(d.item_code): - bal_qty = periodic_data[d.item_code]["balance"] + if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get('balance').get(d.warehouse): + bal_qty = periodic_data[d.item_code]['balance'][d.warehouse] qty_diff = d.qty_after_transaction - bal_qty else: @@ -132,12 +159,12 @@ def get_periodic_data(entry, filters): else: value = d.stock_value_difference - periodic_data.setdefault(d.item_code, {}).setdefault(period, 0.0) - periodic_data.setdefault(d.item_code, {}).setdefault("balance", 0.0) - - periodic_data[d.item_code]["balance"] += value - periodic_data[d.item_code][period] = periodic_data[d.item_code]["balance"] + # period-warehouse wise balance + periodic_data.setdefault(d.item_code, {}).setdefault('balance', {}).setdefault(d.warehouse, 0.0) + periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0) + periodic_data[d.item_code]['balance'][d.warehouse] += value + periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]['balance'][d.warehouse] return periodic_data @@ -160,7 +187,8 @@ def get_data(filters): total = 0 for dummy, end_date in ranges: period = get_period(end_date, filters) - amount = flt(periodic_data.get(item_data.name, {}).get(period)) + period_data = periodic_data.get(item_data.name, {}).get(period) + amount = sum(period_data.values()) if period_data else 0 row[scrub(period)] = amount total += amount row["total"] = total diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 9e56ad41306..fc3d719a780 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -235,12 +235,15 @@ def filter_items_with_no_transactions(iwb_map, float_precision): return iwb_map def get_items(filters): + "Get items based on item code, item group or brand." conditions = [] if filters.get("item_code"): conditions.append("item.name=%(item_code)s") else: if filters.get("item_group"): conditions.append(get_item_group_condition(filters.get("item_group"))) + if filters.get("brand"): # used in stock analytics report + conditions.append("item.brand=%(brand)s") items = [] if conditions: From f4b2f4aaf7823cdae0ed344035fc8835ba2d9909 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 12 Aug 2021 17:11:38 +0530 Subject: [PATCH 135/951] fix: ZeroDivisionError on creating e-invoice for credit note (#26918) --- erpnext/regional/india/e_invoice/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e65442dbff4..2373512cca4 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -190,8 +190,10 @@ def get_item_list(invoice): item.description = sanitize_for_json(d.item_name) item.qty = abs(item.qty) - - item.unit_rate = abs(item.taxable_value / item.qty) + if flt(item.qty) != 0.0: + item.unit_rate = abs(item.taxable_value / item.qty) + else: + item.unit_rate = abs(item.taxable_value) item.gross_amount = abs(item.taxable_value) item.taxable_value = abs(item.taxable_value) item.discount_amount = 0 From 2e6899fbe439a6d194eedb3ef46adbdd9d2e4cfb Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 13 Aug 2021 15:37:45 +0530 Subject: [PATCH 136/951] fix: Copy previous balance dict object instead of assigning (#26942) - Due to plain assignment, dict mutation gave wrong monthly values (cherry picked from commit fe2a34f17197a2877356bcdf5d0bb6c46312ed33) --- erpnext/stock/report/stock_analytics/stock_analytics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index d44685060c7..fde934b1339 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -144,7 +144,8 @@ def get_periodic_data(entry, filters): # if period against item does not exist yet, instantiate it # insert existing balance dict against period, and add/subtract to it if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period): - periodic_data[d.item_code][period] = periodic_data[d.item_code]['balance'] + previous_balance = periodic_data[d.item_code]['balance'].copy() + periodic_data[d.item_code][period] = previous_balance if d.voucher_type == "Stock Reconciliation": if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get('balance').get(d.warehouse): From 67e3971c3bb80a37a03da320172e7df1f17dd18b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 16 Aug 2021 10:38:39 +0530 Subject: [PATCH 137/951] fix: Org Chart fixes (#26952) * fix: add z-index to filter to avoid svg wrapper overlapping * fix: expand all nodes not working when there are only 2 levels - added dom freeze while expanding all nodes and exporting --- .../organizational_chart.py | 17 +++++++++-------- .../hierarchy_chart/hierarchy_chart_desktop.js | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index 1e03e3d06ad..29831982172 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -32,16 +32,17 @@ def get_children(parent=None, company=None, exclude_node=None): def get_connections(employee): num_connections = 0 - connections = frappe.get_list('Employee', filters=[ + nodes_to_expand = frappe.get_list('Employee', filters=[ ['reports_to', '=', employee] ]) - num_connections += len(connections) + num_connections += len(nodes_to_expand) - while connections: - for entry in connections: - connections = frappe.get_list('Employee', filters=[ - ['reports_to', '=', entry.name] - ]) - num_connections += len(connections) + while nodes_to_expand: + parent = nodes_to_expand.pop(0) + descendants = frappe.get_list('Employee', filters=[ + ['reports_to', '=', parent.name] + ]) + num_connections += len(descendants) + nodes_to_expand.extend(descendants) return num_connections \ No newline at end of file diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 89fb8d57925..da050abc6e4 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -98,10 +98,12 @@ erpnext.HierarchyChart = class { company.refresh(); $(`[data-fieldname="company"]`).trigger('change'); + $(`[data-fieldname="company"] .link-field`).css('z-index', 2); } setup_actions() { let me = this; + this.page.clear_inner_toolbar(); this.page.add_inner_button(__('Export'), function() { me.export_chart(); }); @@ -123,6 +125,7 @@ erpnext.HierarchyChart = class { } export_chart() { + frappe.dom.freeze(__('Exporting...')); this.page.main.css({ 'min-height': '', 'max-height': '', @@ -146,6 +149,8 @@ erpnext.HierarchyChart = class { a.href = dataURL; a.download = 'hierarchy_chart'; a.click(); + }).finally(() => { + frappe.dom.unfreeze(); }); this.setup_page_style(); @@ -169,7 +174,9 @@ erpnext.HierarchyChart = class { this.page.main .find('#hierarchy-chart-wrapper') .append(this.$hierarchy); + this.nodes = {}; + this.all_nodes_expanded = false; } make_svg_markers() { @@ -202,7 +209,7 @@ erpnext.HierarchyChart = class { render_root_nodes(expanded_view=false) { let me = this; - frappe.call({ + return frappe.call({ method: me.method, args: { company: me.company @@ -229,8 +236,8 @@ erpnext.HierarchyChart = class { expand_node = node; }); + me.root_node = expand_node; if (!expanded_view) { - me.root_node = expand_node; me.expand_node(expand_node); } } @@ -280,10 +287,12 @@ erpnext.HierarchyChart = class { ]); } else { frappe.run_serially([ + () => frappe.dom.freeze(), () => this.setup_hierarchy(), () => this.render_root_nodes(true), () => this.get_all_nodes(node.id, node.name), - (data_list) => this.render_children_of_all_nodes(data_list) + (data_list) => this.render_children_of_all_nodes(data_list), + () => frappe.dom.unfreeze() ]); } } @@ -359,7 +368,7 @@ erpnext.HierarchyChart = class { node = this.nodes[entry.parent]; if (node) { this.render_child_nodes_for_expanded_view(node, entry.data); - } else { + } else if (data_list.length) { data_list.push(entry); } } From b58b1127f3776735484a41d4884387397e78533d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Aug 2021 13:18:39 +0530 Subject: [PATCH 138/951] fix: Add mandatory depends on condition for export type field --- erpnext/patches.txt | 2 +- erpnext/patches/v13_0/update_export_type_for_gst.py | 12 ++++++++++-- erpnext/regional/india/setup.py | 6 ++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b891719b02d..fec727d289c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -293,4 +293,4 @@ erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships -erpnext.patches.v13_0.update_export_type_for_gst +erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16 diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py index 478a2a6c806..3e20212af6d 100644 --- a/erpnext/patches/v13_0/update_export_type_for_gst.py +++ b/erpnext/patches/v13_0/update_export_type_for_gst.py @@ -8,11 +8,19 @@ def execute(): # Update custom fields fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'}) if fieldname: - frappe.db.set_value('Custom Field', fieldname, 'default', '') + frappe.db.set_value('Custom Field', fieldname, + { + 'default': '', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + }) fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'}) if fieldname: - frappe.db.set_value('Custom Field', fieldname, 'default', '') + frappe.db.set_value('Custom Field', fieldname, + { + 'default': '', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' + }) # Update Customer/Supplier Masters frappe.db.sql(""" diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index e9372f9b8fc..37c714d2162 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -642,7 +642,8 @@ def make_custom_fields(update=True): 'fieldtype': 'Select', 'insert_after': 'gst_category', 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax' + 'options': '\nWith Payment of Tax\nWithout Payment of Tax', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' } ], 'Customer': [ @@ -660,7 +661,8 @@ def make_custom_fields(update=True): 'fieldtype': 'Select', 'insert_after': 'gst_category', 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax' + 'options': '\nWith Payment of Tax\nWithout Payment of Tax', + 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' } ], 'Member': [ From 58c1739eacf18bd7866c6bf852375624aa985f1a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Aug 2021 17:14:40 +0530 Subject: [PATCH 139/951] fix: Budget variance missing values --- .../report/budget_variance_report/budget_variance_report.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index f1b231b6901..9f0eee8aa5c 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -38,8 +38,8 @@ def execute(filters=None): GROUP BY parent''',{'dimension':[dimension]}) if DCC_allocation: filters['budget_against_filter'] = [DCC_allocation[0][0]] - cam_map = get_dimension_account_month_map(filters) - dimension_items = cam_map.get(DCC_allocation[0][0]) + ddc_cam_map = get_dimension_account_month_map(filters) + dimension_items = ddc_cam_map.get(DCC_allocation[0][0]) if dimension_items: data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1]) @@ -48,7 +48,6 @@ def execute(filters=None): return columns, data, None, chart def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): - for account, monthwise_data in iteritems(dimension_items): row = [dimension, account] totals = [0, 0, 0] From 97226418474394e982750d82b522edd7ed124351 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Aug 2021 20:36:04 +0530 Subject: [PATCH 140/951] chore: Release Notes v13.9.0 --- erpnext/change_log/v13/v13_9_0.md | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 erpnext/change_log/v13/v13_9_0.md diff --git a/erpnext/change_log/v13/v13_9_0.md b/erpnext/change_log/v13/v13_9_0.md new file mode 100644 index 00000000000..e52766673ce --- /dev/null +++ b/erpnext/change_log/v13/v13_9_0.md @@ -0,0 +1,46 @@ +# Version 13.9.0 Release Notes + +### Features & Enhancements +- Organizational Chart ([#26261](https://github.com/frappe/erpnext/pull/26261)) +- Enable discount accounting ([#26579](https://github.com/frappe/erpnext/pull/26579)) +- Added multi-select fields in promotional scheme to create multiple pricing rules ([#25622](https://github.com/frappe/erpnext/pull/25622)) +- Over transfer allowance for material transfers ([#26814](https://github.com/frappe/erpnext/pull/26814)) +- Enhancements in Tax Withholding Category ([#26661](https://github.com/frappe/erpnext/pull/26661)) + +### Fixes +- Sales Return cancellation if linked with Payment Entry ([#26883](https://github.com/frappe/erpnext/pull/26883)) +- Production plan not fetching sales order of a variant ([#25845](https://github.com/frappe/erpnext/pull/25845)) +- Stock Analytics Report must consider warehouse during calculation ([#26908](https://github.com/frappe/erpnext/pull/26908)) +- Incorrect date difference calculation ([#26805](https://github.com/frappe/erpnext/pull/26805)) +- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206)) +- Cannot cancel payment entry if linked with invoices ([#26703](https://github.com/frappe/erpnext/pull/26703)) +- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576)) +- Fetch Payment Terms from linked Sales/Purchase Order ([#26723](https://github.com/frappe/erpnext/pull/26723)) +- Let all System Managers be able to delete Company transactions ([#26819](https://github.com/frappe/erpnext/pull/26819)) +- Bank remittance report issue ([#26398](https://github.com/frappe/erpnext/pull/26398)) +- Faulty Gl Entry for Asset LCVs ([#26803](https://github.com/frappe/erpnext/pull/26803)) +- Clean Serial No input on Server Side ([#26878](https://github.com/frappe/erpnext/pull/26878)) +- Supplier invoice importer fix v13 ([#26633](https://github.com/frappe/erpnext/pull/26633)) +- POS payment modes displayed wrong total ([#26808](https://github.com/frappe/erpnext/pull/26808)) +- Fetching of item tax from hsn code ([#26736](https://github.com/frappe/erpnext/pull/26736)) +- Cannot cancel invoice if IRN cancelled on portal ([#26879](https://github.com/frappe/erpnext/pull/26879)) +- Validate python expressions ([#26856](https://github.com/frappe/erpnext/pull/26856)) +- POS Item Cart non-stop scroll issue ([#26693](https://github.com/frappe/erpnext/pull/26693)) +- Add mandatory depends on condition for export type field ([#26958](https://github.com/frappe/erpnext/pull/26958)) +- Cannot generate IRNs for standalone credit notes ([#26824](https://github.com/frappe/erpnext/pull/26824)) +- Added progress bar in Repost Item Valuation to check the status of reposting ([#26630](https://github.com/frappe/erpnext/pull/26630)) +- TDS calculation for first threshold breach for TDS category 194Q ([#26710](https://github.com/frappe/erpnext/pull/26710)) +- Student category mapping from the program enrollment tool ([#26739](https://github.com/frappe/erpnext/pull/26739)) +- Cost center & account validation in Sales/Purchase Taxes and Charges ([#26881](https://github.com/frappe/erpnext/pull/26881)) +- Reset weight_per_unit on replacing Item ([#26791](https://github.com/frappe/erpnext/pull/26791)) +- Do not fetch fully return issued purchase receipts ([#26825](https://github.com/frappe/erpnext/pull/26825)) +- Incorrect amount in work order required items table. ([#26585](https://github.com/frappe/erpnext/pull/26585)) +- Additional discount calculations in Invoices ([#26553](https://github.com/frappe/erpnext/pull/26553)) +- Refactored Asset Repair ([#26415](https://github.com/frappe/erpnext/pull/25798)) +- Exchange rate revaluation posting date and precision fixes ([#26650](https://github.com/frappe/erpnext/pull/26650)) +- POS Invoice consolidated Sales Invoice field set to no copy ([#26768](https://github.com/frappe/erpnext/pull/26768)) +- Consider grand total for threshold check ([#26683](https://github.com/frappe/erpnext/pull/26683)) +- Budget variance missing values ([#26966](https://github.com/frappe/erpnext/pull/26966)) +- GL Entries for exchange gain loss ([#26728](https://github.com/frappe/erpnext/pull/26728)) +- Add missing cess amount in GSTR-3B report ([#26544](https://github.com/frappe/erpnext/pull/26544)) +- GST Reports timeout issue ([#26575](https://github.com/frappe/erpnext/pull/26575)) \ No newline at end of file From 03fdce5a1975602a295de2dcf76f8c6facc9fc6b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 17 Aug 2021 10:25:49 +0550 Subject: [PATCH 141/951] bumped to version 13.9.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index c90e01cfbd6..17d650568a4 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.8.0' +__version__ = '13.9.0' def get_default_company(user=None): '''Get default company for user''' From e7143d8711a636eb56cca49157adbec9d4979478 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Aug 2021 11:28:46 +0530 Subject: [PATCH 142/951] fix: Incorrect unallocated amount calculation in payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 46904f7c571..831b2708583 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -529,7 +529,7 @@ class PaymentEntry(AccountsController): if self.payment_type == "Receive" \ and self.base_total_allocated_amount < self.base_received_amount + total_deductions \ and self.total_allocated_amount < self.paid_amount + (total_deductions / self.source_exchange_rate): - self.unallocated_amount = (self.received_amount + total_deductions - + self.unallocated_amount = (self.base_received_amount + total_deductions - self.base_total_allocated_amount) / self.source_exchange_rate self.unallocated_amount -= included_taxes elif self.payment_type == "Pay" \ From 0a5dff1e1f608f8579b9500e7bef701632048280 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Aug 2021 16:38:25 +0530 Subject: [PATCH 143/951] test: Add test case for payment entry --- .../payment_entry/test_payment_entry.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index d1302f5ae78..801dadc7f17 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -295,6 +295,34 @@ class TestPaymentEntry(unittest.TestCase): outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 80) + def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency (self): + si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", + currency="USD", conversion_rate=50, do_not_save=1) + + si.plc_conversion_rate = 50 + si.save() + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, party_amount=20, + bank_account="_Test Bank USD - _TC", bank_amount=900) + + pe.source_exchange_rate = 45.263 + pe.target_exchange_rate = 45.263 + pe.reference_no = "1" + pe.reference_date = "2016-01-01" + + + pe.append("deductions", { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 94.80 + }) + + pe.save() + + self.assertEqual(flt(pe.difference_amount, 2), 0.0) + self.assertEqual(flt(pe.unallocated_amount, 2), 0.0) + def test_payment_entry_retrieves_last_exchange_rate(self): from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records, save_new_records From 9c1d739946a38a1eb96459e8b6e411b488213780 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Aug 2021 13:20:35 +0550 Subject: [PATCH 144/951] bumped to version 13.9.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 17d650568a4..17960783b14 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.9.0' +__version__ = '13.9.1' def get_default_company(user=None): '''Get default company for user''' From 9f79415186917c7756cb4c64d56b254eee2bfe65 Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 18 Aug 2021 16:30:45 +0530 Subject: [PATCH 145/951] fix: email digest recipient patch --- .../v13_0/update_recipient_email_digest.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 erpnext/patches/v13_0/update_recipient_email_digest.py diff --git a/erpnext/patches/v13_0/update_recipient_email_digest.py b/erpnext/patches/v13_0/update_recipient_email_digest.py new file mode 100644 index 00000000000..ed90e126670 --- /dev/null +++ b/erpnext/patches/v13_0/update_recipient_email_digest.py @@ -0,0 +1,22 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("setup", "doctype", "Email Digest") + frappe.reload_doc("setup", "doctype", "Email Digest Recipient") + email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list']) + for email_digest in email_digests: + if email_digest.recipient_list: + for recipient in email_digest.recipient_list.split("\n"): + if frappe.db.exists('User', recipient): + doc = frappe.get_doc({ + 'doctype': 'Email Digest Recipient', + 'parenttype': 'Email Digest', + 'parentfield': 'recipients', + 'parent': email_digest.name, + 'recipient': recipient + }) + doc.insert() From f0d3a074e03925ee1f0a04e37cb972c406577d74 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 19 Aug 2021 14:55:52 +0550 Subject: [PATCH 146/951] bumped to version 13.9.2 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 17960783b14..fa038cebc3a 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.9.1' +__version__ = '13.9.2' def get_default_company(user=None): '''Get default company for user''' From 7c31e1f8bf4bd11029cfdf63b7c69f3736076e5a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 26 Aug 2021 19:49:27 +0530 Subject: [PATCH 147/951] chore: merge branch 'version-13-hotfix' into 'version-13-pre-release' (#27173) * feat: add provision for process loss in manufac * feat: add is process loss autoset and validation * fix: add warehouse and unset is scrap for process loss items * refactor: shift auto entry of is process loss check, update validations * test: add bom tests for process loss val, add se test for qty calc * fix: add more validations, remove source wh req for pl item * fix: sider * refactor: polyfill ?? * fix: sider * refactor: validation error message formatting * test: check manufacture completion qty in se and wo * fix: wo tests, sider, account for pl in se validation * fix: reword error messages, fix test values * feat: add procss_loss_qty field in work order * feat: process loss report, fix set pl query condition * fix: correct value in test * fix: get filters to work - reorder and rename columns - add work order filter * fix: Shopping cart Exchange rate validation (#27050) * fix: Shopping cart Exchange rate validation - Use `get_exchange_rate` to check for price list exchange rate in cart settings - Move cart exchange rate validation for Price List from hooks to doc event - Call cart exchange rate validation on PL update only if PL is in cart and currency is changed * chore: Comment out obsolete test - Modifying this test means considering extreme edge cases, which seems pointless now * fix: Remove snippet that got in due to cherry-pick from `develop` - This snippet is not present in v13-hotfix. Via https://github.com/frappe/erpnext/pull/26520 Co-authored-by: Nabin Hait * feat: initialize party link for customer & suppliers * feat: toggle to enable common party accounting * feat: auto create advance entry on invoice submission * test: creation of advance entry on invoice submission * fix: remove unwanted filter query * feat: validate multiple links * fix: party link permissions * perf: reduce number of queries to get party link * fix: cost center & naming series * fix: cost center in test_sales_invoice_against_supplier * fix: Don't create inward SLE against SI unless is internal customer enabled (#27086) * fix: Dont create inward SLE against SI unless is internal customer enabled - Check if is internal customer enabled apart from target warehouse - Test to check if inward SLE is made if target warehouse is accidentally set but customer is not internal * test: Use internal customer for delivery of bundle items to target warehouse - created `create_internal_customer` util - reused it in delivery note and sales invoice tests - use internal customer for target warehouse test in delivery note (cherry picked from commit f4dc9ee2aa57d82a0be747a89e1ca573940da959) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py * fix: prevent over riding scrap table values, name kwargs, set currency * fix(regional): minor fixes and test for South Africa VAT report (#26933) (#27162) * fix: allow to change incoming rate manually in case of stand-alone credit note (#27164) * fix: allow to change rate manually in case of stand-alone credit note (#27036) Co-authored-by: Marica (cherry picked from commit fe4540d74d0dfda170c2a781347d745fb9f86fb6) # Conflicts: # erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json * fix: resolve conflicts Co-authored-by: rohitwaghchaure Co-authored-by: Ankush Menat * fix: Fee Validity fixes (#27161) * fix: Fee Validity fixes (#27156) * chore: update Fee Validity form labels * fix: first appointment should not be considered for Fee Validity * fix: Fee Validity test cases * fix: appointment test case (cherry picked from commit 642b4c805cdf912fdc07de5b998df70091a8c8ac) * fix: overlapping appointments Co-authored-by: Rucha Mahabal * fix: Merge conflicts and place internal customer creation util in test_customer.py * fix: internal customer util returns 'str' not doc object * fix: negative qty validation on stock reco cancellation (#27170) (#27171) * test: negative stock validation on SR cancel * fix: negative stock setting ignored in stock reco In stock reconcilation cancellation negative stock setting is ignored as `db.get_value` is returning string `'0'` which is not casted to int/bool for further logic. This causes negative qty, which evantually gets caught by reposting but by design this should stop cancellation. * test: typo and minor refactor (cherry picked from commit e7109c18db6df4ffce80c936911b6c98327cdd0f) Co-authored-by: Ankush Menat Co-authored-by: 18alantom <2.alan.tom@gmail.com> Co-authored-by: Marica Co-authored-by: Nabin Hait Co-authored-by: Saqib Ansari Co-authored-by: Frappe PR Bot Co-authored-by: Ankush Menat Co-authored-by: Rucha Mahabal --- .../accounts_settings/accounts_settings.json | 9 +- .../accounts/doctype/party_link/__init__.py | 0 .../accounts/doctype/party_link/party_link.js | 33 ++ .../doctype/party_link/party_link.json | 102 +++++ .../accounts/doctype/party_link/party_link.py | 26 ++ .../doctype/party_link/test_party_link.py | 8 + .../purchase_invoice/purchase_invoice.py | 2 + .../doctype/sales_invoice/sales_invoice.py | 2 + .../sales_invoice/test_sales_invoice.py | 141 ++++-- .../sales_invoice_item.json | 12 +- erpnext/controllers/accounts_controller.py | 63 ++- .../controllers/sales_and_purchase_return.py | 32 +- erpnext/controllers/selling_controller.py | 29 +- .../doctype/fee_validity/fee_validity.json | 6 +- .../doctype/fee_validity/fee_validity.py | 14 +- .../doctype/fee_validity/test_fee_validity.py | 4 +- .../patient_appointment.py | 6 +- .../test_patient_appointment.py | 11 +- erpnext/hooks.py | 2 +- erpnext/manufacturing/doctype/bom/bom.js | 37 ++ erpnext/manufacturing/doctype/bom/bom.py | 38 +- erpnext/manufacturing/doctype/bom/test_bom.py | 81 +++- .../bom_scrap_item/bom_scrap_item.json | 429 ++++-------------- .../doctype/work_order/test_work_order.py | 65 +++ .../doctype/work_order/work_order.json | 30 +- .../doctype/work_order/work_order.py | 17 + .../vat_audit_report/test_vat_audit_report.py | 193 ++++++++ .../vat_audit_report/vat_audit_report.py | 11 +- .../selling/doctype/customer/test_customer.py | 23 + .../shopping_cart_settings.py | 61 +-- .../test_shopping_cart_settings.py | 26 +- .../delivery_note/test_delivery_note.py | 26 +- .../stock/doctype/price_list/price_list.py | 14 + .../stock/doctype/stock_entry/stock_entry.py | 34 +- .../tests/test_stock_entry_for_manufacture.js | 27 ++ .../stock_entry_detail.json | 9 +- .../stock_reconciliation.py | 2 +- .../test_stock_reconciliation.py | 45 +- .../report/process_loss_report/__init__.py | 0 .../process_loss_report.js | 44 ++ .../process_loss_report.json | 29 ++ .../process_loss_report.py | 132 ++++++ erpnext/stock/stock_ledger.py | 7 +- 43 files changed, 1366 insertions(+), 516 deletions(-) create mode 100644 erpnext/accounts/doctype/party_link/__init__.py create mode 100644 erpnext/accounts/doctype/party_link/party_link.js create mode 100644 erpnext/accounts/doctype/party_link/party_link.json create mode 100644 erpnext/accounts/doctype/party_link/party_link.py create mode 100644 erpnext/accounts/doctype/party_link/test_party_link.py create mode 100644 erpnext/regional/report/vat_audit_report/test_vat_audit_report.py create mode 100644 erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js create mode 100644 erpnext/stock/report/process_loss_report/__init__.py create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.js create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.json create mode 100644 erpnext/stock/report/process_loss_report/process_loss_report.py diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 49a2afee85f..935e29a9d33 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -18,6 +18,7 @@ "delete_linked_ledger_entries", "book_asset_depreciation_entry_automatically", "unlink_advance_payment_on_cancelation_of_order", + "enable_common_party_accounting", "post_change_gl_entries", "enable_discount_accounting", "tax_settings_section", @@ -269,6 +270,12 @@ "fieldname": "enable_discount_accounting", "fieldtype": "Check", "label": "Enable Discount Accounting" + }, + { + "default": "0", + "fieldname": "enable_common_party_accounting", + "fieldtype": "Check", + "label": "Enable Common Party Accounting" } ], "icon": "icon-cog", @@ -276,7 +283,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-07-12 18:54:29.084958", + "modified": "2021-08-19 11:17:38.788054", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/party_link/__init__.py b/erpnext/accounts/doctype/party_link/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/party_link/party_link.js b/erpnext/accounts/doctype/party_link/party_link.js new file mode 100644 index 00000000000..6da9291d64d --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.js @@ -0,0 +1,33 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Party Link', { + refresh: function(frm) { + frm.set_query('primary_role', () => { + return { + filters: { + name: ['in', ['Customer', 'Supplier']] + } + }; + }); + + frm.set_query('secondary_role', () => { + let party_types = Object.keys(frappe.boot.party_account_types) + .filter(p => p != frm.doc.primary_role); + return { + filters: { + name: ['in', party_types] + } + }; + }); + }, + + primary_role(frm) { + frm.set_value('primary_party', ''); + frm.set_value('secondary_role', ''); + }, + + secondary_role(frm) { + frm.set_value('secondary_party', ''); + } +}); diff --git a/erpnext/accounts/doctype/party_link/party_link.json b/erpnext/accounts/doctype/party_link/party_link.json new file mode 100644 index 00000000000..a1bb15f0d6b --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.json @@ -0,0 +1,102 @@ +{ + "actions": [], + "autoname": "ACC-PT-LNK-.###.", + "creation": "2021-08-18 21:06:53.027695", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "primary_role", + "secondary_role", + "column_break_2", + "primary_party", + "secondary_party" + ], + "fields": [ + { + "fieldname": "primary_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Primary Role", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "depends_on": "primary_role", + "fieldname": "secondary_role", + "fieldtype": "Link", + "label": "Secondary Role", + "mandatory_depends_on": "primary_role", + "options": "DocType" + }, + { + "depends_on": "primary_role", + "fieldname": "primary_party", + "fieldtype": "Dynamic Link", + "label": "Primary Party", + "mandatory_depends_on": "primary_role", + "options": "primary_role" + }, + { + "depends_on": "secondary_role", + "fieldname": "secondary_party", + "fieldtype": "Dynamic Link", + "label": "Secondary Party", + "mandatory_depends_on": "secondary_role", + "options": "secondary_role" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-08-25 20:08:56.761150", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Party Link", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "primary_party", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py new file mode 100644 index 00000000000..7d58506ce74 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -0,0 +1,26 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +class PartyLink(Document): + def validate(self): + if self.primary_role not in ['Customer', 'Supplier']: + frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."), + title=_("Invalid Primary Role")) + + existing_party_link = frappe.get_all('Party Link', { + 'primary_party': self.secondary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.secondary_role, self.secondary_party, existing_party_link[0])) + + existing_party_link = frappe.get_all('Party Link', { + 'secondary_party': self.primary_party + }, pluck="primary_role") + if existing_party_link: + frappe.throw(_('{} {} is already linked with another {}') + .format(self.primary_role, self.primary_party, existing_party_link[0])) diff --git a/erpnext/accounts/doctype/party_link/test_party_link.py b/erpnext/accounts/doctype/party_link/test_party_link.py new file mode 100644 index 00000000000..a3ea3959ba4 --- /dev/null +++ b/erpnext/accounts/doctype/party_link/test_party_link.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestPartyLink(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5094b1752b4..fdd8765b411 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -413,6 +413,8 @@ class PurchaseInvoice(BuyingController): self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) + self.process_common_party_accounting() + def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6e643db8d72..9e295d5ae54 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -253,6 +253,8 @@ class SalesInvoice(SellingController): if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_submit") + self.process_common_party_accounting() + def validate_pos_return(self): if self.is_pos and self.is_return: diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5bbde09c1f7..30e0db31f6c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -148,7 +148,7 @@ class TestSalesInvoice(unittest.TestCase): si1 = create_sales_invoice(rate=1000) si2 = create_sales_invoice(rate=300) si3 = create_sales_invoice(qty=-1, rate=300, is_return=1) - + pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC") pe.append('references', { @@ -1107,6 +1107,18 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + def test_incoming_rate_for_stand_alone_credit_note(self): + return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10, + company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1', + income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1') + + incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate') + debit_amount = frappe.db.get_value('GL Entry', + {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit') + + self.assertEqual(debit_amount, 10.0) + self.assertEqual(incoming_rate, 10.0) + def test_discount_on_net_total(self): si = frappe.copy_doc(test_records[2]) si.apply_discount_on = "Net Total" @@ -1783,23 +1795,13 @@ class TestSalesInvoice(unittest.TestCase): acc_settings.save() def test_inter_company_transaction(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer - if not frappe.db.exists("Customer", "_Test Internal Customer"): - customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Internal Customer", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": "_Test Company 1" - }) - - customer.append("companies", { - "company": "Wind Power LLC" - }) - - customer.insert() + create_internal_customer( + customer_name="_Test Internal Customer", + represents_company="_Test Company 1", + allowed_to_interact_with="Wind Power LLC" + ) if not frappe.db.exists("Supplier", "_Test Internal Supplier"): supplier = frappe.get_doc({ @@ -1842,8 +1844,43 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_sle_if_target_warehouse_exists_accidentally(self): + """ + Check if inward entry exists if Target Warehouse accidentally exists + but Customer is not an internal customer. + """ + se = make_stock_entry( + item_code="138-CMS Shoe", + target="Finished Goods - _TC", + company = "_Test Company", + qty=1, + basic_rate=500 + ) + + si = frappe.copy_doc(test_records[0]) + si.update_stock = 1 + si.set_warehouse = "Finished Goods - _TC" + si.set_target_warehouse = "Stores - _TC" + si.get("items")[0].warehouse = "Finished Goods - _TC" + si.get("items")[0].target_warehouse = "Stores - _TC" + si.insert() + si.submit() + + sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name}, + fields=["name", "actual_qty"]) + + # check if only one SLE for outward entry is created + self.assertEqual(len(sles), 1) + self.assertEqual(sles[0].actual_qty, -1) + + # tear down + si.cancel() + se.cancel() + def test_internal_transfer_gl_entry(self): ## Create internal transfer account + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + account = create_account(account_name="Unrealized Profit", parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") @@ -2071,6 +2108,50 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) enable_discount_accounting(enable=0) + def test_sales_invoice_against_supplier(self): + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import make_customer + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # create a customer + customer = make_customer(customer="_Test Common Supplier") + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Supplier").name + + # create a party link between customer & supplier + # set primary role as supplier + party_link = frappe.new_doc("Party Link") + party_link.primary_role = "Supplier" + party_link.primary_party = supplier + party_link.secondary_role = "Customer" + party_link.secondary_party = customer + party_link.save() + + # enable common party accounting + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) + + # create a sales invoice + si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC") + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, 'Paid') + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all('Journal Entry Account', { + 'account': si.debit_to, + 'party_type': 'Customer', + 'party': si.customer, + 'reference_type': si.doctype, + 'reference_name': si.name + }, pluck='credit_in_account_currency') + + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + party_link.delete() + frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' @@ -2283,7 +2364,8 @@ def create_sales_invoice(**args): "discount_amount": args.discount_amount or 0, "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, - "conversion_factor": 1 + "conversion_factor": 1, + "incoming_rate": args.incoming_rate or 0 }) if not args.do_not_save: @@ -2380,29 +2462,6 @@ def get_taxes_and_charges(): "row_id": 1 }] -def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): - if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": represents_company - }) - - customer.append("companies", { - "company": allowed_to_interact_with - }) - - customer.insert() - customer_name = customer.name - else: - customer_name = frappe.db.get_value("Customer", customer_name) - - return customer_name - def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Supplier", supplier_name): supplier = frappe.get_doc({ 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 eede3268365..d27a3a779ed 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -53,7 +53,6 @@ "column_break_24", "base_net_rate", "base_net_amount", - "incoming_rate", "drop_ship", "delivered_by_supplier", "accounting", @@ -81,6 +80,7 @@ "target_warehouse", "quality_inspection", "batch_no", + "incoming_rate", "col_break5", "allow_zero_valuation_rate", "serial_no", @@ -808,12 +808,12 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_return && parent.update_stock && !parent.return_against", "fieldname": "incoming_rate", "fieldtype": "Currency", - "label": "Incoming Rate", + "label": "Incoming Rate (Costing)", "no_copy": 1, - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "depends_on": "eval: doc.uom != doc.stock_uom", @@ -834,7 +834,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-12 20:15:42.668399", + "modified": "2021-08-19 13:41:53.435827", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -842,4 +842,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b17d1868d99..e02e7351520 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -14,7 +14,7 @@ from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_a from erpnext.utilities.transaction_base import TransactionBase from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.sales_and_purchase_return import validate_return -from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled +from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_account from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, apply_pricing_rule_for_free_items, get_applied_pricing_rules) from erpnext.exceptions import InvalidCurrency @@ -1368,6 +1368,67 @@ class AccountsController(TransactionBase): return False + def process_common_party_accounting(self): + is_invoice = self.doctype in ['Sales Invoice', 'Purchase Invoice'] + if not is_invoice: + return + + if frappe.db.get_single_value('Accounts Settings', 'enable_common_party_accounting'): + party_link = self.get_common_party_link() + if party_link and self.outstanding_amount: + self.create_advance_and_reconcile(party_link) + + def get_common_party_link(self): + party_type, party = self.get_party() + return frappe.db.get_value( + doctype='Party Link', + filters={'secondary_role': party_type, 'secondary_party': party}, + fieldname=['primary_role', 'primary_party'], + as_dict=True + ) + + def create_advance_and_reconcile(self, party_link): + secondary_party_type, secondary_party = self.get_party() + primary_party_type, primary_party = party_link.primary_role, party_link.primary_party + + primary_account = get_party_account(primary_party_type, primary_party, self.company) + secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) + + jv = frappe.new_doc('Journal Entry') + jv.voucher_type = 'Journal Entry' + jv.posting_date = self.posting_date + jv.company = self.company + jv.remark = 'Adjustment for {} {}'.format(self.doctype, self.name) + + reconcilation_entry = frappe._dict() + advance_entry = frappe._dict() + + reconcilation_entry.account = secondary_account + reconcilation_entry.party_type = secondary_party_type + reconcilation_entry.party = secondary_party + reconcilation_entry.reference_type = self.doctype + reconcilation_entry.reference_name = self.name + reconcilation_entry.cost_center = self.cost_center + + advance_entry.account = primary_account + advance_entry.party_type = primary_party_type + advance_entry.party = primary_party + advance_entry.cost_center = self.cost_center + advance_entry.is_advance = 'Yes' + + if self.doctype == 'Sales Invoice': + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + advance_entry.debit_in_account_currency = self.outstanding_amount + else: + advance_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + + jv.append('accounts', reconcilation_entry) + jv.append('accounts', advance_entry) + + jv.save() + jv.submit() + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 5ee1f2f7fb5..01486fcd65d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -394,19 +394,6 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None if not return_against: return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against") - if not return_against and voucher_type == 'Sales Invoice' and sle: - return get_incoming_rate({ - "item_code": sle.item_code, - "warehouse": sle.warehouse, - "posting_date": sle.get('posting_date'), - "posting_time": sle.get('posting_time'), - "qty": sle.actual_qty, - "serial_no": sle.get('serial_no'), - "company": sle.company, - "voucher_type": sle.voucher_type, - "voucher_no": sle.voucher_no - }, raise_error_if_no_rate=False) - return_against_item_field = get_return_against_item_fields(voucher_type) filters = get_filters(voucher_type, voucher_no, voucher_detail_no, @@ -417,7 +404,24 @@ def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None else: select_field = "abs(stock_value_difference / actual_qty)" - return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) + rate = flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field)) + if not (rate and return_against) and voucher_type in ['Sales Invoice', 'Delivery Note']: + rate = frappe.db.get_value(f'{voucher_type} Item', voucher_detail_no, 'incoming_rate') + + if not rate and sle: + rate = get_incoming_rate({ + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.get('posting_date'), + "posting_time": sle.get('posting_time'), + "qty": sle.actual_qty, + "serial_no": sle.get('serial_no'), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no + }, raise_error_if_no_rate=False) + + return rate def get_return_against_item_fields(voucher_type): return_against_item_fields = { diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index fc2cc97e0a5..844c40c8a64 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -362,7 +362,7 @@ class SellingController(StockController): sales_order.update_reserved_qty(so_item_rows) def set_incoming_rate(self): - if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"): + if self.doctype not in ("Delivery Note", "Sales Invoice"): return items = self.get("items") + (self.get("packed_items") or []) @@ -371,18 +371,19 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get('stock_qty') or d.get('actual_qty')) - d.incoming_rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": self.get('posting_date') or self.get('transaction_date'), - "posting_time": self.get('posting_time') or nowtime(), - "qty": qty if cint(self.get("is_return")) else (-1 * qty), - "serial_no": d.get('serial_no'), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - "allow_zero_valuation": d.get("allow_zero_valuation") - }, raise_error_if_no_rate=False) + if not d.incoming_rate: + d.incoming_rate = get_incoming_rate({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": self.get('posting_date') or self.get('transaction_date'), + "posting_time": self.get('posting_time') or nowtime(), + "qty": qty if cint(self.get("is_return")) else (-1 * qty), + "serial_no": d.get('serial_no'), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation") + }, raise_error_if_no_rate=False) # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): @@ -422,7 +423,7 @@ class SellingController(StockController): or (cint(self.is_return) and self.docstatus==2)): sl_entries.append(self.get_sle_for_source_warehouse(d)) - if d.target_warehouse: + if d.target_warehouse and self.get("is_internal_customer"): sl_entries.append(self.get_sle_for_target_warehouse(d)) if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.json b/erpnext/healthcare/doctype/fee_validity/fee_validity.json index b001bf024ce..d76b42e6836 100644 --- a/erpnext/healthcare/doctype/fee_validity/fee_validity.json +++ b/erpnext/healthcare/doctype/fee_validity/fee_validity.json @@ -46,13 +46,13 @@ { "fieldname": "visited", "fieldtype": "Int", - "label": "Visited yet", + "label": "Visits Completed", "read_only": 1 }, { "fieldname": "valid_till", "fieldtype": "Date", - "label": "Valid till", + "label": "Valid Till", "read_only": 1 }, { @@ -106,7 +106,7 @@ ], "in_create": 1, "links": [], - "modified": "2020-03-17 20:25:06.487418", + "modified": "2021-08-26 10:51:05.609349", "modified_by": "Administrator", "module": "Healthcare", "name": "Fee Validity", diff --git a/erpnext/healthcare/doctype/fee_validity/fee_validity.py b/erpnext/healthcare/doctype/fee_validity/fee_validity.py index 5b9c17934fa..59586e0c31b 100644 --- a/erpnext/healthcare/doctype/fee_validity/fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/fee_validity.py @@ -11,7 +11,6 @@ import datetime class FeeValidity(Document): def validate(self): self.update_status() - self.set_start_date() def update_status(self): if self.visited >= self.max_visits: @@ -19,13 +18,6 @@ class FeeValidity(Document): else: self.status = 'Pending' - def set_start_date(self): - self.start_date = getdate() - for appointment in self.ref_appointments: - appointment_date = frappe.db.get_value('Patient Appointment', appointment.appointment, 'appointment_date') - if getdate(appointment_date) < self.start_date: - self.start_date = getdate(appointment_date) - def create_fee_validity(appointment): if not check_is_new_patient(appointment): @@ -36,11 +28,9 @@ def create_fee_validity(appointment): fee_validity.patient = appointment.patient fee_validity.max_visits = frappe.db.get_single_value('Healthcare Settings', 'max_visits') or 1 valid_days = frappe.db.get_single_value('Healthcare Settings', 'valid_days') or 1 - fee_validity.visited = 1 + fee_validity.visited = 0 + fee_validity.start_date = getdate(appointment.appointment_date) fee_validity.valid_till = getdate(appointment.appointment_date) + datetime.timedelta(days=int(valid_days)) - fee_validity.append('ref_appointments', { - 'appointment': appointment.name - }) fee_validity.save(ignore_permissions=True) return fee_validity diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 29b4c5c9b98..957f85211de 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -22,14 +22,14 @@ class TestFeeValidity(unittest.TestCase): item = create_healthcare_service_items() healthcare_settings = frappe.get_single("Healthcare Settings") healthcare_settings.enable_free_follow_ups = 1 - healthcare_settings.max_visits = 2 + healthcare_settings.max_visits = 1 healthcare_settings.valid_days = 7 healthcare_settings.automate_appointment_invoicing = 1 healthcare_settings.op_consulting_charge_item = item healthcare_settings.save(ignore_permissions=True) patient, practitioner = create_healthcare_docs() - # For first appointment, invoice is generated + # For first appointment, invoice is generated. First appointment not considered in fee validity appointment = create_appointment(patient, practitioner, nowdate()) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") self.assertEqual(invoiced, 1) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 10f2d537891..36047c48381 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -137,9 +137,13 @@ class PatientAppointment(Document): frappe.db.set_value('Patient Appointment', self.name, 'notes', comments) def update_fee_validity(self): + if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): + return + fee_validity = manage_fee_validity(self) if fee_validity: - frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) + frappe.msgprint(_('{0}: {1} has fee validity till {2}').format(self.patient, + frappe.bold(self.patient_name), fee_validity.valid_till)) @frappe.whitelist() def get_therapy_types(self): diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 062a32a92e6..d0db3226326 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -110,18 +110,21 @@ class TestPatientAppointment(unittest.TestCase): patient, practitioner = create_healthcare_docs() frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) appointment = create_appointment(patient, practitioner, nowdate()) - fee_validity = frappe.db.get_value('Fee Validity Reference', {'appointment': appointment.name}, 'parent') + fee_validity = frappe.db.get_value('Fee Validity', {'patient': patient, 'practitioner': practitioner}) # fee validity created self.assertTrue(fee_validity) - visited = frappe.db.get_value('Fee Validity', fee_validity, 'visited') + # first follow up appointment + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1)) + self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 1) + update_status(appointment.name, 'Cancelled') # check fee validity updated - self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), visited - 1) + self.assertEqual(frappe.db.get_value('Fee Validity', fee_validity, 'visited'), 0) frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) - appointment = create_appointment(patient, practitioner, nowdate(), invoice=1) + appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1) update_status(appointment.name, 'Cancelled') # check invoice cancelled sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent') diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 748bd088077..aede8ff2f46 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -243,7 +243,7 @@ doc_events = { "on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions", "erpnext.portal.utils.set_default_role"] }, - ("Sales Taxes and Charges Template", 'Price List'): { + "Sales Taxes and Charges Template": { "on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings" }, "Website Settings": { diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 04aa8a43da5..ef074052620 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -446,6 +446,11 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) { }, callback: function(r) { d = locals[cdt][cdn]; + if (d.is_process_loss) { + r.message.rate = 0; + r.message.base_rate = 0; + } + $.extend(d, r.message); refresh_field("items"); refresh_field("scrap_items"); @@ -655,3 +660,35 @@ frappe.ui.form.on("BOM", "with_operations", function(frm) { frm.set_value("operations", []); } }); + +frappe.ui.form.on("BOM Scrap Item", { + item_code(frm, cdt, cdn) { + const { item_code } = locals[cdt][cdn]; + if (item_code === frm.doc.item) { + locals[cdt][cdn].is_process_loss = 1; + trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code); + } + }, +}); + +function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) { + frappe.prompt( + { + fieldname: "percent", + fieldtype: "Percent", + label: __("% Finished Item Quantity"), + description: + __("Set quantity of process loss item:") + + ` ${item_code} ` + + __("as a percentage of finished item quantity"), + }, + (data) => { + const row = locals[cdt][cdn]; + row.stock_qty = (frm.doc.quantity * data.percent) / 100; + row.qty = row.stock_qty / (row.conversion_factor || 1); + refresh_field("scrap_items"); + }, + __("Set Process Loss Item Quantity"), + __("Set Quantity") + ); +} diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index eb1dfc8cae8..70237f9147f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -154,9 +154,11 @@ class BOM(WebsiteGenerator): self.validate_operations() self.calculate_cost() self.update_stock_qty() + self.validate_scrap_items() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) self.set_bom_level() + def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -230,7 +232,7 @@ class BOM(WebsiteGenerator): } ret = self.get_bom_material_detail(args) for key, value in ret.items(): - if not item.get(key): + if item.get(key) is None: item.set(key, value) @frappe.whitelist() @@ -687,6 +689,33 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + + def validate_scrap_items(self): + for item in self.scrap_items: + msg = "" + if item.item_code == self.item and not item.is_process_loss: + msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked.') \ + .format(frappe.bold(item.item_code)) + elif item.item_code != self.item and item.is_process_loss: + msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked') \ + .format(frappe.bold(item.item_code)) + + must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") + if item.is_process_loss and must_be_whole_number: + msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM.") \ + .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) + + if item.is_process_loss and (item.stock_qty >= self.quantity): + msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \ + .format(frappe.bold(item.item_code)) + + if item.is_process_loss and (item.rate > 0): + msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked.") \ + .format(frappe.bold(item.item_code)) + + if msg: + frappe.throw(msg, title=_("Note")) + def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) @@ -822,8 +851,11 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) elif fetch_scrap_items: - query = query.format(table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description", is_stock_item=is_stock_item, qty_field="stock_qty") + query = query.format( + table="BOM Scrap Item", where_conditions="", + select_columns=", bom_item.idx, item.description, is_process_loss", + is_stock_item=is_stock_item, qty_field="stock_qty" + ) items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) else: diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 57a54587269..6a81ac33679 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -226,6 +226,40 @@ class TestBOM(unittest.TestCase): supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) + + def test_bom_with_process_loss_item(self): + fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() + + if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"): + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1 + ) + bom_doc.submit() + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 + ) + # PL Item qty can't be >= FG Item qty + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100 + ) + # PL Item rate has to be 0 + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0 + ) + # Items with whole UOMs can't be PL Items + self.assertRaises(frappe.ValidationError, bom_doc.submit) + + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0 + ) + # FG Items in Scrap/Loss Table should have Is Process Loss set + self.assertRaises(frappe.ValidationError, bom_doc.submit) + def test_bom_tree_representation(self): bom_tree = { "Assembly": { @@ -248,13 +282,9 @@ class TestBOM(unittest.TestCase): for reqd_item, created_item in zip(reqd_order, created_order): self.assertEqual(reqd_item, created_item.item_code) - def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) - - - def level_order_traversal(node): traversal = [] q = deque() @@ -300,6 +330,7 @@ def create_nested_bom(tree, prefix="_Test bom "): bom = frappe.get_doc(doctype="BOM", item=bom_item_code) for child_item in child_items.keys(): bom.append("items", {"item_code": prefix + child_item}) + bom.currency = "INR" bom.insert() bom.submit() @@ -321,3 +352,45 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non for warehouse in warehouse_list: create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) + +def create_bom_with_process_loss_item( + fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1): + bom_doc = frappe.new_doc("BOM") + bom_doc.item = fg_item.item_code + bom_doc.quantity = fg_qty + bom_doc.append("items", { + "item_code": bom_item.item_code, + "qty": 1, + "uom": bom_item.stock_uom, + "stock_uom": bom_item.stock_uom, + "rate": 100.0 + }) + bom_doc.append("scrap_items", { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + "is_process_loss": is_process_loss + }) + bom_doc.currency = "INR" + return bom_doc + +def create_process_loss_bom_items(): + item_list = [ + ("_Test Item - Non Whole UOM", "Kg"), + ("_Test Item - Whole UOM", "Unit"), + ("_Test PL BOM Item", "Unit") + ] + return [create_process_loss_bom_item(it) for it in item_list] + +def create_process_loss_bom_item(item_tuple): + item_code, stock_uom = item_tuple + if frappe.db.exists("Item", item_code) is None: + return make_item( + item_code, + {'stock_uom':stock_uom, 'valuation_rate':100} + ) + else: + return frappe.get_doc("Item", item_code) diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json index 9f7091dd8d7..7018082e402 100644 --- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json +++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json @@ -1,345 +1,112 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-26 02:19:21.642081", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-09-26 02:19:21.642081", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "is_process_loss", + "quantity_and_rate", + "stock_qty", + "rate", + "amount", + "column_break_6", + "stock_uom", + "base_rate", + "base_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quantity_and_rate", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Quantity and Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "quantity_and_rate", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amount", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_6", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Stock UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_rate", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Basic Rate (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "base_rate", + "fieldtype": "Currency", + "label": "Basic Rate (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Basic Amount (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "base_amount", + "fieldtype": "Currency", + "label": "Basic Amount (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-07-04 16:04:32.442287", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Scrap Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2021-06-22 16:46:12.153311", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Scrap Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bf1ccb71594..3a334a530cd 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -690,6 +690,71 @@ class TestWorkOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + def test_wo_completion_with_pl_bom(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_process_loss_bom_items + from erpnext.manufacturing.doctype.bom.test_bom import create_bom_with_process_loss_item + + qty = 4 + scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG + source_warehouse = "Stores - _TC" + wip_warehouse = "_Test Warehouse - _TC" + fg_item_non_whole, _, bom_item = create_process_loss_bom_items() + + test_stock_entry.make_stock_entry(item_code=bom_item.item_code, + target=source_warehouse, qty=4, basic_rate=100) + + bom_no = f"BOM-{fg_item_non_whole.item_code}-001" + if not frappe.db.exists("BOM", bom_no): + bom_doc = create_bom_with_process_loss_item( + fg_item_non_whole, bom_item, scrap_qty=scrap_qty, + scrap_rate=0, fg_qty=1, is_process_loss=1 + ) + bom_doc.submit() + + wo = make_wo_order_test_record( + production_item=fg_item_non_whole.item_code, + bom_no=bom_no, + wip_warehouse=wip_warehouse, + qty=qty, + skip_transfer=1, + stock_uom=fg_item_non_whole.stock_uom, + ) + + se = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", qty) + ) + se.get("items")[0].s_warehouse = "Stores - _TC" + se.insert() + se.submit() + + se = frappe.get_doc( + make_stock_entry(wo.name, "Manufacture", qty) + ) + se.insert() + se.submit() + + # Testing stock entry values + items = se.get("items") + self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + + source_item, fg_item, pl_item = items + + total_pl_qty = qty * scrap_qty + actual_fg_qty = qty - total_pl_qty + + self.assertEqual(pl_item.qty, total_pl_qty) + self.assertEqual(fg_item.qty, actual_fg_qty) + + # Testing Work Order values + self.assertEqual( + frappe.db.get_value("Work Order", wo.name, "produced_qty"), + qty + ) + self.assertEqual( + frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), + total_pl_qty + ) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 3b56854aaf3..913fc85af61 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -19,6 +19,7 @@ "qty", "material_transferred_for_manufacturing", "produced_qty", + "process_loss_qty", "sales_order", "project", "serial_no_and_batch_for_finished_good_section", @@ -64,16 +65,12 @@ "description", "stock_uom", "column_break2", - "references_section", "material_request", "material_request_item", "sales_order_item", - "column_break_61", "production_plan", "production_plan_item", "production_plan_sub_assembly_item", - "parent_work_order", - "bom_level", "product_bundle_item", "amended_from" ], @@ -553,20 +550,29 @@ "read_only": 1 }, { - "fieldname": "production_plan_sub_assembly_item", - "fieldtype": "Data", - "label": "Production Plan Sub-assembly Item", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - } + "fieldname": "production_plan_sub_assembly_item", + "fieldtype": "Data", + "label": "Production Plan Sub-assembly Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval: doc.process_loss_qty", + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "no_copy": 1, + "non_negative": 1, + "read_only": 1 + } ], "icon": "fa fa-cogs", "idx": 1, "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-06-28 16:19:14.902699", + "modified": "2021-08-24 15:14:03.844937", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 5fe9fec2af1..24b33d523e4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -214,6 +214,7 @@ class WorkOrder(Document): self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError) self.db_set(fieldname, qty) + self.set_process_loss_qty() from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item @@ -223,6 +224,22 @@ class WorkOrder(Document): if self.production_plan: self.update_production_plan_status() + def set_process_loss_qty(self): + process_loss_qty = flt(frappe.db.sql(""" + SELECT sum(qty) FROM `tabStock Entry Detail` + WHERE + is_process_loss=1 + AND parent IN ( + SELECT name FROM `tabStock Entry` + WHERE + work_order=%s + AND purpose='Manufacture' + AND docstatus=1 + ) + """, (self.name, ))[0][0]) + if process_loss_qty is not None: + self.db_set('process_loss_qty', process_loss_qty) + def update_production_plan_status(self): production_plan = frappe.get_doc('Production Plan', self.production_plan) produced_qty = 0 diff --git a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py new file mode 100644 index 00000000000..dea17a66fda --- /dev/null +++ b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py @@ -0,0 +1,193 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from unittest import TestCase +from frappe.utils import today + +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + +from erpnext.regional.report.vat_audit_report.vat_audit_report import execute + +class TestVATAuditReport(TestCase): + def setUp(self): + frappe.set_user("Administrator") + make_company("_Test Company SA VAT", "_TCSV") + + create_account(account_name="VAT - 0%", account_type="Tax", + parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + create_account(account_name="VAT - 15%", account_type="Tax", + parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + set_sa_vat_accounts() + + make_item("_Test SA VAT Item") + make_item("_Test SA VAT Zero Rated Item", properties = {"is_zero_rated": 1}) + + make_customer() + make_supplier() + + make_sales_invoices() + create_purchase_invoices() + + def tearDown(self): + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company SA VAT'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company SA VAT'") + + def test_vat_audit_report(self): + filters = { + "company": "_Test Company SA VAT", + "from_date": today(), + "to_date": today() + } + columns, data = execute(filters) + total_tax_amount = 0 + total_row_tax = 0 + for row in data: + keys = row.keys() + # skips total row tax_amount in if.. and skips section header in elif.. + if 'voucher_no' in keys: + total_tax_amount = total_tax_amount + row['tax_amount'] + elif 'tax_amount' in keys: + total_row_tax = total_row_tax + row['tax_amount'] + + self.assertEqual(total_tax_amount, total_row_tax) + +def make_company(company_name, abbr): + if not frappe.db.exists("Company", company_name): + company = frappe.get_doc({ + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "ZAR", + "country": "South Africa", + "create_chart_of_accounts_based_on": "Standard Template" + }) + company.insert() + else: + company = frappe.get_doc("Company", company_name) + + company.create_default_warehouses() + + if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): + company.create_default_cost_center() + + company.save() + + return company + +def set_sa_vat_accounts(): + if not frappe.db.exists("South Africa VAT Settings", "_Test Company SA VAT"): + vat_accounts = frappe.get_all( + "Account", + fields=["name"], + filters = { + "company": "_Test Company SA VAT", + "is_group": 0, + "account_type": "Tax" + } + ) + + sa_vat_accounts = [] + for account in vat_accounts: + sa_vat_accounts.append({ + "doctype": "South Africa VAT Account", + "account": account.name + }) + + frappe.get_doc({ + "company": "_Test Company SA VAT", + "vat_accounts": sa_vat_accounts, + "doctype": "South Africa VAT Settings", + }).insert() + +def make_customer(): + if not frappe.db.exists("Customer", "_Test SA Customer"): + frappe.get_doc({ + "doctype": "Customer", + "customer_name": "_Test SA Customer", + "customer_type": "Company", + }).insert() + +def make_supplier(): + if not frappe.db.exists("Supplier", "_Test SA Supplier"): + frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": "_Test SA Supplier", + "supplier_type": "Company", + "supplier_group":"All Supplier Groups" + }).insert() + +def make_item(item_code, properties=None): + if not frappe.db.exists("Item", item_code): + item = frappe.get_doc({ + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products" + }) + + if properties: + item.update(properties) + + item.insert() + +def make_sales_invoices(): + def make_sales_invoices_wrapper(item, rate, tax_account, tax_rate, tax=True): + si = create_sales_invoice( + company="_Test Company SA VAT", + customer = "_Test SA Customer", + currency = "ZAR", + item=item, + rate=rate, + warehouse = "Finished Goods - _TCSV", + debit_to = "Debtors - _TCSV", + income_account = "Sales - _TCSV", + expense_account = "Cost of Goods Sold - _TCSV", + cost_center = "Main - _TCSV", + do_not_save=1 + ) + if tax: + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": tax_account, + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": tax_rate + }) + + si.submit() + + test_item = "_Test SA VAT Item" + test_zero_rated_item = "_Test SA VAT Zero Rated Item" + + make_sales_invoices_wrapper(test_item, 100.0, "VAT - 15% - _TCSV", 15.0) + make_sales_invoices_wrapper(test_zero_rated_item, 100.0, "VAT - 0% - _TCSV", 0.0) + +def create_purchase_invoices(): + pi = make_purchase_invoice( + company = "_Test Company SA VAT", + supplier = "_Test SA Supplier", + supplier_warehouse = "Finished Goods - _TCSV", + warehouse = "Finished Goods - _TCSV", + currency = "ZAR", + cost_center = "Main - _TCSV", + expense_account = "Cost of Goods Sold - _TCSV", + item = "_Test SA VAT Item", + qty = 1, + rate = 100, + uom = "Nos", + do_not_save = 1 + ) + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "VAT - 15% - _TCSV", + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": 15.0 + }) + + pi.submit() diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 292605ef13d..ebf297113d7 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -1,11 +1,11 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals import frappe import json from frappe import _ -from frappe.utils import formatdate +from frappe.utils import formatdate, get_link_to_form def execute(filters=None): return VATAuditReport(filters).run() @@ -42,7 +42,8 @@ class VATAuditReport(object): self.sa_vat_accounts = frappe.get_list("South Africa VAT Account", filters = {"parent": self.filters.company}, pluck="account") if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: - frappe.throw(_("Please set VAT Accounts in South Africa VAT Settings")) + link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") + frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings)) def get_invoice_data(self, doctype): conditions = self.get_conditions() @@ -69,7 +70,7 @@ class VATAuditReport(object): items = frappe.db.sql(""" SELECT - item_code, parent, taxable_value, base_net_amount, is_zero_rated + item_code, parent, base_net_amount, is_zero_rated FROM `tab%s Item` WHERE @@ -79,7 +80,7 @@ class VATAuditReport(object): if d.item_code not in self.invoice_items.get(d.parent, {}): self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, { 'net_amount': 0.0}) - self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('base_net_amount', 0) self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated def get_items_based_on_tax_rate(self, doctype): diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index b1a5b52f963..5b337313d3d 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -352,3 +352,26 @@ def set_credit_limit(customer, company, credit_limit): 'credit_limit': credit_limit }) customer.credit_limits[-1].db_insert() + +def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company + }) + + customer.append("companies", { + "company": allowed_to_interact_with + }) + + customer.insert() + customer_name = customer.name + else: + customer_name = frappe.db.get_value("Customer", customer_name) + + return customer_name \ No newline at end of file diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py index 2a497225fbc..efed1968a14 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _, msgprint -from frappe.utils import comma_and +from frappe.utils import flt from frappe.model.document import Document from frappe.utils import get_datetime, get_datetime_str, now_datetime @@ -18,46 +18,35 @@ class ShoppingCartSettings(Document): def validate(self): if self.enabled: - self.validate_exchange_rates_exist() + self.validate_price_list_exchange_rate() + + def validate_price_list_exchange_rate(self): + "Check if exchange rate exists for Price List currency (to Company's currency)." + from erpnext.setup.utils import get_exchange_rate + + if not self.enabled or not self.company or not self.price_list: + return # this function is also called from hooks, check values again + + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency") - def validate_exchange_rates_exist(self): - """check if exchange rates exist for all Price List currencies (to company's currency)""" - company_currency = frappe.get_cached_value('Company', self.company, "default_currency") if not company_currency: - msgprint(_("Please specify currency in Company") + ": " + self.company, - raise_exception=ShoppingCartSetupError) + msg = f"Please specify currency in Company {self.company}" + frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - price_list_currency_map = frappe.db.get_values("Price List", - [self.price_list], "currency") + if not price_list_currency: + msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}" + frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError) - price_list_currency_map = dict(price_list_currency_map) + if price_list_currency != company_currency: + from_currency, to_currency = price_list_currency, company_currency - # check if all price lists have a currency - for price_list, currency in price_list_currency_map.items(): - if not currency: - frappe.throw(_("Currency is required for Price List {0}").format(price_list)) + # Get exchange rate checks Currency Exchange Records too + exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling") - expected_to_exist = [currency + "-" + company_currency - for currency in price_list_currency_map.values() - if currency != company_currency] - - # manqala 20/09/2016: set up selection parameters for query from tabCurrency Exchange - from_currency = [currency for currency in price_list_currency_map.values() if currency != company_currency] - to_currency = company_currency - # manqala end - - if expected_to_exist: - # manqala 20/09/2016: modify query so that it uses date in the selection from Currency Exchange. - # exchange rates defined with date less than the date on which this document is being saved will be selected - exists = frappe.db.sql_list("""select CONCAT(from_currency,'-',to_currency) from `tabCurrency Exchange` - where from_currency in (%s) and to_currency = "%s" and date <= curdate()""" % (", ".join(["%s"]*len(from_currency)), to_currency), tuple(from_currency)) - # manqala end - - missing = list(set(expected_to_exist).difference(exists)) - - if missing: - msgprint(_("Missing Currency Exchange Rates for {0}").format(comma_and(missing)), - raise_exception=ShoppingCartSetupError) + if not flt(exchange_rate): + msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}" + frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError) def validate_tax_rule(self): if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart" : 1}, "name"): @@ -71,7 +60,7 @@ class ShoppingCartSettings(Document): def get_shipping_rules(self, shipping_territory): return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule") -def validate_cart_settings(doc, method): +def validate_cart_settings(doc=None, method=None): frappe.get_doc("Shopping Cart Settings", "Shopping Cart Settings").run_method("validate") def get_shopping_cart_settings(): diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py index 008751e2088..9965e1af672 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py @@ -16,17 +16,25 @@ class TestShoppingCartSettings(unittest.TestCase): return frappe.get_doc({"doctype": "Shopping Cart Settings", "company": "_Test Company"}) - def test_exchange_rate_exists(self): - frappe.db.sql("""delete from `tabCurrency Exchange`""") + # NOTE: Exchangrate API has all enabled currencies that ERPNext supports. + # We aren't checking just currency exchange record anymore + # while validating price list currency exchange rate to that of company. + # The API is being used to fetch the rate which again almost always + # gives back a valid value (for valid currencies). + # This makes the test obsolete. + # Commenting because im not sure if there's a better test we can write - cart_settings = self.get_cart_settings() - cart_settings.price_list = "_Test Price List Rest of the World" - self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist) + # def test_exchange_rate_exists(self): + # frappe.db.sql("""delete from `tabCurrency Exchange`""") - from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ - currency_exchange_records - frappe.get_doc(currency_exchange_records[0]).insert() - cart_settings.validate_exchange_rates_exist() + # cart_settings = self.get_cart_settings() + # cart_settings.price_list = "_Test Price List Rest of the World" + # self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate) + + # from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ + # currency_exchange_records + # frappe.get_doc(currency_exchange_records[0]).insert() + # cart_settings.validate_price_list_exchange_rate() def test_tax_rule_validation(self): frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 91e7c006eef..b333a6b57ea 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -430,12 +430,19 @@ class TestDeliveryNote(unittest.TestCase): }) def test_delivery_of_bundled_items_to_target_warehouse(self): + from erpnext.selling.doctype.customer.test_customer import create_internal_customer + company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + customer_name = create_internal_customer( + customer_name="_Test Internal Customer 2", + represents_company="_Test Company with perpetual inventory", + allowed_to_interact_with="_Test Company with perpetual inventory" + ) set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item Home Desktop 100", "FIFO") - target_warehouse=get_warehouse(company=company, abbr="TCP1", + target_warehouse = get_warehouse(company=company, abbr="TCP1", warehouse_name="_Test Customer Warehouse").name for warehouse in ("Stores - TCP1", target_warehouse): @@ -444,10 +451,16 @@ class TestDeliveryNote(unittest.TestCase): create_stock_reconciliation(item_code="_Test Item Home Desktop 100", company = company, expense_account = "Stock Adjustment - TCP1", warehouse=warehouse, qty=500, rate=100) - dn = create_delivery_note(item_code="_Test Product Bundle Item", - company='_Test Company with perpetual inventory', cost_center = 'Main - TCP1', - expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True, qty=5, rate=500, - warehouse="Stores - TCP1", target_warehouse=target_warehouse) + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + company="_Test Company with perpetual inventory", + customer=customer_name, + cost_center = 'Main - TCP1', + expense_account = "Cost of Goods Sold - TCP1", + do_not_submit=True, + qty=5, rate=500, + warehouse="Stores - TCP1", + target_warehouse=target_warehouse) dn.submit() @@ -487,6 +500,9 @@ class TestDeliveryNote(unittest.TestCase): for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) + # tear down + frappe.db.rollback() + def test_closed_delivery_note(self): from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 10abde17eb2..002d3d898eb 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -13,6 +13,9 @@ class PriceList(Document): if not cint(self.buying) and not cint(self.selling): throw(_("Price List must be applicable for Buying or Selling")) + if not self.is_new(): + self.check_impact_on_shopping_cart() + def on_update(self): self.set_default_if_missing() self.update_item_price() @@ -32,6 +35,17 @@ class PriceList(Document): buying=%s, selling=%s, modified=NOW() where price_list=%s""", (self.currency, cint(self.buying), cint(self.selling), self.name)) + def check_impact_on_shopping_cart(self): + "Check if Price List currency change impacts Shopping Cart." + from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import validate_cart_settings + + doc_before_save = self.get_doc_before_save() + currency_changed = self.currency != doc_before_save.currency + affects_cart = self.name == frappe.get_cached_value("Shopping Cart Settings", None, "price_list") + + if currency_changed and affects_cart: + validate_cart_settings() + def on_trash(self): self.delete_price_list_details_key() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2b2a80ce109..ba7c6d11337 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -272,7 +272,7 @@ class StockEntry(StockController): item_wise_qty = {} if self.purpose == "Manufacture" and self.work_order: for d in self.items: - if d.is_finished_item: + if d.is_finished_item or d.is_process_loss: item_wise_qty.setdefault(d.item_code, []).append(d.qty) for item_code, qty_list in iteritems(item_wise_qty): @@ -333,7 +333,7 @@ class StockEntry(StockController): if self.purpose == "Manufacture": if validate_for_manufacture: - if d.is_finished_item or d.is_scrap_item: + if d.is_finished_item or d.is_scrap_item or d.is_process_loss: d.s_warehouse = None if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) @@ -465,7 +465,7 @@ class StockEntry(StockController): """ # Set rate for outgoing items outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) + finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss) # Set basic rate for incoming items for d in self.get('items'): @@ -486,6 +486,8 @@ class StockEntry(StockController): raise_error_if_no_rate=raise_error_if_no_rate) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) + if d.is_process_loss: + d.basic_rate = flt(0.) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): @@ -1043,6 +1045,7 @@ class StockEntry(StockController): self.set_scrap_items() self.set_actual_qty() + self.update_items_for_process_loss() self.validate_customer_provided_item() self.calculate_rate_and_amount() @@ -1400,6 +1403,7 @@ class StockEntry(StockController): get_default_cost_center(item_dict[d], company = self.company)) se_child.is_finished_item = item_dict[d].get("is_finished_item", 0) se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) + se_child.is_process_loss = item_dict[d].get("is_process_loss", 0) for field in ["idx", "po_detail", "original_item", "expense_account", "description", "item_name", "serial_no", "batch_no"]: @@ -1578,6 +1582,30 @@ class StockEntry(StockController): if material_request and material_request not in material_requests: material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + + def update_items_for_process_loss(self): + process_loss_dict = {} + for d in self.get("items"): + if not d.is_process_loss: + continue + + scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse") + if scrap_warehouse is not None: + d.t_warehouse = scrap_warehouse + d.is_scrap_item = 0 + + if d.item_code not in process_loss_dict: + process_loss_dict[d.item_code] = [flt(0), flt(0)] + process_loss_dict[d.item_code][0] += flt(d.transfer_qty) + process_loss_dict[d.item_code][1] += flt(d.qty) + + for d in self.get("items"): + if not d.is_finished_item or d.item_code not in process_loss_dict: + continue + # Assumption: 1 finished item has 1 row. + d.transfer_qty -= process_loss_dict[d.item_code][0] + d.qty -= process_loss_dict[d.item_code][1] + def set_serial_no_batch_for_finished_good(self): args = {} diff --git a/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js new file mode 100644 index 00000000000..285ae4f59e8 --- /dev/null +++ b/erpnext/stock/doctype/stock_entry/tests/test_stock_entry_for_manufacture.js @@ -0,0 +1,27 @@ +QUnit.module('Stock'); + +QUnit.test("test manufacture from bom", function(assert) { + assert.expect(2); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make("Stock Entry", [ + { purpose: "Manufacture" }, + { from_bom: 1 }, + { bom_no: "BOM-_Test Item - Non Whole UOM-001" }, + { fg_completed_qty: 2 } + ]); + }, + () => cur_frm.save(), + () => frappe.click_button("Update Rate and Availability"), + () => { + assert.ok(cur_frm.doc.items[1] === 0.75, " Finished Item Qty correct"); + assert.ok(cur_frm.doc.items[2] === 0.25, " Process Loss Item Qty correct"); + }, + () => frappe.tests.click_button('Submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(0.3), + () => done() + ]); +}); + diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 22f412a2989..2282b6aa167 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -18,6 +18,7 @@ "col_break2", "is_finished_item", "is_scrap_item", + "is_process_loss", "quality_inspection", "subcontracted_item", "section_break_8", @@ -543,13 +544,19 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_process_loss", + "fieldtype": "Check", + "label": "Is Process Loss" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-21 16:03:18.834880", + "modified": "2021-06-22 16:47:11.268975", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 324bb7a62d9..4531652a13c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -390,7 +390,7 @@ class StockReconciliation(StockController): sl_entries = self.merge_similar_item_serial_nos(sl_entries) sl_entries.reverse() - allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 94b006c8944..e4381271ed2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -15,6 +15,7 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.tests.utils import change_settings class TestStockReconciliation(unittest.TestCase): @@ -310,6 +311,7 @@ class TestStockReconciliation(unittest.TestCase): pr2.cancel() pr1.cancel() + @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_future_negative_stock(self): """ Test if a backdated stock reco causes future negative stock and is blocked. @@ -327,8 +329,6 @@ class TestStockReconciliation(unittest.TestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) - negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0) pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2)) @@ -348,11 +348,50 @@ class TestStockReconciliation(unittest.TestCase): self.assertRaises(NegativeStockError, sr3.submit) # teardown - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting) sr3.cancel() dn2.cancel() pr1.cancel() + + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_backdated_stock_reco_cancellation_future_negative_stock(self): + """ + Test if a backdated stock reco cancellation that causes future negative stock is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) + DN | DN | 100 | 0 (posting date: today) + """ + from erpnext.stock.stock_ledger import NegativeStockError + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + frappe.db.commit() + + item_code = "Backdated-Reco-Cancellation-Item" + warehouse = "_Test Warehouse - _TC" + create_item(item_code) + + + sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=100, rate=100, + posting_date=add_days(nowdate(), -1)) + + dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=100, rate=120, + posting_date=nowdate()) + + dn_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, + "qty_after_transaction") + self.assertEqual(dn_balance, 0) + + # check if cancellation of stock reco is blocked + self.assertRaises(NegativeStockError, sr.cancel) + + repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name})) + self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation") + + # teardown + frappe.db.rollback() + + def test_valid_batch(self): create_batch_item_with_batch("Testing Batch Item 1", "001") create_batch_item_with_batch("Testing Batch Item 2", "002") diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/stock/report/process_loss_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/stock/report/process_loss_report/process_loss_report.js new file mode 100644 index 00000000000..b0c2b94a254 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.js @@ -0,0 +1,44 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Process Loss Report"] = { + filters: [ + { + label: __("Company"), + fieldname: "company", + fieldtype: "Link", + options: "Company", + mandatory: true, + default: frappe.defaults.get_user_default("Company"), + }, + { + label: __("Item"), + fieldname: "item", + fieldtype: "Link", + options: "Item", + mandatory: false, + }, + { + label: __("Work Order"), + fieldname: "work_order", + fieldtype: "Link", + options: "Work Order", + mandatory: false, + }, + { + label: __("From Date"), + fieldname: "from_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.year_start(), + }, + { + label: __("To Date"), + fieldname: "to_date", + fieldtype: "Date", + mandatory: true, + default: frappe.datetime.get_today(), + }, + ] +}; diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.json b/erpnext/stock/report/process_loss_report/process_loss_report.json new file mode 100644 index 00000000000..afe4aff7f1c --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-08-24 16:38:15.233395", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-08-24 16:38:15.233395", + "modified_by": "Administrator", + "module": "Stock", + "name": "Process Loss Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Work Order", + "report_name": "Process Loss Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Manufacturing User" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/stock/report/process_loss_report/process_loss_report.py new file mode 100644 index 00000000000..7494328ab43 --- /dev/null +++ b/erpnext/stock/report/process_loss_report/process_loss_report.py @@ -0,0 +1,132 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from typing import Dict, List, Tuple + +Filters = frappe._dict +Row = frappe._dict +Data = List[Row] +Columns = List[Dict[str, str]] +QueryArgs = Dict[str, str] + +def execute(filters: Filters) -> Tuple[Columns, Data]: + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_data(filters: Filters) -> Data: + query_args = get_query_args(filters) + data = run_query(query_args) + update_data_with_total_pl_value(data) + return data + +def get_columns() -> Columns: + return [ + { + 'label': 'Work Order', + 'fieldname': 'name', + 'fieldtype': 'Link', + 'options': 'Work Order', + 'width': '200' + }, + { + 'label': 'Item', + 'fieldname': 'production_item', + 'fieldtype': 'Link', + 'options': 'Item', + 'width': '100' + }, + { + 'label': 'Status', + 'fieldname': 'status', + 'fieldtype': 'Data', + 'width': '100' + }, + { + 'label': 'Manufactured Qty', + 'fieldname': 'produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Loss Qty', + 'fieldname': 'process_loss_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Actual Manufactured Qty', + 'fieldname': 'actual_produced_qty', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Loss Value', + 'fieldname': 'total_pl_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'FG Value', + 'fieldname': 'total_fg_value', + 'fieldtype': 'Float', + 'width': '150' + }, + { + 'label': 'Raw Material Value', + 'fieldname': 'total_rm_value', + 'fieldtype': 'Float', + 'width': '150' + } + ] + +def get_query_args(filters: Filters) -> QueryArgs: + query_args = {} + query_args.update(filters) + query_args.update( + get_filter_conditions(filters) + ) + return query_args + +def run_query(query_args: QueryArgs) -> Data: + return frappe.db.sql(""" + SELECT + wo.name, wo.status, wo.production_item, wo.qty, + wo.produced_qty, wo.process_loss_qty, + (wo.produced_qty - wo.process_loss_qty) as actual_produced_qty, + sum(se.total_incoming_value) as total_fg_value, + sum(se.total_outgoing_value) as total_rm_value + FROM + `tabWork Order` wo INNER JOIN `tabStock Entry` se + ON wo.name=se.work_order + WHERE + process_loss_qty > 0 + AND wo.company = %(company)s + AND se.docstatus = 1 + AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s + {item_filter} + {work_order_filter} + GROUP BY + se.work_order + """.format(**query_args), query_args, as_dict=1, debug=1) + +def update_data_with_total_pl_value(data: Data) -> None: + for row in data: + value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty'] + row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg + +def get_filter_conditions(filters: Filters) -> QueryArgs: + filter_conditions = dict(item_filter="", work_order_filter="") + if "item" in filters: + production_item = filters.get("item") + filter_conditions.update( + {"item_filter": f"AND wo.production_item='{production_item}'"} + ) + if "work_order" in filters: + work_order_name = filters.get("work_order") + filter_conditions.update( + {"work_order_filter": f"AND wo.name='{work_order_name}'"} + ) + return filter_conditions + diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f762cc7b890..afd3ab2b5ae 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -324,6 +324,7 @@ class update_entries_after(object): where item_code = %(item_code)s and warehouse = %(warehouse)s + and is_cancelled = 0 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) order by @@ -946,7 +947,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate -def update_qty_in_future_sle(args, allow_negative_stock=None): +def update_qty_in_future_sle(args, allow_negative_stock=False): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" qty_shift = args.actual_qty @@ -1035,8 +1036,8 @@ def get_datetime_limit_condition(detail): ) )""" -def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): - allow_negative_stock = allow_negative_stock \ +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: From 64fab5b7d133b2ab43cae2780a3a22e5aafdf5c4 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 27 Aug 2021 11:02:44 +0530 Subject: [PATCH 148/951] fix: operation time auto set to zero (#27190) * fix: operation time auto set to zero (#27188) (cherry picked from commit e6799d78efb031538dd69bb27f9f41494f81cf90) # Conflicts: # erpnext/patches.txt * fix: conflicts Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.py | 12 ++++++++---- erpnext/patches.txt | 1 + .../set_operation_time_based_on_operating_cost.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 70237f9147f..ed0874b5775 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -514,17 +514,21 @@ class BOM(WebsiteGenerator): def update_rate_and_time(self, row, update_hour_rate = False): if not row.hour_rate or update_hour_rate: hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) - row.hour_rate = (hour_rate / flt(self.conversion_rate) - if self.conversion_rate and hour_rate else hour_rate) + + if hour_rate: + row.hour_rate = (hour_rate / flt(self.conversion_rate) + if self.conversion_rate and hour_rate else hour_rate) if self.routing: - row.time_in_mins = flt(frappe.db.get_value("BOM Operation", { + time_in_mins = flt(frappe.db.get_value("BOM Operation", { "workstation": row.workstation, "operation": row.operation, - "sequence_id": row.sequence_id, "parent": self.routing }, ["time_in_mins"])) + if time_in_mins: + row.time_in_mins = time_in_mins + if row.hour_rate and row.time_in_mins: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 86bd65a82ea..8f7431a5416 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -303,3 +303,4 @@ 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.set_operation_time_based_on_operating_cost diff --git a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py new file mode 100644 index 00000000000..4acbdd63a00 --- /dev/null +++ b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py @@ -0,0 +1,15 @@ +import frappe + +def execute(): + frappe.reload_doc('manufacturing', 'doctype', 'bom') + frappe.reload_doc('manufacturing', 'doctype', 'bom_operation') + + frappe.db.sql(''' + UPDATE + `tabBOM Operation` + SET + time_in_mins = (operating_cost * 60) / hour_rate + WHERE + time_in_mins = 0 AND operating_cost > 0 + AND hour_rate > 0 AND docstatus = 1 AND parenttype = "BOM" + ''') \ No newline at end of file From 0767d2dac2817c175911390ef30457305be80e36 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 27 Aug 2021 12:59:00 +0530 Subject: [PATCH 149/951] fix: v13 migration fails due to missing reload_doc (#27192) (#27194) closes #25948 (cherry picked from commit 1eb2526d0b24a976e034661e8694db4b7af8108e) Co-authored-by: Ankush Menat --- .../patches/v12_0/add_company_link_to_einvoice_settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py index c2ed6c288fe..712eb4f61c2 100644 --- a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py +++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py @@ -3,10 +3,14 @@ import frappe def execute(): company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company or not frappe.db.count('E Invoice User'): + + if not company: return frappe.reload_doc("regional", "doctype", "e_invoice_user") + if not frappe.db.count('E Invoice User'): + return + for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']): company_name = frappe.db.sql(""" select dl.link_name from `tabAddress` a, `tabDynamic Link` dl From d88346c6cde01ee700d002bbb8a4a149ce030e2c Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Sat, 28 Aug 2021 14:53:59 +0530 Subject: [PATCH 150/951] fix: patches were breaking while migrating (#27205) * fix: patches were breaking while migrating (#27195) * fix: patches were breaking while migrating * fix: Removed duplicate function Co-authored-by: Nabin Hait (cherry picked from commit 17e0fa7a8b8c7d6471a13dc50b4556d82d6c8592) # Conflicts: # erpnext/patches.txt * fix: resolve conflicts Co-authored-by: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Co-authored-by: Ankush Menat --- erpnext/patches.txt | 2 +- .../patches/v13_0/check_is_income_tax_component.py | 4 ++-- erpnext/patches/v13_0/delete_old_purchase_reports.py | 11 +++++++++++ erpnext/patches/v13_0/delete_old_sales_reports.py | 2 ++ erpnext/patches/v13_0/rename_issue_doctype_fields.py | 3 +++ erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py | 1 + 6 files changed, 20 insertions(+), 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8f7431a5416..b284ed59538 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -217,6 +217,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings") +erpnext.patches.v12_0.create_itc_reversal_custom_fields erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v12_0.add_taxjar_integration_field @@ -274,7 +275,6 @@ erpnext.patches.v13_0.rename_discharge_date_in_ip_record erpnext.patches.v12_0.create_taxable_value_field erpnext.patches.v12_0.add_gst_category_in_delivery_note erpnext.patches.v12_0.purchase_receipt_status -erpnext.patches.v12_0.create_itc_reversal_custom_fields erpnext.patches.v13_0.fix_non_unique_represents_company erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index ebae3ad7157..7a52dc88d21 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -19,10 +19,10 @@ def execute(): ] for doctype in doctypes: - frappe.reload_doc('Payroll', 'doctype', doctype) + frappe.reload_doc('Payroll', 'doctype', doctype, force=True) - reports = ['Professional Tax Deductions', 'Provident Fund Deductions'] + reports = ['Professional Tax Deductions', 'Provident Fund Deductions', 'E-Invoice Summary'] for report in reports: frappe.reload_doc('Regional', 'Report', report) frappe.reload_doc('Regional', 'Report', report) diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index c17aad06c7f..360a82e5c08 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -13,6 +13,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) + check_linked_reports(report) frappe.delete_doc("Report", report) @@ -21,3 +22,13 @@ def delete_auto_email_reports(report): auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) + +def check_linked_reports(report): + """ Check if reports are referenced in Desktop Icon """ + icons = frappe.get_all("Desktop Icon", + fields = ['name'], + filters = { + "_report": report + }) + if icons: + frappe.delete_doc("Desktop Icon", icons) \ No newline at end of file diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index 671c012c8a0..69493e2c004 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from erpnext.patches.v13_0.delete_old_purchase_reports import check_linked_reports def execute(): reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] @@ -11,6 +12,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) + check_linked_reports(report) frappe.delete_doc("Report", report) diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index 41c51c36dcb..4885c0b7afa 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -41,6 +41,7 @@ def execute(): rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') # change fieldtype to duration + frappe.reload_doc('crm', 'doctype', 'opportunity', force=True) count = 0 for entry in opportunities: mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') @@ -58,6 +59,8 @@ def execute(): def convert_to_seconds(value, unit): seconds = 0 + if value == 0: + return seconds if unit == 'Hours': seconds = value * 3600 if unit == 'Minutes': diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py index e642547ef82..a5769d2957c 100644 --- a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py +++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py @@ -8,6 +8,7 @@ def execute(): frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') frappe.reload_doc('stock', 'doctype', 'delivery_note') frappe.reload_doc('stock', 'doctype', 'delivery_note_item') + frappe.reload_doc('stock', 'doctype', 'stock_settings') def update_from_return_docs(doctype): for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}): From f71ff830efd85f9d9be29e0c3529661fb1defbe1 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 30 Aug 2021 11:49:43 +0530 Subject: [PATCH 151/951] fix: remove non-existent method call in hooks (#27224) --- erpnext/hooks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index aede8ff2f46..2385b7cbade 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -444,7 +444,6 @@ regional_overrides = { 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', - 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' From 03dcecff67a9bb3c0bfdbe0ffea6865df194c782 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 30 Aug 2021 18:07:05 +0530 Subject: [PATCH 152/951] ci: use node action instead of apt (#27226) (#27237) * ci: use node action instead of apt (#27220) (cherry picked from commit e5e00700e5415d9abc91ef5e119649a17c347d9a) * ci: keep python version 3.6 for v13 * ci: use node v12 Co-authored-by: Ankush Menat (cherry picked from commit dc948cab3e22ccd791cfb8b19eb8cf849d36737d) --- .github/helper/install.sh | 7 +------ .github/workflows/patch.yml | 6 ++++++ .github/workflows/server-tests.yml | 6 ++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index a6a6069d358..e7f46410e6c 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -4,11 +4,7 @@ set -e cd ~ || exit -sudo apt-get install redis-server - -sudo apt install nodejs - -sudo apt install npm +sudo apt-get install redis-server libcups2-dev pip install frappe-bench @@ -32,7 +28,6 @@ wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/w tar -xf /tmp/wkhtmltox.tar.xz -C /tmp sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf sudo chmod o+x /usr/local/bin/wkhtmltopdf -sudo apt-get install libcups2-dev cd ~/frappe-bench || exit diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 0f28838d2bf..eaab24b9081 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -32,6 +32,12 @@ jobs: with: python-version: 3.6 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 12 + check-latest: true + - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 124ed7ad3e9..a008b638c3f 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -42,6 +42,12 @@ jobs: with: python-version: 3.7 + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 12 + check-latest: true + - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts From d9b9888ad5f8796445dc10fad8361c4188a5df86 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 30 Aug 2021 18:49:40 +0530 Subject: [PATCH 153/951] feat: (consistency) Add Primary Address and Contact section in Supplier (#27232) (#27233) * feat: (consistency) Add Primary Address and Contact section in Supplier - The same is present in customer and is inconsistent with supplier - Helps quickly create primary address and contact via quick entry * fix: Popup stale build and data consistency - Include `supplier_quick_entry.js` in erpnext.bundle.js - Create primary supplier address on update - Set newly created address (quick entry) in Supplier and Customer - Clear address set in supplier and customer on delete (dependency) * fix: Indentation and removed f-strings - Sider: fixed indentation in js - Dont use f-strings in queries (cherry picked from commit 3d87d9f1d323bc0d5231934f6f20569a862246b0) Co-authored-by: Marica --- erpnext/buying/doctype/supplier/supplier.js | 43 +++++++++++ erpnext/buying/doctype/supplier/supplier.json | 50 +++++++++++- erpnext/buying/doctype/supplier/supplier.py | 55 +++++++++++++ erpnext/public/build.json | 1 + .../public/js/utils/supplier_quick_entry.js | 77 +++++++++++++++++++ erpnext/selling/doctype/customer/customer.py | 20 ++++- 6 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 erpnext/public/js/utils/supplier_quick_entry.js diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 1766c2c80cc..7ee91961ca5 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -24,7 +24,26 @@ frappe.ui.form.on("Supplier", { } } }); + + frm.set_query("supplier_primary_contact", function(doc) { + return { + query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact", + filters: { + "supplier": doc.name + } + }; + }); + + frm.set_query("supplier_primary_address", function(doc) { + return { + filters: { + "link_doctype": "Supplier", + "link_name": doc.name + } + }; + }); }, + refresh: function (frm) { frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' } @@ -78,6 +97,30 @@ frappe.ui.form.on("Supplier", { }); }, + supplier_primary_address: function(frm) { + if (frm.doc.supplier_primary_address) { + frappe.call({ + method: 'frappe.contacts.doctype.address.address.get_address_display', + args: { + "address_dict": frm.doc.supplier_primary_address + }, + callback: function(r) { + frm.set_value("primary_address", r.message); + } + }); + } + if (!frm.doc.supplier_primary_address) { + frm.set_value("primary_address", ""); + } + }, + + supplier_primary_contact: function(frm) { + if (!frm.doc.supplier_primary_contact) { + frm.set_value("mobile_no", ""); + frm.set_value("email_id", ""); + } + }, + is_internal_supplier: function(frm) { if (frm.doc.is_internal_supplier == 1) { frm.toggle_reqd("represents_company", true); diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 38b8dfdf48d..c7a5db59941 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -49,6 +49,13 @@ "address_html", "column_break1", "contact_html", + "primary_address_and_contact_detail_section", + "supplier_primary_contact", + "mobile_no", + "email_id", + "column_break_44", + "supplier_primary_address", + "primary_address", "default_payable_accounts", "accounts", "default_tax_withholding_config", @@ -378,6 +385,47 @@ "fieldname": "allow_purchase_invoice_creation_without_purchase_receipt", "fieldtype": "Check", "label": "Allow Purchase Invoice Creation Without Purchase Receipt" + }, + { + "fieldname": "primary_address_and_contact_detail_section", + "fieldtype": "Section Break", + "label": "Primary Address and Contact Detail" + }, + { + "description": "Reselect, if the chosen contact is edited after save", + "fieldname": "supplier_primary_contact", + "fieldtype": "Link", + "label": "Supplier Primary Contact", + "options": "Contact" + }, + { + "fetch_from": "supplier_primary_contact.mobile_no", + "fieldname": "mobile_no", + "fieldtype": "Read Only", + "label": "Mobile No" + }, + { + "fetch_from": "supplier_primary_contact.email_id", + "fieldname": "email_id", + "fieldtype": "Read Only", + "label": "Email Id" + }, + { + "fieldname": "column_break_44", + "fieldtype": "Column Break" + }, + { + "fieldname": "primary_address", + "fieldtype": "Text", + "label": "Primary Address", + "read_only": 1 + }, + { + "description": "Reselect, if the chosen address is edited after save", + "fieldname": "supplier_primary_address", + "fieldtype": "Link", + "label": "Supplier Primary Address", + "options": "Address" } ], "icon": "fa fa-user", @@ -390,7 +438,7 @@ "link_fieldname": "supplier" } ], - "modified": "2021-05-18 15:10:11.087191", + "modified": "2021-08-27 18:02:44.314077", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index fd16b23c220..c9750caa65a 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -42,7 +42,12 @@ class Supplier(TransactionBase): if not self.naming_series: self.naming_series = '' + self.create_primary_contact() + self.create_primary_address() + def validate(self): + self.flags.is_new_doc = self.is_new() + # validation for Naming Series mandatory field... if frappe.defaults.get_global_default('supp_master_name') == 'Naming Series': if not self.naming_series: @@ -76,7 +81,39 @@ class Supplier(TransactionBase): frappe.throw(_("Internal Supplier for company {0} already exists").format( frappe.bold(self.represents_company))) + def create_primary_contact(self): + from erpnext.selling.doctype.customer.customer import make_contact + + if not self.supplier_primary_contact: + if self.mobile_no or self.email_id: + contact = make_contact(self) + self.db_set('supplier_primary_contact', contact.name) + self.db_set('mobile_no', self.mobile_no) + self.db_set('email_id', self.email_id) + + def create_primary_address(self): + from erpnext.selling.doctype.customer.customer import make_address + from frappe.contacts.doctype.address.address import get_address_display + + if self.flags.is_new_doc and self.get('address_line1'): + address = make_address(self) + address_display = get_address_display(address.name) + + self.db_set("supplier_primary_address", address.name) + self.db_set("primary_address", address_display) + def on_trash(self): + if self.supplier_primary_contact: + frappe.db.sql(""" + UPDATE `tabSupplier` + SET + supplier_primary_contact=null, + supplier_primary_address=null, + mobile_no=null, + email_id=null, + primary_address=null + WHERE name=%(name)s""", {"name": self.name}) + delete_contact_and_address('Supplier', self.name) def after_rename(self, olddn, newdn, merge=False): @@ -104,3 +141,21 @@ class Supplier(TransactionBase): doc.name, args.get('supplier_email_' + str(i))) except frappe.NameError: pass + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): + supplier = filters.get("supplier") + return frappe.db.sql(""" + SELECT + `tabContact`.name from `tabContact`, + `tabDynamic Link` + WHERE + `tabContact`.name = `tabDynamic Link`.parent + and `tabDynamic Link`.link_name = %(supplier)s + and `tabDynamic Link`.link_doctype = 'Supplier' + and `tabContact`.name like %(txt)s + """, { + 'supplier': supplier, + 'txt': '%%%s%%' % txt + }) diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 3c60e3ee500..6b70dab8037 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -38,6 +38,7 @@ "public/js/templates/item_quick_entry.html", "public/js/utils/item_quick_entry.js", "public/js/utils/customer_quick_entry.js", + "public/js/utils/supplier_quick_entry.js", "public/js/education/student_button.html", "public/js/education/assessment_result_tool.html", "public/js/hub/hub_factory.js", diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js new file mode 100644 index 00000000000..8d591a96510 --- /dev/null +++ b/erpnext/public/js/utils/supplier_quick_entry.js @@ -0,0 +1,77 @@ +frappe.provide('frappe.ui.form'); + +frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends frappe.ui.form.QuickEntryForm { + constructor(doctype, after_insert, init_callback, doc, force) { + super(doctype, after_insert, init_callback, doc, force); + this.skip_redirect_on_error = true; + } + + render_dialog() { + this.mandatory = this.mandatory.concat(this.get_variant_fields()); + super.render_dialog(); + } + + get_variant_fields() { + var variant_fields = [ + { + fieldtype: "Section Break", + label: __("Primary Contact Details"), + collapsible: 1 + }, + { + label: __("Email Id"), + fieldname: "email_id", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("Mobile Number"), + fieldname: "mobile_no", + fieldtype: "Data" + }, + { + fieldtype: "Section Break", + label: __("Primary Address Details"), + collapsible: 1 + }, + { + label: __("Address Line 1"), + fieldname: "address_line1", + fieldtype: "Data" + }, + { + label: __("Address Line 2"), + fieldname: "address_line2", + fieldtype: "Data" + }, + { + label: __("ZIP Code"), + fieldname: "pincode", + fieldtype: "Data" + }, + { + fieldtype: "Column Break" + }, + { + label: __("City"), + fieldname: "city", + fieldtype: "Data" + }, + { + label: __("State"), + fieldname: "state", + fieldtype: "Data" + }, + { + label: __("Country"), + fieldname: "country", + fieldtype: "Link", + options: "Country" + } + ]; + + return variant_fields; + } +}; diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 30809978bb9..27e9f08e8d6 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -150,8 +150,14 @@ class Customer(TransactionBase): self.db_set('email_id', self.email_id) def create_primary_address(self): + from frappe.contacts.doctype.address.address import get_address_display + if self.flags.is_new_doc and self.get('address_line1'): - make_address(self) + address = make_address(self) + address_display = get_address_display(address.name) + + self.db_set("customer_primary_address", address.name) + self.db_set("primary_address", address_display) def update_lead_status(self): '''If Customer created from Lead, update lead status to "Converted" @@ -246,9 +252,15 @@ class Customer(TransactionBase): def on_trash(self): if self.customer_primary_contact: - frappe.db.sql("""update `tabCustomer` - set customer_primary_contact=null, mobile_no=null, email_id=null - where name=%s""", self.name) + frappe.db.sql(""" + UPDATE `tabCustomer` + SET + customer_primary_contact=null, + customer_primary_address=null, + mobile_no=null, + email_id=null, + primary_address=null + WHERE name=%(name)s""", {"name": self.name}) delete_contact_and_address('Customer', self.name) if self.lead_name: From bafc9ddde40ab445fbeef94b589d3ed0c229c6b6 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 30 Aug 2021 19:04:58 +0530 Subject: [PATCH 154/951] fix: patches were breaking during migration (#27213) (#27241) * fix: patches were breaking during migration (#27200) * fix: patches were breaking during migrating * fix: patches were breaking during migration (cherry picked from commit 743375748980426b1b7fe82141ce4f0de1ecc8c6) # Conflicts: # erpnext/patches.txt * fix: resolve conflicts Co-authored-by: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com> Co-authored-by: Ankush Menat (cherry picked from commit aa040514161f68b75ee26041c710fbc6c93bddc9) --- erpnext/accounts/utils.py | 11 ++++++++ erpnext/patches.txt | 1 + .../move_item_tax_to_item_tax_template.py | 3 ++- .../v13_0/delete_old_purchase_reports.py | 13 ++-------- .../patches/v13_0/delete_old_sales_reports.py | 4 +-- .../v13_0/validate_options_for_data_field.py | 25 +++++++++++++++++++ 6 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 erpnext/patches/v13_0/validate_options_for_data_field.py diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 68355535c7e..9120602adf2 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1086,3 +1086,14 @@ def get_journal_entry(account, stock_adjustment_account, amount): db_or_cr_stock_adjustment_account : abs(amount) }] } + +def check_and_delete_linked_reports(report): + """ Check if reports are referenced in Desktop Icon """ + icons = frappe.get_all("Desktop Icon", + fields = ['name'], + filters = { + "_report": report + }) + if icons: + for icon in icons: + frappe.delete_doc("Desktop Icon", icon) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b284ed59538..aeca7df802e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -304,3 +304,4 @@ 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.set_operation_time_based_on_operating_cost +erpnext.patches.v13_0.validate_options_for_data_field diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index a6471eb53cd..5c3fa5991c9 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -91,8 +91,9 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp item_tax_template.title = make_autoname("Item Tax Template-.####") for tax_type, tax_rate in iteritems(item_tax_map): - account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1) + account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type', 'company'], as_dict=1) if account_details: + item_tax_template.company = account_details.company if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') else: diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index 360a82e5c08..57620d3e986 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): reports_to_delete = ["Requested Items To Be Ordered", @@ -13,7 +14,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) - check_linked_reports(report) + check_and_delete_linked_reports(report) frappe.delete_doc("Report", report) @@ -22,13 +23,3 @@ def delete_auto_email_reports(report): auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) - -def check_linked_reports(report): - """ Check if reports are referenced in Desktop Icon """ - icons = frappe.get_all("Desktop Icon", - fields = ['name'], - filters = { - "_report": report - }) - if icons: - frappe.delete_doc("Desktop Icon", icons) \ No newline at end of file diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index 69493e2c004..905a42c0c4c 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from erpnext.patches.v13_0.delete_old_purchase_reports import check_linked_reports +from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] @@ -12,7 +12,7 @@ def execute(): for report in reports_to_delete: if frappe.db.exists("Report", report): delete_auto_email_reports(report) - check_linked_reports(report) + check_and_delete_linked_reports(report) frappe.delete_doc("Report", report) diff --git a/erpnext/patches/v13_0/validate_options_for_data_field.py b/erpnext/patches/v13_0/validate_options_for_data_field.py new file mode 100644 index 00000000000..568d1a4b0cb --- /dev/null +++ b/erpnext/patches/v13_0/validate_options_for_data_field.py @@ -0,0 +1,25 @@ +# Copyright (c) 2021, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model import data_field_options + +def execute(): + + for field in frappe.get_all('Custom Field', + fields = ['name'], + filters = { + 'fieldtype': 'Data', + 'options': ['!=', None] + }): + + if field not in data_field_options: + frappe.db.sql(""" + UPDATE + `tabCustom Field` + SET + options=NULL + WHERE + name=%s + """, (field)) From f20913fb690c40221ffd16b6b3a29e1609788425 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 31 Aug 2021 11:08:58 +0530 Subject: [PATCH 155/951] fix: Correct company address not getting copied from Purchase Order to Invoice (#27217) (#27234) * fix: Correct company adderess not getting copied from Purchase Order to Invoice * fix: Linting issues (cherry picked from commit fd467e6d326fc6abb7820efc458e6106c38e307f) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/public/js/controllers/transaction.js | 26 +++++++++++--------- erpnext/public/js/utils/party.js | 4 +-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 16613b365f1..0e99b43befa 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -846,21 +846,25 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { - erpnext.utils.get_shipping_address(this.frm, function(){ + erpnext.utils.get_shipping_address(this.frm, function() { set_party_account(set_pricing); }); // Get default company billing address in Purchase Invoice, Order and Receipt - frappe.call({ - 'method': 'frappe.contacts.doctype.address.address.get_default_address', - 'args': { - 'doctype': 'Company', - 'name': this.frm.doc.company - }, - 'callback': function(r) { - me.frm.set_value('billing_address', r.message); - } - }); + if (this.frm.doc.company && frappe.meta.get_docfield(this.frm.doctype, "billing_address")) { + frappe.call({ + method: "erpnext.setup.doctype.company.company.get_default_company_address", + args: {name: this.frm.doc.company, existing_address: this.frm.doc.billing_address || ""}, + debounce: 2000, + callback: function(r) { + if (r.message) { + me.frm.set_value("billing_address", r.message); + } else { + me.frm.set_value("company_address", ""); + } + } + }); + } } else { set_party_account(set_pricing); diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 4d432e3d5cc..a492b32a9f6 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -289,8 +289,8 @@ erpnext.utils.get_shipping_address = function(frm, callback) { company: frm.doc.company, address: frm.doc.shipping_address }, - callback: function(r){ - if (r.message){ + callback: function(r) { + if (r.message) { frm.set_value("shipping_address", r.message[0]) //Address title or name frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page } From 0c4f29edcfeacb3d94efe0a64b90872d914c9180 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 31 Aug 2021 18:53:30 +0530 Subject: [PATCH 156/951] fix(minor): Incorrect unallocated amount on type receive (#27262) (#27263) (cherry picked from commit c37cec9b9d0583d470b92cb8307881e0deb0d185) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index cc8ab453fd9..727ef55b3c7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -872,7 +872,7 @@ frappe.ui.form.on('Payment Entry', { && frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) { unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges - + frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; + - frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; } else if (frm.doc.payment_type == "Pay" && frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions && frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) { From 155df936cdfd44668824e26a56798a1badd49869 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 31 Aug 2021 19:13:04 +0530 Subject: [PATCH 157/951] Revert "fix: add child item groups into the filters (#26997)" (#27266) (#27268) This reverts commit c60d5523bca0a0631555a6234a485cd7a1e3c245. (cherry picked from commit 763450dcf867c31ad954ac5b45ed76e5379f28bf) Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- .../item_group_wise_sales_target_variance.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 24ca666f6b1..89cfa16abe0 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -44,18 +44,6 @@ def get_data(filters, period_list, partner_doctype): if d.item_group not in item_groups: item_groups.append(d.item_group) - if item_groups: - child_items = [] - for item_group in item_groups: - if frappe.db.get_value("Item Group", {"name":item_group}, "is_group"): - for child_item_group in frappe.get_all("Item Group", {"parent_item_group":item_group}): - if child_item_group['name'] not in child_items: - child_items.append(child_item_group['name']) - - for item in child_items: - if item not in item_groups: - item_groups.append(item) - date_field = ("transaction_date" if filters.get('doctype') == "Sales Order" else "posting_date") From d641dd68d4536b6389021a5f7ffcfa084c8dfbb5 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 31 Aug 2021 19:46:13 +0530 Subject: [PATCH 158/951] fix: revert "refactor: simplify initialize_previous_data" (#27270) (#27271) This reverts commit 2f5624e588541103adb4a3170f2886590dcee42e. (cherry picked from commit c1d986a0c62c8673e897e9a83d2b2dc3fa760606) Co-authored-by: Ankush Menat --- erpnext/stock/stock_ledger.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index afd3ab2b5ae..e98df737cb7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -271,13 +271,15 @@ class update_entries_after(object): } """ + self.data.setdefault(args.warehouse, frappe._dict()) + warehouse_dict = self.data[args.warehouse] previous_sle = get_previous_sle_of_current_voucher(args) + warehouse_dict.previous_sle = previous_sle - self.data[args.warehouse] = frappe._dict({ - "previous_sle": previous_sle, - "qty_after_transaction": flt(previous_sle.qty_after_transaction), - "valuation_rate": flt(previous_sle.valuation_rate), - "stock_value": flt(previous_sle.stock_value), + for key in ("qty_after_transaction", "valuation_rate", "stock_value"): + setattr(warehouse_dict, key, flt(previous_sle.get(key))) + + warehouse_dict.update({ "prev_stock_value": previous_sle.stock_value or 0.0, "stock_queue": json.loads(previous_sle.stock_queue or "[]"), "stock_value_difference": 0.0 From c31bf155f04e6f43622bcaaaa405e82f6f6bbe29 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 31 Aug 2021 21:18:22 +0530 Subject: [PATCH 159/951] fix: Healthcare Service Unit fixes (#27273) (#27274) * fix: validate service unit setup against practitioner schedule * fix: service unit properties getting overwritten (cherry picked from commit ef76f62bc19c41b0a3b7fe70dfab45bd3b8a620d) Co-authored-by: Rucha Mahabal --- .../healthcare_service_unit.py | 2 +- .../patient_appointment.py | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py index 989d4267897..5e76ed7284f 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.py @@ -30,7 +30,7 @@ class HealthcareServiceUnit(NestedSet): self.validate_one_root() def set_service_unit_properties(self): - if self.is_group: + if cint(self.is_group): self.allow_appointments = False self.overlap_appointments = False self.inpatient_occupancy = False diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 36047c48381..f0d5af93416 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document import json -from frappe.utils import getdate, get_time, flt +from frappe.utils import getdate, get_time, flt, get_link_to_form from frappe.model.mapper import get_mapped_doc from frappe import _ import datetime @@ -333,17 +333,13 @@ def check_employee_wise_availability(date, practitioner_doc): def get_available_slots(practitioner_doc, date): - available_slots = [] - slot_details = [] + available_slots = slot_details = [] weekday = date.strftime('%A') practitioner = practitioner_doc.name for schedule_entry in practitioner_doc.practitioner_schedules: - if schedule_entry.schedule: - practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule) - else: - frappe.throw(_('{0} does not have a Healthcare Practitioner Schedule. Add it in Healthcare Practitioner').format( - frappe.bold(practitioner)), title=_('Practitioner Schedule Not Found')) + validate_practitioner_schedules(schedule_entry, practitioner) + practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule) if practitioner_schedule: available_slots = [] @@ -386,6 +382,19 @@ def get_available_slots(practitioner_doc, date): return slot_details +def validate_practitioner_schedules(schedule_entry, practitioner): + if schedule_entry.schedule: + if not schedule_entry.service_unit: + frappe.throw(_('Practitioner {0} does not have a Service Unit set against the Practitioner Schedule {1}.').format( + get_link_to_form('Healthcare Practitioner', practitioner), frappe.bold(schedule_entry.schedule)), + title=_('Service Unit Not Found')) + + else: + frappe.throw(_('Practitioner {0} does not have a Practitioner Schedule assigned.').format( + get_link_to_form('Healthcare Practitioner', practitioner)), + title=_('Practitioner Schedule Not Found')) + + @frappe.whitelist() def update_status(appointment_id, status): frappe.db.set_value('Patient Appointment', appointment_id, 'status', status) From 68482b223f806dab93052c1a6a98628f6288a8dc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 1 Sep 2021 22:21:10 +0530 Subject: [PATCH 160/951] chore: change log for v13.10.0 --- erpnext/change_log/v13/v13_10_0.md | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 erpnext/change_log/v13/v13_10_0.md diff --git a/erpnext/change_log/v13/v13_10_0.md b/erpnext/change_log/v13/v13_10_0.md new file mode 100644 index 00000000000..ee844e5526a --- /dev/null +++ b/erpnext/change_log/v13/v13_10_0.md @@ -0,0 +1,58 @@ +# Version 13.10.0 Release Notes + +### Features & Enhancements +- POS invoice coupon code feature ([#27004](https://github.com/frappe/erpnext/pull/27004)) +- Add Primary Address and Contact section in Supplier ([#27197](https://github.com/frappe/erpnext/pull/27197)) +- Capacity for Service Unit, concurrent appointments based on Capacity, Patient enhancements ([#24860](https://github.com/frappe/erpnext/pull/24860)) +- Increase number of supported currency exchanges ([#26763](https://github.com/frappe/erpnext/pull/26763)) +- South Africa VAT Audit Report ([#27017](https://github.com/frappe/erpnext/pull/27017)) +- Training Event Status Update and Validations ([#26698](https://github.com/frappe/erpnext/pull/26698)) +- Allow draft POS Invoices even if no stock available ([#27106](https://github.com/frappe/erpnext/pull/27106)) +- Column for total amount due in Accounts Receivable/Payable Summary ([#27069](https://github.com/frappe/erpnext/pull/27069)) +- Provision to create customer from opportunity ([#27141](https://github.com/frappe/erpnext/pull/27141)) +- Employee reminders ([#25735](https://github.com/frappe/erpnext/pull/25735)) +- Fetching details from supplier/customer groups ([#26131](https://github.com/frappe/erpnext/pull/26131)) +- Unreconcile on cancellation of bank transaction ([#27109](https://github.com/frappe/erpnext/pull/27109)) + +### Fixes +- Healthcare Redesign Changes ([#27100](https://github.com/frappe/erpnext/pull/27100)) +- Eway bill version changed to 1.0.0421 ([#27044](https://github.com/frappe/erpnext/pull/27044)) +- Org Chart fixes ([#26952](https://github.com/frappe/erpnext/pull/26952)) +- TDS calculation on net total ([#27058](https://github.com/frappe/erpnext/pull/27058)) +- Dimension filter query fix to avoid including disabled dimensions ([#26988](https://github.com/frappe/erpnext/pull/26988)) +- Various minor perf fixes for ledger postings ([#26775](https://github.com/frappe/erpnext/pull/26775)) +- Healthcare Service Unit fixes ([#27273](https://github.com/frappe/erpnext/pull/27273)) +- Selected batch no changed on changing of qty ([#27126](https://github.com/frappe/erpnext/pull/27126)) +- Changed label to "Inpatient Visit Charge" in appointment type ([#26906](https://github.com/frappe/erpnext/pull/26906)) +- Stock Analytics Report must consider warehouse during calculation ([#26908](https://github.com/frappe/erpnext/pull/26908)) +- Reduce Sales Invoice row size ([#27136](https://github.com/frappe/erpnext/pull/27136)) +- Allow backdated discharge for inpatient ([#25124](https://github.com/frappe/erpnext/pull/25124)) +- Sequence of sub-operations in job card ([#27138](https://github.com/frappe/erpnext/pull/27138)) +- Social media post fixes ([#24664](https://github.com/frappe/erpnext/pull/24664)) +- Consolidated balance sheet showing incorrect values ([#26975](https://github.com/frappe/erpnext/pull/26975)) +- Correct company address not getting copied from Purchase Order to Invoice ([#27217](https://github.com/frappe/erpnext/pull/27217)) +- Add child item groups into the filters ([#26997](https://github.com/frappe/erpnext/pull/26997)) +- Pass planned start date to in work order from production plan ([#27031](https://github.com/frappe/erpnext/pull/27031)) +- Filtering of items in Sales and Purchase Orders ([#26936](https://github.com/frappe/erpnext/pull/26936)) +- Sales order qty update fails in "Update Items" button ([#26992](https://github.com/frappe/erpnext/pull/26992)) +- Refactor stock module onboarding ([#25745](https://github.com/frappe/erpnext/pull/25745)) +- Calculation of gross profit percentage in Gross Profit Report ([#27045](https://github.com/frappe/erpnext/pull/27045)) +- Correct price list rate field value in return Sales Invoice ([#27105](https://github.com/frappe/erpnext/pull/27105)) +- Return Qty in PR/DN for legacy data ([#27003](https://github.com/frappe/erpnext/pull/27003)) +- Sales pipeline graph issue ([#26626](https://github.com/frappe/erpnext/pull/26626)) +- Additional salary processing ([#27005](https://github.com/frappe/erpnext/pull/27005)) +- Dimension filter query fix to avoid including disabled dimensions ([#27006](https://github.com/frappe/erpnext/pull/27006)) +- Incorrect Gl Entry on period closing involving finance books ([#27104](https://github.com/frappe/erpnext/pull/26921)) +- Set production plan to completed even on over production ([#27032](https://github.com/frappe/erpnext/pull/27032)) +- Budget variance missing values ([#26963](https://github.com/frappe/erpnext/pull/26963)) +- No able to create asset depreciation entry when cost_center is mandatory ([#26912](https://github.com/frappe/erpnext/pull/26912)) +- Keep stock entry title & purpose in sync ([#27043](https://github.com/frappe/erpnext/pull/27043)) +- Add mandatory depends on condition for export type field ([#26957](https://github.com/frappe/erpnext/pull/26957)) +- Fixed patched which were breaking while migrating ([#27205](https://github.com/frappe/erpnext/pull/27205)) +- ZeroDivisionError on creating e-invoice for credit note ([#26919](https://github.com/frappe/erpnext/pull/26919)) +- Stock analytics report date range issues and add company filter ([#27014](https://github.com/frappe/erpnext/pull/27014)) +- Stock Ledger report not working if include uom selected in filter ([#27127](https://github.com/frappe/erpnext/pull/27127)) +- Show proper currency symbol in Taxes and Charges table ([#26935](https://github.com/frappe/erpnext/pull/26935)) +- Operation time auto set to zero ([#27190](https://github.com/frappe/erpnext/pull/27190)) +- Set account for change amount even if pos profile not found ([#26986](https://github.com/frappe/erpnext/pull/26986)) +- Discard empty rows from update items ([#27021](https://github.com/frappe/erpnext/pull/27021)) \ No newline at end of file From 702eea3b543347505669333502011210332c7c1b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 1 Sep 2021 22:53:11 +0550 Subject: [PATCH 161/951] bumped to version 13.10.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index fa038cebc3a..89e4b4a677b 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.9.2' +__version__ = '13.10.0' def get_default_company(user=None): '''Get default company for user''' From 96aee284d2c2ddbc27959fb85b6ccb62719523b1 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 3 Sep 2021 12:29:27 +0530 Subject: [PATCH 162/951] fix: south africa vat patch failure (#27324) * fix: south africa vat patch failure (#27323) reload doc is necessary on new doctypes (cherry picked from commit d1fe060e4afb96324492beca77c69698cb32a085) # Conflicts: # erpnext/patches/v13_0/add_custom_field_for_south_africa.py * fix: resolve conflicts Co-authored-by: Ankush Menat --- erpnext/patches/v13_0/add_custom_field_for_south_africa.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py index 73ff1cad5b6..5bfa823b4a1 100644 --- a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py +++ b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py @@ -1,8 +1,8 @@ # Copyright (c) 2020, Frappe and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe + from erpnext.regional.south_africa.setup import make_custom_fields, add_permissions def execute(): @@ -10,5 +10,8 @@ def execute(): if not company: return + frappe.reload_doc('regional', 'doctype', 'south_africa_vat_settings') + frappe.reload_doc('accounts', 'doctype', 'south_africa_vat_account') + make_custom_fields() add_permissions() From 2565b1fb33fea8aa10516fad357ddf93fc12d9b6 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 6 Sep 2021 13:51:27 +0530 Subject: [PATCH 163/951] fix: patch failure for vat audit report (#27355) (#27356) (cherry picked from commit 14b01619dee91ff41381d180bc907ad18744e15a) Co-authored-by: Ankush Menat --- erpnext/patches/v13_0/add_custom_field_for_south_africa.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py index 5bfa823b4a1..32566b8d21d 100644 --- a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py +++ b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py @@ -11,6 +11,7 @@ def execute(): return frappe.reload_doc('regional', 'doctype', 'south_africa_vat_settings') + frappe.reload_doc('regional', 'report', 'vat_audit_report') frappe.reload_doc('accounts', 'doctype', 'south_africa_vat_account') make_custom_fields() From adb07ebe093b23e65e61291d0c234fe497c31bfe Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 6 Sep 2021 23:53:19 +0550 Subject: [PATCH 164/951] bumped to version 13.10.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 89e4b4a677b..4a12e657765 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.10.0' +__version__ = '13.10.1' def get_default_company(user=None): '''Get default company for user''' From ede188d138ae6ec0408e78b3a1d17c3e2122efb1 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 7 Sep 2021 13:00:40 +0530 Subject: [PATCH 165/951] fix: missed to add voucher_type, voucher_no to get GL Entries (#27377) * fix: missed to add voucher_type, voucher_no to get GL Entries (#27368) * fix: missed to add voucher_type, voucher_no to get gl entries * test: get voucherwise details utilities # Conflicts: # erpnext/accounts/test/test_utils.py * fix: resolve conflicts Co-authored-by: rohitwaghchaure Co-authored-by: Ankush Menat (cherry picked from commit 058d98342adcef0438a6fdad061af19dfe1fe702) --- erpnext/accounts/test/test_utils.py | 76 +++++++++++++++++------------ erpnext/accounts/utils.py | 5 +- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 628c8ce6463..c3f6d274437 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -1,22 +1,52 @@ from __future__ import unicode_literals + import unittest -from erpnext.accounts.party import get_party_shipping_address + from frappe.test_runner import make_test_objects +from erpnext.accounts.party import get_party_shipping_address +from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + class TestUtils(unittest.TestCase): @classmethod def setUpClass(cls): super(TestUtils, cls).setUpClass() - make_test_objects('Address', ADDRESS_RECORDS) + make_test_objects("Address", ADDRESS_RECORDS) def test_get_party_shipping_address(self): - address = get_party_shipping_address('Customer', '_Test Customer 1') - self.assertEqual(address, '_Test Billing Address 2 Title-Billing') + address = get_party_shipping_address("Customer", "_Test Customer 1") + self.assertEqual(address, "_Test Billing Address 2 Title-Billing") def test_get_party_shipping_address2(self): - address = get_party_shipping_address('Customer', '_Test Customer 2') - self.assertEqual(address, '_Test Shipping Address 2 Title-Shipping') + address = get_party_shipping_address("Customer", "_Test Customer 2") + self.assertEqual(address, "_Test Shipping Address 2 Title-Shipping") + + def test_get_voucher_wise_gl_entry(self): + + pr = make_purchase_receipt( + item_code="_Test Item", + posting_date="2021-02-01", + rate=100, + qty=1, + warehouse="Stores - TCP1", + company="_Test Company with perpetual inventory", + ) + + future_vouchers = get_future_stock_vouchers("2021-01-01", "00:00:00", for_items=["_Test Item"]) + + voucher_type_and_no = ("Purchase Receipt", pr.name) + self.assertTrue( + voucher_type_and_no in future_vouchers, + msg="get_future_stock_vouchers not returning correct value", + ) + + posting_date = "2021-01-01" + gl_entries = get_voucherwise_gl_entries(future_vouchers, posting_date) + self.assertTrue( + voucher_type_and_no in gl_entries, msg="get_voucherwise_gl_entries not returning expected GLes", + ) ADDRESS_RECORDS = [ @@ -28,12 +58,8 @@ ADDRESS_RECORDS = [ "city": "Lagos", "country": "Nigeria", "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer 2", - "doctype": "Dynamic Link" - } - ] + {"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"} + ], }, { "doctype": "Address", @@ -43,12 +69,8 @@ ADDRESS_RECORDS = [ "city": "Lagos", "country": "Nigeria", "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer 2", - "doctype": "Dynamic Link" - } - ] + {"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"} + ], }, { "doctype": "Address", @@ -59,12 +81,8 @@ ADDRESS_RECORDS = [ "country": "Nigeria", "is_shipping_address": "1", "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer 2", - "doctype": "Dynamic Link" - } - ] + {"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"} + ], }, { "doctype": "Address", @@ -75,11 +93,7 @@ ADDRESS_RECORDS = [ "country": "Nigeria", "is_shipping_address": "1", "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer 1", - "doctype": "Dynamic Link" - } - ] - } + {"link_doctype": "Customer", "link_name": "_Test Customer 1", "doctype": "Dynamic Link"} + ], + }, ] diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9120602adf2..44623634baf 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -960,6 +960,9 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): Only fetches GLE fields required for comparing with new GLE. Check compare_existing_and_expected_gle function below. + + returns: + Dict[Tuple[voucher_type, voucher_no], List[GL Entries]] """ gl_entries = {} if not future_stock_vouchers: @@ -968,7 +971,7 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): voucher_nos = [d[1] for d in future_stock_vouchers] gles = frappe.db.sql(""" - select name, account, credit, debit, cost_center, project + select name, account, credit, debit, cost_center, project, voucher_type, voucher_no from `tabGL Entry` where posting_date >= %s and voucher_no in (%s)""" % From 4f3e2240b8c9e89fd1c1f78b9e7931de6dff3555 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 7 Sep 2021 13:25:13 +0550 Subject: [PATCH 166/951] bumped to version 13.10.2 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 4a12e657765..250ac44d487 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '13.10.1' +__version__ = '13.10.2' def get_default_company(user=None): '''Get default company for user''' From 135e3b0f092b1670efbc1a68e750f25d857b1a57 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 10 Sep 2021 13:00:30 +0530 Subject: [PATCH 167/951] fix: sider issues --- erpnext/accounts/custom/address.py | 5 +++- .../v13_0/delete_old_purchase_reports.py | 3 --- .../patches/v13_0/delete_old_sales_reports.py | 3 --- .../projects/doctype/timesheet/timesheet.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 24 ------------------- 5 files changed, 5 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py index a6d08d8ff61..a28402eaef4 100644 --- a/erpnext/accounts/custom/address.py +++ b/erpnext/accounts/custom/address.py @@ -31,7 +31,10 @@ class ERPNextAddress(Address): After Address is updated, update the related 'Primary Address' on Customer. """ address_display = get_address_display(self.as_dict()) - filters = { "customer_primary_address": self.name } + filters = { + "customer_primary_address": self.name + } + customers = frappe.db.get_all("Customer", filters=filters, as_list=True) for customer_name in customers: frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display) diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index 86e55ef782f..57620d3e986 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -6,9 +6,6 @@ from __future__ import unicode_literals import frappe from erpnext.accounts.utils import check_and_delete_linked_reports -from erpnext.accounts.utils import check_and_delete_linked_reports - - def execute(): reports_to_delete = ["Requested Items To Be Ordered", "Purchase Order Items To Be Received or Billed","Purchase Order Items To Be Received", diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index 98d635980ae..905a42c0c4c 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -6,9 +6,6 @@ from __future__ import unicode_literals import frappe from erpnext.accounts.utils import check_and_delete_linked_reports -from erpnext.accounts.utils import check_and_delete_linked_reports - - def execute(): reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 9155f45184f..a20c70a6dac 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -235,7 +235,7 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to tsd.activity_type as activity_type, tsd.description as description, ts.currency as currency, - tsd.project_name as project_name + tsd.project_name as project_name FROM `tabTimesheet Detail` tsd diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 87bef798a7c..df98b681106 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1595,30 +1595,6 @@ class StockEntry(StockController): if material_request and material_request not in material_requests: material_requests.append(material_request) frappe.db.set_value('Material Request', material_request, 'transfer_status', status) - - def update_items_for_process_loss(self): - process_loss_dict = {} - for d in self.get("items"): - if not d.is_process_loss: - continue - - scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse") - if scrap_warehouse is not None: - d.t_warehouse = scrap_warehouse - d.is_scrap_item = 0 - - if d.item_code not in process_loss_dict: - process_loss_dict[d.item_code] = [flt(0), flt(0)] - process_loss_dict[d.item_code][0] += flt(d.transfer_qty) - process_loss_dict[d.item_code][1] += flt(d.qty) - - for d in self.get("items"): - if not d.is_finished_item or d.item_code not in process_loss_dict: - continue - # Assumption: 1 finished item has 1 row. - d.transfer_qty -= process_loss_dict[d.item_code][0] - d.qty -= process_loss_dict[d.item_code][1] - def update_items_for_process_loss(self): process_loss_dict = {} From 71e4230ab0b32ea2c27c67661e95dd7c1f120bc9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 13 Sep 2021 10:31:00 +0530 Subject: [PATCH 168/951] chore: whitespace/imports --- erpnext/accounts/doctype/party_link/party_link.py | 1 + erpnext/accounts/doctype/party_link/test_party_link.py | 1 + erpnext/patches/v13_0/delete_old_purchase_reports.py | 2 ++ erpnext/patches/v13_0/delete_old_sales_reports.py | 2 ++ .../patches/v13_0/set_operation_time_based_on_operating_cost.py | 1 + erpnext/regional/report/vat_audit_report/vat_audit_report.py | 1 + 6 files changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py index a5421ff8c8d..fe9d8c6592c 100644 --- a/erpnext/accounts/doctype/party_link/party_link.py +++ b/erpnext/accounts/doctype/party_link/party_link.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document + class PartyLink(Document): def validate(self): if self.primary_role not in ['Customer', 'Supplier']: diff --git a/erpnext/accounts/doctype/party_link/test_party_link.py b/erpnext/accounts/doctype/party_link/test_party_link.py index a3ea3959ba4..2ae338133e0 100644 --- a/erpnext/accounts/doctype/party_link/test_party_link.py +++ b/erpnext/accounts/doctype/party_link/test_party_link.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestPartyLink(unittest.TestCase): pass diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index 57620d3e986..3cb7e120d67 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -4,8 +4,10 @@ from __future__ import unicode_literals import frappe + from erpnext.accounts.utils import check_and_delete_linked_reports + def execute(): reports_to_delete = ["Requested Items To Be Ordered", "Purchase Order Items To Be Received or Billed","Purchase Order Items To Be Received", diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index 905a42c0c4c..c9a366655ce 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -4,8 +4,10 @@ from __future__ import unicode_literals import frappe + from erpnext.accounts.utils import check_and_delete_linked_reports + def execute(): reports_to_delete = ["Ordered Items To Be Delivered", "Ordered Items To Be Billed"] diff --git a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py index 4acbdd63a00..0366d4902dc 100644 --- a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py +++ b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py @@ -1,5 +1,6 @@ import frappe + def execute(): frappe.reload_doc('manufacturing', 'doctype', 'bom') frappe.reload_doc('manufacturing', 'doctype', 'bom_operation') diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 4514bb79de5..3637bcaf439 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -9,6 +9,7 @@ import frappe from frappe import _ from frappe.utils import formatdate, get_link_to_form + def execute(filters=None): return VATAuditReport(filters).run() From e39db1abe3b2eec762f45e64b8b863170a59fc6e Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 13 Sep 2021 10:33:28 +0530 Subject: [PATCH 169/951] revert: "fix: Salary component account filter (#26605)" (#27446) (#27447) This reverts commit aaea5edbdb2d14ce3599e9e5d47048cf032e3f6c. (cherry picked from commit 5c1f0c98f8a57e854f9aa2302b507ffdbb30beaa) Co-authored-by: Ankush Menat --- .../doctype/salary_component/salary_component.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index e9e6f81862c..dbf75140ac1 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -4,18 +4,11 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { frm.set_query("account", "accounts", function(doc, cdt, cdn) { - let d = frappe.get_doc(cdt, cdn); - - let root_type = "Liability"; - if (frm.doc.type == "Deduction") { - root_type = "Expense"; - } - + var d = locals[cdt][cdn]; return { filters: { "is_group": 0, - "company": d.company, - "root_type": root_type + "company": d.company } }; }); From 72d1cf0537cc299b9fcc5a8185c57349565bc875 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 13 Sep 2021 14:01:39 +0530 Subject: [PATCH 170/951] feat: (get_items_from) filter material request item in purchase order (#27452) --- .../buying/doctype/purchase_order/purchase_order.js | 5 ++++- erpnext/public/js/utils.js | 5 ++++- .../doctype/material_request/material_request.py | 13 +++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 233a9c87e59..6e943c2832d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -425,7 +425,10 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( status: ["!=", "Stopped"], per_ordered: ["<", 100], company: me.frm.doc.company - } + }, + allow_child_item_selection: true, + child_fielname: "items", + child_columns: ["item_code", "qty"] }) }, __("Get Items From")); diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index fdf4e35e6d4..ee8a516a148 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -709,6 +709,9 @@ erpnext.utils.map_current_doc = function(opts) { setters: opts.setters, get_query: opts.get_query, add_filters_group: 1, + allow_child_item_selection: opts.allow_child_item_selection, + child_fieldname: opts.child_fielname, + child_columns: opts.child_columns, action: function(selections, args) { let values = selections; if(values.length === 0){ @@ -716,7 +719,7 @@ erpnext.utils.map_current_doc = function(opts) { return; } opts.source_name = values; - opts.setters = args; + opts.args = args; d.dialog.hide(); _map(); }, diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 9eb47216266..2569c04251c 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -6,10 +6,13 @@ from __future__ import unicode_literals +import json + import frappe from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc from frappe.utils import cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate +from six import string_types from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController @@ -269,7 +272,10 @@ def update_status(name, status): material_request.update_status(status) @frappe.whitelist() -def make_purchase_order(source_name, target_doc=None): +def make_purchase_order(source_name, target_doc=None, args={}): + + if isinstance(args, string_types): + args = json.loads(args) def postprocess(source, target_doc): if frappe.flags.args and frappe.flags.args.default_supplier: @@ -284,7 +290,10 @@ def make_purchase_order(source_name, target_doc=None): set_missing_values(source, target_doc) def select_item(d): - return d.ordered_qty < d.stock_qty + filtered_items = args.get('filtered_children', []) + child_filter = d.name in filtered_items if filtered_items else True + + return d.ordered_qty < d.stock_qty and child_filter doclist = get_mapped_doc("Material Request", source_name, { "Material Request": { From dc5f7a0c09fa3375a4ceb8d5a0d74dbac3a4ca41 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 13 Sep 2021 15:14:28 +0530 Subject: [PATCH 171/951] fix(Payroll): incorrect component amount calculation if dependent on another payment days based component (#27454) * fix(Payroll): incorrect component amount calculation if dependent on another payment days based component (#27349) * fix(Payroll): incorrect component amount calculation if dependent on another payment days based component * fix: set component amount precision at the end * fix: consider default amount during taxt calculations * test: component amount dependent on another payment days based component * fix: test (cherry picked from commit bab644a249de4355d6700f53a7bfbf0114ebb30c) # Conflicts: # erpnext/payroll/doctype/salary_slip/test_salary_slip.py * fix: conflicts in test file Co-authored-by: Rucha Mahabal --- .../doctype/salary_slip/salary_slip.py | 33 ++-- .../doctype/salary_slip/test_salary_slip.py | 152 ++++++++++++++++++ 2 files changed, 171 insertions(+), 14 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 695aafffb52..888150f0ae3 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -487,7 +487,7 @@ class SalarySlip(TransactionBase): self.calculate_component_amounts("deductions") self.set_loan_repayment() - self.set_component_amounts_based_on_payment_days() + self.set_precision_for_component_amounts() self.set_net_pay() def set_net_pay(self): @@ -713,6 +713,17 @@ class SalarySlip(TransactionBase): component_row.amount = amount + self.update_component_amount_based_on_payment_days(component_row) + + def update_component_amount_based_on_payment_days(self, component_row): + joining_date, relieving_date = self.get_joining_and_relieving_dates() + component_row.amount = self.get_amount_based_on_payment_days(component_row, joining_date, relieving_date)[0] + + def set_precision_for_component_amounts(self): + for component_type in ("earnings", "deductions"): + for component_row in self.get(component_type): + component_row.amount = flt(component_row.amount, component_row.precision("amount")) + def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period): if not payroll_period: frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}.") @@ -870,14 +881,7 @@ class SalarySlip(TransactionBase): return total_tax_paid def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0): - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) - - if not relieving_date: - relieving_date = getdate(self.end_date) - - if not joining_date: - frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) + joining_date, relieving_date = self.get_joining_and_relieving_dates() taxable_earnings = 0 additional_income = 0 @@ -888,7 +892,10 @@ class SalarySlip(TransactionBase): if based_on_payment_days: amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date) else: - amount, additional_amount = earning.amount, earning.additional_amount + if earning.additional_amount: + amount, additional_amount = earning.amount, earning.additional_amount + else: + amount, additional_amount = earning.default_amount, earning.additional_amount if earning.is_tax_applicable: if additional_amount: @@ -1059,7 +1066,7 @@ class SalarySlip(TransactionBase): total += amount return total - def set_component_amounts_based_on_payment_days(self): + def get_joining_and_relieving_dates(self): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) @@ -1069,9 +1076,7 @@ class SalarySlip(TransactionBase): if not joining_date: frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) - for component_type in ("earnings", "deductions"): - for d in self.get(component_type): - d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount")) + return joining_date, relieving_date def set_loan_repayment(self): self.total_loan_repayment = 0 diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 07b84b27a33..81582cecae3 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -17,6 +17,7 @@ from frappe.utils import ( getdate, nowdate, ) +from frappe.utils.make_random import get_random import erpnext from erpnext.accounts.utils import get_fiscal_year @@ -134,6 +135,65 @@ class TestSalarySlip(unittest.TestCase): frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_component_amount_dependent_on_another_payment_days_based_component(self): + from erpnext.hr.doctype.attendance.attendance import mark_attendance + from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( + create_salary_structure_assignment, + ) + + no_of_days = self.get_no_of_days() + # Payroll based on attendance + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance") + + salary_structure = make_salary_structure_for_payment_days_based_component_dependency() + employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company") + + # base = 50000 + create_salary_structure_assignment(employee, salary_structure.name, company="_Test Company", currency="INR") + + # mark employee absent for a day since this case works fine if payment days are equal to working days + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + mark_attendance(employee, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + + # make salary slip and assert payment days + ss = make_salary_slip_for_payment_days_dependency_test("test_payment_days_based_component@salary.com", salary_structure.name) + self.assertEqual(ss.absent_days, 1) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1) + + ss.reload() + payment_days_based_comp_amount = 0 + for component in ss.earnings: + if component.salary_component == "HRA - Payment Days": + payment_days_based_comp_amount = flt(component.amount, component.precision("amount")) + break + + # check if the dependent component is calculated using the amount updated after payment days + actual_amount = 0 + precision = 0 + for component in ss.deductions: + if component.salary_component == "P - Employee Provident Fund": + precision = component.precision("amount") + actual_amount = flt(component.amount, precision) + break + + expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision) + + self.assertEqual(actual_amount, expected_amount) + frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave") + def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) @@ -851,6 +911,7 @@ def setup_test(): def make_holiday_list(): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) + holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List") if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): holiday_list = frappe.get_doc({ "doctype": "Holiday List", @@ -861,3 +922,94 @@ def make_holiday_list(): }).insert() holiday_list.get_weekly_off_dates() holiday_list.save() + holiday_list = holiday_list.name + + return holiday_list + +def make_salary_structure_for_payment_days_based_component_dependency(): + earnings = [ + { + "salary_component": "Basic Salary - Payment Days", + "abbr": "P_BS", + "type": "Earning", + "formula": "base", + "amount_based_on_formula": 1 + }, + { + "salary_component": "HRA - Payment Days", + "abbr": "P_HRA", + "type": "Earning", + "depends_on_payment_days": 1, + "amount_based_on_formula": 1, + "formula": "base * 0.20" + } + ] + + make_salary_component(earnings, False, company_list=["_Test Company"]) + + deductions = [ + { + "salary_component": "P - Professional Tax", + "abbr": "P_PT", + "type": "Deduction", + "depends_on_payment_days": 1, + "amount": 200.00 + }, + { + "salary_component": "P - Employee Provident Fund", + "abbr": "P_EPF", + "type": "Deduction", + "exempted_from_income_tax": 1, + "amount_based_on_formula": 1, + "depends_on_payment_days": 0, + "formula": "(gross_pay - P_HRA) * 0.12" + } + ] + + make_salary_component(deductions, False, company_list=["_Test Company"]) + + salary_structure = "Salary Structure with PF" + if frappe.db.exists("Salary Structure", salary_structure): + frappe.db.delete("Salary Structure", salary_structure) + + details = { + "doctype": "Salary Structure", + "name": salary_structure, + "company": "_Test Company", + "payroll_frequency": "Monthly", + "payment_account": get_random("Account", filters={"account_currency": "INR"}), + "currency": "INR" + } + + salary_structure_doc = frappe.get_doc(details) + + for entry in earnings: + salary_structure_doc.append("earnings", entry) + + for entry in deductions: + salary_structure_doc.append("deductions", entry) + + salary_structure_doc.insert() + salary_structure_doc.submit() + + return salary_structure_doc + +def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure): + employee = frappe.db.get_value("Employee", { + "user_id": employee + }, + ["name", "company", "employee_name"], + as_dict=True) + + salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})}) + + if not salary_slip_name: + salary_slip = make_salary_slip(salary_structure, employee=employee.name) + salary_slip.employee_name = employee.employee_name + salary_slip.payroll_frequency = "Monthly" + salary_slip.posting_date = nowdate() + salary_slip.insert() + else: + salary_slip = frappe.get_doc("Salary Slip", salary_slip_name) + + return salary_slip \ No newline at end of file From 3887a67f7e9c7e665177097089b37081ee475235 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 13 Sep 2021 18:48:32 +0530 Subject: [PATCH 172/951] fix: editable price list rate field in sales transactions (#27455) (#27460) (cherry picked from commit a5baf909b7b9defd046b23e11d2c0c3a32737e2c) Co-authored-by: Saqib --- erpnext/selling/sales_common.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 091c22271e6..a068430c6c1 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -243,7 +243,12 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate")); if(df && editable_price_list_rate) { - df.read_only = 0; + const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item"); + if (!this.frm.fields_dict[parent_field]) return; + + this.frm.fields_dict[parent_field].grid.update_docfield_property( + 'price_list_rate', 'read_only', 0 + ); } }, From b86454e7f4db0fc3e909738b711ec4a4dbd87e1b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 31 Aug 2021 18:23:28 +0530 Subject: [PATCH 173/951] feat: Validity dates in Tax Withholding Rates --- .../tax_withholding_category.py | 111 ++++---- .../test_tax_withholding_category.py | 17 +- .../tax_withholding_rate.json | 256 +++++------------- erpnext/patches.txt | 1 + ...pdate_dates_in_tax_withholding_category.py | 22 ++ 5 files changed, 158 insertions(+), 249 deletions(-) create mode 100644 erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index b4bdd73c492..0dc96ea0568 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -13,7 +13,24 @@ from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): - pass + def validate(self): + self.validate_dates() + self.validate_thresholds() + + def validate_dates(self): + last_date = None + for d in self.get('rates'): + if getdate(d.from_date) >= getdate(d.to_date): + frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx)) + + # validate overlapping of dates + if last_date and getdate(r.to_date) < getdate(last_date): + frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx)) + + def validate_thresholds(self): + for d in self.get('rates'): + if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold: + frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx)) def get_party_details(inv): party_type, party = '', '' @@ -52,8 +69,8 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): if not parties: parties.append(party) - fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company) - tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company) + posting_date = inv.get('posting_date') or inv.get('transaction_date') + tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company) if not tax_details: frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') @@ -67,7 +84,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): tax_amount, tax_deducted = get_tax_amount( party_type, parties, inv, tax_details, - fiscal_year, pan_no + posting_date, pan_no ) if party_type == 'Supplier': @@ -77,16 +94,18 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None): return tax_row -def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): +def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) - tax_rate_detail = get_tax_withholding_rates(tax_withholding, fiscal_year) + tax_rate_detail = get_tax_withholding_rates(tax_withholding, posting_date) for account_detail in tax_withholding.accounts: if company == account_detail.company: return frappe._dict({ "account_head": account_detail.account, "rate": tax_rate_detail.tax_withholding_rate, + "from_date": tax_rate_detail.from_date, + "to_date": tax_rate_detail.to_date, "threshold": tax_rate_detail.single_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold, "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category, @@ -95,13 +114,13 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company): "round_off_tax_amount": tax_withholding.round_off_tax_amount }) -def get_tax_withholding_rates(tax_withholding, fiscal_year): +def get_tax_withholding_rates(tax_withholding, posting_date): # returns the row that matches with the fiscal year from posting date for rate in tax_withholding.rates: - if rate.fiscal_year == fiscal_year: + if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date): return rate - frappe.throw(_("No Tax Withholding data found for the current Fiscal Year.")) + frappe.throw(_("No Tax Withholding data found for the current posting date.")) def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted): row = { @@ -143,38 +162,38 @@ def get_tax_row_for_tds(tax_details, tax_amount): "account_head": tax_details.account_head } -def get_lower_deduction_certificate(fiscal_year, pan_no): - ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name') +def get_lower_deduction_certificate(tax_details, pan_no): + ldc_name = frappe.db.get_value('Lower Deduction Certificate', + { + 'pan_no': pan_no, + 'valid_from': ('>=', tax_details.from_date), + 'valid_upto': ('<=', tax_details.to_date) + }, 'name') + if ldc_name: return frappe.get_doc('Lower Deduction Certificate', ldc_name) -def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): - fiscal_year = fiscal_year_details[0] - - - vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) - advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) +def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): + vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type) + advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, + to_date=tax_details.to_date, party_type=party_type) taxable_vouchers = vouchers + advance_vouchers tax_deducted = 0 if taxable_vouchers: - tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details) + tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) tax_amount = 0 - posting_date = inv.get('posting_date') or inv.get('transaction_date') if party_type == 'Supplier': - ldc = get_lower_deduction_certificate(fiscal_year, pan_no) + ldc = get_lower_deduction_certificate(tax_details, pan_no) if tax_deducted: net_total = inv.net_total if ldc: - tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total) + tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 else: - tax_amount = get_tds_amount( - ldc, parties, inv, tax_details, - fiscal_year_details, tax_deducted, vouchers - ) + tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers) elif party_type == 'Customer': if tax_deducted: @@ -183,14 +202,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p else: # if no TCS has been charged in FY, # then chargeable value is "prev invoices + advances" value which cross the threshold - tax_amount = get_tcs_amount( - parties, inv, tax_details, - fiscal_year_details, vouchers, advance_vouchers - ) + tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers) return tax_amount, tax_deducted -def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'): +def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'): dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' filters = { @@ -198,14 +214,14 @@ def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'): 'company': company, 'party_type': party_type, 'party': ['in', parties], - 'fiscal_year': fiscal_year, + 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], 'is_opening': 'No', 'is_cancelled': 0 } return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""] -def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'): +def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'): # for advance vouchers, debit and credit is reversed dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit' @@ -218,8 +234,6 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None 'against_voucher': ['is', 'not set'] } - if fiscal_year: - filters['fiscal_year'] = fiscal_year if company: filters['company'] = company if from_date and to_date: @@ -227,20 +241,21 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] -def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): +def get_deducted_tax(taxable_vouchers, tax_details): # check if TDS / TCS account is already charged on taxable vouchers filters = { 'is_cancelled': 0, 'credit': ['>', 0], - 'fiscal_year': fiscal_year, + 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], 'account': tax_details.account_head, 'voucher_no': ['in', taxable_vouchers], } - field = "sum(credit)" + field = "credit" - return frappe.db.get_value('GL Entry', filters, field) or 0.0 + entries = frappe.db.get_all('GL Entry', filters, pluck=field) + return sum(entries) -def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): +def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): tds_amount = 0 invoice_filters = { 'name': ('in', vouchers), @@ -264,7 +279,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu supp_credit_amt += supp_jv_credit_amt supp_credit_amt += inv.net_total - debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company) + debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company) supp_credit_amt -= debit_note_amount threshold = tax_details.get('threshold', 0) @@ -292,9 +307,8 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu return tds_amount -def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers): +def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): tcs_amount = 0 - fiscal_year, _, _ = fiscal_year_details # sum of debit entries made from sales invoices invoiced_amt = frappe.db.get_value('GL Entry', { @@ -313,14 +327,14 @@ def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv }, 'sum(credit)') or 0.0 # sum of credit entries made from sales invoice - credit_note_amt = frappe.db.get_value('GL Entry', { + credit_note_amt = sum(frappe.db.get_all('GL Entry', { 'is_cancelled': 0, 'credit': ['>', 0], 'party': ['in', parties], - 'fiscal_year': fiscal_year, + 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], 'company': inv.company, 'voucher_type': 'Sales Invoice', - }, 'sum(credit)') or 0.0 + }, pluck='credit')) cumulative_threshold = tax_details.get('cumulative_threshold', 0) @@ -339,7 +353,7 @@ def get_invoice_total_without_tcs(inv, tax_details): return inv.grand_total - tcs_tax_row_amount -def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total): +def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total): tds_amount = 0 limit_consumed = frappe.db.get_value('Purchase Invoice', { 'supplier': ('in', parties), @@ -356,14 +370,13 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post return tds_amount -def get_debit_note_amount(suppliers, fiscal_year_details, company=None): - _, year_start_date, year_end_date = fiscal_year_details +def get_debit_note_amount(suppliers, from_date, to_date, company=None): filters = { 'supplier': ['in', suppliers], 'is_return': 1, 'docstatus': 1, - 'posting_date': ['between', (year_start_date, year_end_date)] + 'posting_date': ['between', (from_date, to_date)] } fields = ['abs(sum(net_total)) as net_total'] diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index c4a5ba52fea..8a88d798d8b 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -313,16 +313,16 @@ def create_records(): }).insert() def create_tax_with_holding_category(): - fiscal_year = get_fiscal_year(today(), company="_Test Company")[0] - - # Cummulative thresold + fiscal_year = get_fiscal_year(today(), company="_Test Company") + # Cumulative threshold if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"): frappe.get_doc({ "doctype": "Tax Withholding Category", "name": "Cumulative Threshold TDS", "category_name": "10% TDS", "rates": [{ - 'fiscal_year': fiscal_year, + 'from_date': fiscal_year[1], + 'to_date': fiscal_year[2], 'tax_withholding_rate': 10, 'single_threshold': 0, 'cumulative_threshold': 30000.00 @@ -339,7 +339,8 @@ def create_tax_with_holding_category(): "name": "Cumulative Threshold TCS", "category_name": "10% TCS", "rates": [{ - 'fiscal_year': fiscal_year, + 'from_date': fiscal_year[1], + 'to_date': fiscal_year[2], 'tax_withholding_rate': 10, 'single_threshold': 0, 'cumulative_threshold': 30000.00 @@ -357,7 +358,8 @@ def create_tax_with_holding_category(): "name": "Single Threshold TDS", "category_name": "10% TDS", "rates": [{ - 'fiscal_year': fiscal_year, + 'from_date': fiscal_year[1], + 'to_date': fiscal_year[2], 'tax_withholding_rate': 10, 'single_threshold': 20000.00, 'cumulative_threshold': 0 @@ -377,7 +379,8 @@ def create_tax_with_holding_category(): "consider_party_ledger_amount": 1, "tax_on_excess_amount": 1, "rates": [{ - 'fiscal_year': fiscal_year, + 'from_date': fiscal_year[1], + 'to_date': fiscal_year[2], 'tax_withholding_rate': 10, 'single_threshold': 0, 'cumulative_threshold': 30000 diff --git a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json index 1e8194af6e4..d2c505c6300 100644 --- a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json +++ b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json @@ -1,202 +1,72 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-07-17 16:53:13.716665", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-07-17 16:53:13.716665", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from_date", + "to_date", + "tax_withholding_rate", + "column_break_3", + "single_threshold", + "cumulative_threshold" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "fiscal_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Fiscal Year", - "length": 0, - "no_copy": 0, - "options": "Fiscal Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 1, + "fieldname": "tax_withholding_rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Tax Withholding Rate", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "tax_withholding_rate", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Tax Withholding Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "single_threshold", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Single Transaction Threshold" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "single_threshold", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Single Transaction Threshold", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 3, + "fieldname": "cumulative_threshold", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Cumulative Transaction Threshold" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 3, - "fieldname": "cumulative_threshold", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Cumulative Transaction Threshold", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "columns": 2, + "fieldname": "from_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "From Date", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "to_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "To Date", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-07-17 17:13:09.819580", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Tax Withholding Rate", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-08-31 11:42:12.213977", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withholding Rate", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fc178350337..1fde68fcf7a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -312,3 +312,4 @@ erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.create_website_items erpnext.patches.v13_0.populate_e_commerce_settings erpnext.patches.v13_0.make_homepage_products_website_items +erpnext.patches.v13_0.update_dates_in_tax_withholding_category diff --git a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py new file mode 100644 index 00000000000..2563d8a8c4b --- /dev/null +++ b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py @@ -0,0 +1,22 @@ +# Copyright (c) 2021, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from erpnext.accounts.utils import get_fiscal_year + +def execute(): + frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate') + tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year']) + + fiscal_year_map = {} + for rate in tds_category_rates: + if not fiscal_year_map.get(rate.fiscal_year): + fiscal_year_map[rate.fiscal_year] = get_fiscal_year(fiscal_year=rate.fiscal_year) + + from_date = fiscal_year_map.get(rate.fiscal_year)[1] + to_date = fiscal_year_map.get(rate.fiscal_year)[2] + + frappe.db.set_value('Tax Withholding Rate', rate.name, { + 'from_date': from_date, + 'to_date': to_date + }) \ No newline at end of file From 86220e9ed69401484f2ca88affbeb25c25802119 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 1 Sep 2021 10:05:10 +0530 Subject: [PATCH 174/951] fix: Advance TDS test case --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e77b60bb1cd..33858b2cc83 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1212,7 +1212,8 @@ def update_tax_witholding_category(company, account, date): {'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}): tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual') tds_category.append('rates', { - 'fiscal_year': fiscal_year[0], + 'from_date': fiscal_year[1], + 'to_date': fiscal_year[2], 'tax_withholding_rate': 10, 'single_threshold': 2500, 'cumulative_threshold': 0 From 9c35e3aa89408c4e834ac76fa1edea92d09acda8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 1 Sep 2021 10:11:18 +0530 Subject: [PATCH 175/951] fix: Linting and patch fixes --- .../tax_withholding_category.py | 2 +- ...pdate_dates_in_tax_withholding_category.py | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 0dc96ea0568..33b7e475e51 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -24,7 +24,7 @@ class TaxWithholdingCategory(Document): frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx)) # validate overlapping of dates - if last_date and getdate(r.to_date) < getdate(last_date): + if last_date and getdate(d.to_date) < getdate(last_date): frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx)) def validate_thresholds(self): diff --git a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py index 2563d8a8c4b..33c49428533 100644 --- a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py +++ b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py @@ -6,17 +6,19 @@ from erpnext.accounts.utils import get_fiscal_year def execute(): frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate') - tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year']) - fiscal_year_map = {} - for rate in tds_category_rates: - if not fiscal_year_map.get(rate.fiscal_year): - fiscal_year_map[rate.fiscal_year] = get_fiscal_year(fiscal_year=rate.fiscal_year) + if frappe.db.has_column('Tax Withholding Rate', 'fiscal_year'): + tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year']) - from_date = fiscal_year_map.get(rate.fiscal_year)[1] - to_date = fiscal_year_map.get(rate.fiscal_year)[2] + fiscal_year_map = {} + for rate in tds_category_rates: + if not fiscal_year_map.get(rate.fiscal_year): + fiscal_year_map[rate.fiscal_year] = get_fiscal_year(fiscal_year=rate.fiscal_year) - frappe.db.set_value('Tax Withholding Rate', rate.name, { - 'from_date': from_date, - 'to_date': to_date - }) \ No newline at end of file + from_date = fiscal_year_map.get(rate.fiscal_year)[1] + to_date = fiscal_year_map.get(rate.fiscal_year)[2] + + frappe.db.set_value('Tax Withholding Rate', rate.name, { + 'from_date': from_date, + 'to_date': to_date + }) \ No newline at end of file From 1fa4962723731a8826b781df09dbaa32c0dbae37 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 1 Sep 2021 21:15:24 +0530 Subject: [PATCH 176/951] test: Update test case --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 33858b2cc83..dd161fea85b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1209,7 +1209,8 @@ def update_tax_witholding_category(company, account, date): fiscal_year = get_fiscal_year(date=date, company=company) if not frappe.db.get_value('Tax Withholding Rate', - {'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}): + {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), + 'to_date': ('<=', fiscal_year[2])}): tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual') tds_category.append('rates', { 'from_date': fiscal_year[1], From c161daa4be1ed3a94a2eb7c7d44cea339e5c0a12 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 3 Sep 2021 20:04:18 +0530 Subject: [PATCH 177/951] fix: Debug CI --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index dd161fea85b..5dd6cd86fe3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1207,6 +1207,7 @@ def update_tax_witholding_category(company, account, date): from erpnext.accounts.utils import get_fiscal_year fiscal_year = get_fiscal_year(date=date, company=company) + print(fiscal_year[0], fiscal_year[1], fiscal_year[2], "$#$#$#") if not frappe.db.get_value('Tax Withholding Rate', {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), From 0b296e2190fcb0cfc2ead241280e41ce692d722e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Sep 2021 17:56:12 +0530 Subject: [PATCH 178/951] fix: Hardcode fiscal year and posting date --- .../doctype/fiscal_year/fiscal_year_dashboard.py | 2 +- .../doctype/purchase_invoice/test_purchase_invoice.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py index 58480df1190..92e8a426cff 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py @@ -13,7 +13,7 @@ def get_data(): }, { 'label': _('References'), - 'items': ['Period Closing Voucher', 'Tax Withholding Category'] + 'items': ['Period Closing Voucher'] }, { 'label': _('Target Details'), diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 5dd6cd86fe3..49077ddc144 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1128,10 +1128,11 @@ class TestPurchaseInvoice(unittest.TestCase): tax_withholding_category = 'TDS - 194 - Dividends - Individual') # Update tax withholding category with current fiscal year and rate details - update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) + update_tax_witholding_category('_Test Company', 'TDS Payable - _TC') # Create Purchase Order with TDS applied - po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item') + po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item', + posting_date='2021-09-15') po.apply_tds = 1 po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.save() @@ -1203,11 +1204,10 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) -def update_tax_witholding_category(company, account, date): +def update_tax_witholding_category(company, account): from erpnext.accounts.utils import get_fiscal_year - fiscal_year = get_fiscal_year(date=date, company=company) - print(fiscal_year[0], fiscal_year[1], fiscal_year[2], "$#$#$#") + fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021') if not frappe.db.get_value('Tax Withholding Rate', {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), From 9fda447dd9caec9e566fa80abaa2a36637cf8ade Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 9 Sep 2021 11:13:29 +0530 Subject: [PATCH 179/951] fix: Test Case --- .../doctype/purchase_invoice/test_purchase_invoice.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 49077ddc144..e47b3308482 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1207,12 +1207,14 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def update_tax_witholding_category(company, account): from erpnext.accounts.utils import get_fiscal_year - fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021') + fiscal_year = get_fiscal_year(fiscal_year='2021') if not frappe.db.get_value('Tax Withholding Rate', {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), 'to_date': ('<=', fiscal_year[2])}): tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual') + tds_category.set('rates', []) + tds_category.append('rates', { 'from_date': fiscal_year[1], 'to_date': fiscal_year[2], From a3db15ccb76814853c22a740b9cb15094d1628c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 9 Sep 2021 11:36:57 +0530 Subject: [PATCH 180/951] fix: Linting Issues --- .../tax_withholding_category/tax_withholding_category.py | 6 ++---- .../v13_0/update_dates_in_tax_withholding_category.py | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 33b7e475e51..fa4ea218e90 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -9,8 +9,6 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import cint, getdate -from erpnext.accounts.utils import get_fiscal_year - class TaxWithholdingCategory(Document): def validate(self): @@ -163,9 +161,9 @@ def get_tax_row_for_tds(tax_details, tax_amount): } def get_lower_deduction_certificate(tax_details, pan_no): - ldc_name = frappe.db.get_value('Lower Deduction Certificate', + ldc_name = frappe.db.get_value('Lower Deduction Certificate', { - 'pan_no': pan_no, + 'pan_no': pan_no, 'valid_from': ('>=', tax_details.from_date), 'valid_upto': ('<=', tax_details.to_date) }, 'name') diff --git a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py index 33c49428533..2af7f954128 100644 --- a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py +++ b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py @@ -2,8 +2,10 @@ # License: GNU General Public License v3. See license.txt import frappe + from erpnext.accounts.utils import get_fiscal_year + def execute(): frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate') From e8411e8bc2c9758fe13c7dd5083aabf813d78155 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 14 Sep 2021 15:06:35 +0530 Subject: [PATCH 181/951] fix: Update fiscal year --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e47b3308482..64aa9bc962a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1207,7 +1207,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def update_tax_witholding_category(company, account): from erpnext.accounts.utils import get_fiscal_year - fiscal_year = get_fiscal_year(fiscal_year='2021') + fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021') if not frappe.db.get_value('Tax Withholding Rate', {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), From 3c3f0adbd999e0aaeaa8240eefa38e023c06a907 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 14 Sep 2021 17:35:02 +0530 Subject: [PATCH 182/951] Merge pull request #27481 from deepeshgarg007/gstin_filter_issue_v13 (#27484) fix: GSTR-1 Reports not showing any data (cherry picked from commit d5f4160260caef6c190eaa01698b77efe924a66d) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/regional/report/gstr_1/gstr_1.py | 61 +++++++++++++----------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index ca0defa648a..cf4850e2781 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -96,35 +96,36 @@ class Gstr1Report(object): def get_b2c_data(self): b2cs_output = {} - for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): - invoice_details = self.invoices.get(inv) - for rate, items in items_based_on_rate.items(): - place_of_supply = invoice_details.get("place_of_supply") - ecommerce_gstin = invoice_details.get("ecommerce_gstin") + if self.invoices: + for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): + invoice_details = self.invoices.get(inv) + for rate, items in items_based_on_rate.items(): + place_of_supply = invoice_details.get("place_of_supply") + ecommerce_gstin = invoice_details.get("ecommerce_gstin") - b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{ - "place_of_supply": "", - "ecommerce_gstin": "", - "rate": "", - "taxable_value": 0, - "cess_amount": 0, - "type": "", - "invoice_number": invoice_details.get("invoice_number"), - "posting_date": invoice_details.get("posting_date"), - "invoice_value": invoice_details.get("base_grand_total"), - }) + b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin), { + "place_of_supply": "", + "ecommerce_gstin": "", + "rate": "", + "taxable_value": 0, + "cess_amount": 0, + "type": "", + "invoice_number": invoice_details.get("invoice_number"), + "posting_date": invoice_details.get("posting_date"), + "invoice_value": invoice_details.get("base_grand_total"), + }) - row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) - row["place_of_supply"] = place_of_supply - row["ecommerce_gstin"] = ecommerce_gstin - row["rate"] = rate - row["taxable_value"] += sum([abs(net_amount) - for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items]) - row["cess_amount"] += flt(self.invoice_cess.get(inv), 2) - row["type"] = "E" if ecommerce_gstin else "OE" + row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) + row["place_of_supply"] = place_of_supply + row["ecommerce_gstin"] = ecommerce_gstin + row["rate"] = rate + row["taxable_value"] += sum([abs(net_amount) + for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items]) + row["cess_amount"] += flt(self.invoice_cess.get(inv), 2) + row["type"] = "E" if ecommerce_gstin else "OE" - for key, value in iteritems(b2cs_output): - self.data.append(value) + for key, value in iteritems(b2cs_output): + self.data.append(value) def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items): row = [] @@ -173,9 +174,10 @@ class Gstr1Report(object): company_gstins = get_company_gstin_number(self.filters.get('company'), all_gstins=True) - self.filters.update({ - 'company_gstins': company_gstins - }) + if company_gstins: + self.filters.update({ + 'company_gstins': company_gstins + }) invoice_data = frappe.db.sql(""" select @@ -1050,6 +1052,7 @@ def get_company_gstin_number(company, address=None, all_gstins=False): ["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_name", "=", company], ["Dynamic Link", "parenttype", "=", "Address"], + ["gstin", "!=", ''] ] gstin = frappe.get_all("Address", filters=filters, pluck="gstin", order_by="is_primary_address desc") if gstin and not all_gstins: From cf2d1681d81b0ae189291d373029a6f8dbfb9739 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 14 Sep 2021 17:57:39 +0530 Subject: [PATCH 183/951] Merge pull request #27486 from marination/job-card-excess-transfer-hotfix fix: Handle Excess/Multiple Item Transfer against Job Card (cherry picked from commit d76e5dcb93f292b96f96eec4578bb15807862417) --- .../doctype/job_card/job_card.js | 14 ++- .../doctype/job_card/job_card.json | 5 +- .../doctype/job_card/job_card.py | 21 +++- .../doctype/job_card/test_job_card.py | 107 +++++++++++++++++- .../manufacturing_settings.json | 27 ++++- .../doctype/work_order/test_work_order.py | 7 +- .../stock/doctype/stock_entry/stock_entry.py | 4 +- 7 files changed, 158 insertions(+), 27 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 91eb4a0fa90..35be38813e5 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -26,15 +26,23 @@ frappe.ui.form.on('Job Card', { refresh: function(frm) { frappe.flags.pause_job = 0; frappe.flags.resume_job = 0; + let has_items = frm.doc.items && frm.doc.items.length; - if(!frm.doc.__islocal && frm.doc.items && frm.doc.items.length) { - if (frm.doc.for_quantity != frm.doc.transferred_qty) { + if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) { + let to_request = frm.doc.for_quantity > frm.doc.transferred_qty; + let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; + + if (to_request || excess_transfer_allowed) { frm.add_custom_button(__("Material Request"), () => { frm.trigger("make_material_request"); }); } - if (frm.doc.for_quantity != frm.doc.transferred_qty) { + // check if any row has untransferred materials + // in case of multiple items in JC + let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty); + + if (to_transfer || excess_transfer_allowed) { frm.add_custom_button(__("Material Transfer"), () => { frm.trigger("make_stock_entry"); }).addClass("btn-primary"); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 046e2fd1825..f5bbac33b81 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -185,7 +185,7 @@ "default": "0", "fieldname": "transferred_qty", "fieldtype": "Float", - "label": "Transferred Qty", + "label": "FG Qty from Transferred Raw Materials", "read_only": 1 }, { @@ -396,10 +396,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-16 15:59:32.766484", + "modified": "2021-09-13 21:34:15.177928", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ceae63cb940..3209546a12c 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -1,9 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - -from __future__ import unicode_literals - import datetime import json @@ -37,6 +34,10 @@ class OperationSequenceError(frappe.ValidationError): pass class JobCardCancelError(frappe.ValidationError): pass class JobCard(Document): + def onload(self): + excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + self.set_onload("job_card_excess_transfer", excess_transfer) + def validate(self): self.validate_time_logs() self.set_status() @@ -449,6 +450,7 @@ class JobCard(Document): frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty)) def set_transferred_qty(self, update_status=False): + "Set total FG Qty for which RM was transferred." if not self.items: self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 @@ -457,6 +459,7 @@ class JobCard(Document): return if self.items: + # sum of 'For Quantity' of Stock Entries against JC self.transferred_qty = frappe.db.get_value('Stock Entry', { 'job_card': self.name, 'work_order': self.work_order, @@ -500,7 +503,9 @@ class JobCard(Document): self.status = 'Work In Progress' if (self.docstatus == 1 and - (self.for_quantity == self.transferred_qty or not self.items)): + (self.for_quantity <= self.transferred_qty or not self.items)): + # consider excess transfer + # completed qty is checked via separate validation self.status = 'Completed' if self.status != 'Completed': @@ -618,7 +623,11 @@ def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.purpose = "Material Transfer for Manufacture" target.from_bom = 1 - target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) + + # avoid negative 'For Quantity' + pending_fg_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) + target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0 + target.set_transfer_qty() target.calculate_rate_and_amount() target.set_missing_values() diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 80295bba635..57336e1b330 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1,22 +1,38 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals - import unittest import frappe from frappe.utils import random_string -from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError +from erpnext.manufacturing.doctype.job_card.job_card import ( + make_stock_entry as make_stock_entry_from_jc, + OperationMismatchError, + OverlapError +) from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestJobCard(unittest.TestCase): def setUp(self): - self.work_order = make_wo_order_test_record(item="_Test FG Item 2", qty=2) + transfer_material_against, source_warehouse = None, None + tests_that_transfer_against_jc = ("test_job_card_multiple_materials_transfer", + "test_job_card_excess_material_transfer") + + if self._testMethodName in tests_that_transfer_against_jc: + transfer_material_against = "Job Card" + source_warehouse = "Stores - _TC" + + self.work_order = make_wo_order_test_record( + item="_Test FG Item 2", + qty=2, + transfer_material_against=transfer_material_against, + source_warehouse=source_warehouse + ) def tearDown(self): frappe.db.rollback() @@ -96,3 +112,84 @@ class TestJobCard(unittest.TestCase): "employee": employee, }) self.assertRaises(OverlapError, jc2.save) + + def test_job_card_multiple_materials_transfer(self): + "Test transferring RMs separately against Job Card with multiple RMs." + make_stock_entry( + item_code="_Test Item", + target="Stores - _TC", + qty=10, + basic_rate=100 + ) + make_stock_entry( + item_code="_Test Item Home Desktop Manufactured", + target="Stores - _TC", + qty=6, + basic_rate=100 + ) + + job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card = frappe.get_doc("Job Card", job_card_name) + + transfer_entry_1 = make_stock_entry_from_jc(job_card_name) + del transfer_entry_1.items[1] # transfer only 1 of 2 RMs + transfer_entry_1.insert() + transfer_entry_1.submit() + + job_card.reload() + + self.assertEqual(transfer_entry_1.fg_completed_qty, 2) + self.assertEqual(job_card.transferred_qty, 2) + + # transfer second RM + transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + del transfer_entry_2.items[0] + transfer_entry_2.insert() + transfer_entry_2.submit() + + # 'For Quantity' here will be 0 since + # transfer was made for 2 fg qty in first transfer Stock Entry + self.assertEqual(transfer_entry_2.fg_completed_qty, 0) + + def test_job_card_excess_material_transfer(self): + "Test transferring more than required RM against Job Card." + make_stock_entry(item_code="_Test Item", target="Stores - _TC", + qty=25, basic_rate=100) + make_stock_entry(item_code="_Test Item Home Desktop Manufactured", + target="Stores - _TC", qty=15, basic_rate=100) + + job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card = frappe.get_doc("Job Card", job_card_name) + + # fully transfer both RMs + transfer_entry_1 = make_stock_entry_from_jc(job_card_name) + transfer_entry_1.insert() + transfer_entry_1.submit() + + # transfer extra qty of both RM due to previously damaged RM + transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + # deliberately change 'For Quantity' + transfer_entry_2.fg_completed_qty = 1 + transfer_entry_2.items[0].qty = 5 + transfer_entry_2.items[1].qty = 3 + transfer_entry_2.insert() + transfer_entry_2.submit() + + job_card.reload() + self.assertGreater(job_card.transferred_qty, job_card.for_quantity) + + # Check if 'For Quantity' is negative + # as 'transferred_qty' > Qty to Manufacture + transfer_entry_3 = make_stock_entry_from_jc(job_card_name) + self.assertEqual(transfer_entry_3.fg_completed_qty, 0) + + job_card.append("time_logs", { + "from_time": "2021-01-01 00:01:00", + "to_time": "2021-01-01 06:00:00", + "completed_qty": 2 + }) + job_card.save() + job_card.submit() + + # JC is Completed with excess transfer + self.assertEqual(job_card.status, "Completed") \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 024f7847259..01647d56c91 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -25,9 +25,12 @@ "overproduction_percentage_for_sales_order", "column_break_16", "overproduction_percentage_for_work_order", + "job_card_section", + "add_corrective_operation_cost_in_finished_good_valuation", + "column_break_24", + "job_card_excess_transfer", "other_settings_section", "update_bom_costs_automatically", - "add_corrective_operation_cost_in_finished_good_valuation", "column_break_23", "make_serial_no_batch_from_work_order" ], @@ -96,10 +99,10 @@ }, { "default": "0", - "description": "Allow multiple material consumptions against a Work Order", + "description": "Allow material consumptions without immediately manufacturing finished goods against a Work Order", "fieldname": "material_consumption", "fieldtype": "Check", - "label": "Allow Multiple Material Consumption" + "label": "Allow Continuous Material Consumption" }, { "default": "0", @@ -175,13 +178,29 @@ "fieldname": "add_corrective_operation_cost_in_finished_good_valuation", "fieldtype": "Check", "label": "Add Corrective Operation Cost in Finished Good Valuation" + }, + { + "fieldname": "job_card_section", + "fieldtype": "Section Break", + "label": "Job Card" + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Allow transferring raw materials even after the Required Quantity is fulfilled", + "fieldname": "job_card_excess_transfer", + "fieldtype": "Check", + "label": "Allow Excess Material Transfer" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-03-16 15:54:38.967341", + "modified": "2021-09-13 22:09:09.401559", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bb431498636..d87b5ec654f 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1,9 +1,5 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt - - -from __future__ import unicode_literals - import unittest import frappe @@ -814,6 +810,7 @@ def make_wo_order_test_record(**args): wo_order.get_items_and_operations_from_bom() wo_order.sales_order = args.sales_order or None wo_order.planned_start_date = args.planned_start_date or now() + wo_order.transfer_material_against = args.transfer_material_against or "Work Order" if args.source_warehouse: for item in wo_order.get("required_items"): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index df98b681106..094ad6f0ae9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1264,9 +1264,9 @@ class StockEntry(StockController): po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from `tabWork Order` where name=%s""", self.work_order, as_dict=1)[0] - manufacturing_qty = flt(po_qty.qty) + manufacturing_qty = flt(po_qty.qty) or 1 produced_qty = flt(po_qty.produced_qty) - trans_qty = flt(po_qty.material_transferred_for_manufacturing) + trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1 for item in transferred_materials: qty= item.qty From 52157cc0005b841c3fbbff269fce7e7cf867644b Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 14 Sep 2021 18:43:15 +0530 Subject: [PATCH 184/951] Merge pull request #27488 from marination/validate-cart-settings fix: Args missing error on changing Price List currency with cart enabled (cherry picked from commit 2a9fbc609dc5b46ef6c9f8bd0bbd0eb634c615da) --- .../doctype/e_commerce_settings/e_commerce_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6fc5ef9e958..1f3b388a0ad 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 @@ -127,7 +127,7 @@ class ECommerceSettings(Document): if not (new_fields == old_fields): create_website_items_index() -def validate_cart_settings(doc, method): +def validate_cart_settings(doc=None, method=None): frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate") def get_shopping_cart_settings(): From 9659acb31ed088da146dd173fb2f72d0fffb539a Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 15 Sep 2021 01:19:15 +0530 Subject: [PATCH 185/951] fix: calculate operating cost based on BOM Quantity (#27464) (#27500) * fix: calculate operating cost based on BOM Quantity * fix: added test cases (cherry picked from commit 2e2985e4f14ddc063a9f481a4ac5c40b18e1f633) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.py | 12 +++- erpnext/manufacturing/doctype/bom/test_bom.py | 18 ++++++ .../doctype/bom_operation/bom_operation.json | 64 +++++++++++++++++-- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 1463aa7d34a..3ea756eec97 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -511,8 +511,14 @@ class BOM(WebsiteGenerator): if d.workstation: self.update_rate_and_time(d, update_hour_rate) - self.operating_cost += flt(d.operating_cost) - self.base_operating_cost += flt(d.base_operating_cost) + operating_cost = d.operating_cost + base_operating_cost = d.base_operating_cost + if d.set_cost_based_on_bom_qty: + operating_cost = flt(d.cost_per_unit) * flt(self.quantity) + base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity) + + self.operating_cost += flt(operating_cost) + self.base_operating_cost += flt(base_operating_cost) def update_rate_and_time(self, row, update_hour_rate = False): if not row.hour_rate or update_hour_rate: @@ -536,6 +542,8 @@ class BOM(WebsiteGenerator): row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) + row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) if update_hour_rate: row.db_update() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 6484b7f699d..8338fa30ddc 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -107,6 +107,24 @@ class TestBOM(unittest.TestCase): self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) + def test_bom_cost_with_batch_size(self): + bom = frappe.copy_doc(test_records[2]) + bom.docstatus = 0 + op_cost = 0.0 + for op_row in bom.operations: + op_row.docstatus = 0 + op_row.batch_size = 2 + op_row.set_cost_based_on_bom_qty = 1 + op_cost += op_row.operating_cost + + bom.save() + + for op_row in bom.operations: + self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2) + + self.assertAlmostEqual(bom.operating_cost, op_cost/2) + bom.delete() + def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)): diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 4458e6db234..ec617f3aaa9 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -8,15 +8,23 @@ "field_order": [ "sequence_id", "operation", - "workstation", - "description", "col_break1", - "hour_rate", + "workstation", "time_in_mins", - "operating_cost", + "costing_section", + "hour_rate", "base_hour_rate", + "column_break_9", + "operating_cost", "base_operating_cost", + "column_break_11", "batch_size", + "set_cost_based_on_bom_qty", + "cost_per_unit", + "base_cost_per_unit", + "more_information_section", + "description", + "column_break_18", "image" ], "fields": [ @@ -117,13 +125,59 @@ "fieldname": "sequence_id", "fieldtype": "Int", "label": "Sequence ID" + }, + { + "depends_on": "eval:doc.batch_size > 0 && doc.set_cost_based_on_bom_qty", + "fieldname": "cost_per_unit", + "fieldtype": "Float", + "label": "Cost Per Unit", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "base_cost_per_unit", + "fieldtype": "Float", + "hidden": 1, + "label": "Base Cost Per Unit", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "costing_section", + "fieldtype": "Section Break", + "label": "Costing" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "set_cost_based_on_bom_qty", + "fieldtype": "Check", + "label": "Set Operating Cost Based On BOM Quantity" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-12 14:48:09.596843", + "modified": "2021-09-13 16:45:01.092868", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", From 2ae48eeac87cc7c811c1b7dffe1eebd1b7d4a2f7 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 15 Sep 2021 10:29:50 +0530 Subject: [PATCH 186/951] fix: Patch for updating tax withholding category dates (#27489) (#27494) (cherry picked from commit c53b78e712428f9b83ecd1825891e7287eb325e4) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- ...pdate_dates_in_tax_withholding_category.py | 14 +- erpnext/regional/india/setup.py | 123 +++++++++--------- 2 files changed, 69 insertions(+), 68 deletions(-) diff --git a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py index 2af7f954128..90fb50fb42c 100644 --- a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py +++ b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py @@ -3,8 +3,6 @@ import frappe -from erpnext.accounts.utils import get_fiscal_year - def execute(): frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate') @@ -13,12 +11,14 @@ def execute(): tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year']) fiscal_year_map = {} - for rate in tds_category_rates: - if not fiscal_year_map.get(rate.fiscal_year): - fiscal_year_map[rate.fiscal_year] = get_fiscal_year(fiscal_year=rate.fiscal_year) + fiscal_year_details = frappe.get_all('Fiscal Year', fields=['name', 'year_start_date', 'year_end_date']) - from_date = fiscal_year_map.get(rate.fiscal_year)[1] - to_date = fiscal_year_map.get(rate.fiscal_year)[2] + for d in fiscal_year_details: + fiscal_year_map.setdefault(d.name, d) + + for rate in tds_category_rates: + from_date = fiscal_year_map.get(rate.fiscal_year).get('year_start_date') + to_date = fiscal_year_map.get(rate.fiscal_year).get('year_end_date') frappe.db.set_value('Tax Withholding Rate', rate.name, { 'from_date': from_date, diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 888dcfc7bd7..03b5c8ad5f9 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -803,11 +803,11 @@ def set_tax_withholding_category(company): accounts = [dict(company=company, account=tds_account)] try: - fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0] + fiscal_year_details = get_fiscal_year(today(), verbose=0, company=company) except FiscalYearError: pass - docs = get_tds_details(accounts, fiscal_year) + docs = get_tds_details(accounts, fiscal_year_details) for d in docs: if not frappe.db.exists("Tax Withholding Category", d.get("name")): @@ -822,9 +822,10 @@ def set_tax_withholding_category(company): if accounts: doc.append("accounts", accounts[0]) - if fiscal_year: + if fiscal_year_details: # if fiscal year don't match with any of the already entered data, append rate row - fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] + fy_exist = [k for k in doc.get('rates') if k.get('from_date') <= fiscal_year_details[1] \ + and k.get('to_date') >= fiscal_year_details[2]] if not fy_exist: doc.append("rates", d.get('rates')[0]) @@ -847,149 +848,149 @@ def set_tds_account(docs, company): } ]) -def get_tds_details(accounts, fiscal_year): +def get_tds_details(accounts, fiscal_year_details): # bootstrap default tax withholding sections return [ dict(name="TDS - 194C - Company", category_name="Payment to Contractors (Single / Aggregate)", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2, - "single_threshold": 30000, "cumulative_threshold": 100000}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, "single_threshold": 30000, "cumulative_threshold": 100000}]), dict(name="TDS - 194C - Individual", category_name="Payment to Contractors (Single / Aggregate)", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1, - "single_threshold": 30000, "cumulative_threshold": 100000}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, "single_threshold": 30000, "cumulative_threshold": 100000}]), dict(name="TDS - 194C - No PAN / Invalid PAN", category_name="Payment to Contractors (Single / Aggregate)", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 30000, "cumulative_threshold": 100000}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 100000}]), dict(name="TDS - 194D - Company", category_name="Insurance Commission", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5, - "single_threshold": 15000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), dict(name="TDS - 194D - Company Assessee", category_name="Insurance Commission", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 15000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 15000, "cumulative_threshold": 0}]), dict(name="TDS - 194D - Individual", category_name="Insurance Commission", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5, - "single_threshold": 15000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), dict(name="TDS - 194D - No PAN / Invalid PAN", category_name="Insurance Commission", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 15000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), dict(name="TDS - 194DA - Company", category_name="Non-exempt payments made under a life insurance policy", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1, - "single_threshold": 100000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), dict(name="TDS - 194DA - Individual", category_name="Non-exempt payments made under a life insurance policy", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 1, - "single_threshold": 100000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), dict(name="TDS - 194DA - No PAN / Invalid PAN", category_name="Non-exempt payments made under a life insurance policy", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 100000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 100000, "cumulative_threshold": 0}]), dict(name="TDS - 194H - Company", category_name="Commission / Brokerage", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5, - "single_threshold": 15000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), dict(name="TDS - 194H - Individual", category_name="Commission / Brokerage", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 5, - "single_threshold": 15000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), dict(name="TDS - 194H - No PAN / Invalid PAN", category_name="Commission / Brokerage", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 15000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), dict(name="TDS - 194I - Rent - Company", category_name="Rent", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 180000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), dict(name="TDS - 194I - Rent - Individual", category_name="Rent", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 180000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), dict(name="TDS - 194I - Rent - No PAN / Invalid PAN", category_name="Rent", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 180000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), dict(name="TDS - 194I - Rent/Machinery - Company", category_name="Rent-Plant / Machinery", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2, - "single_threshold": 180000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), dict(name="TDS - 194I - Rent/Machinery - Individual", category_name="Rent-Plant / Machinery", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 2, - "single_threshold": 180000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN", category_name="Rent-Plant / Machinery", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 180000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), dict(name="TDS - 194J - Professional Fees - Company", category_name="Professional Fees", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 30000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), dict(name="TDS - 194J - Professional Fees - Individual", category_name="Professional Fees", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 30000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN", category_name="Professional Fees", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 30000, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 0}]), dict(name="TDS - 194J - Director Fees - Company", category_name="Director Fees", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 0, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), dict(name="TDS - 194J - Director Fees - Individual", category_name="Director Fees", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 0, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN", category_name="Director Fees", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 0, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 0, "cumulative_threshold": 0}]), dict(name="TDS - 194 - Dividends - Company", category_name="Dividends", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 2500, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), dict(name="TDS - 194 - Dividends - Individual", category_name="Dividends", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 10, - "single_threshold": 2500, "cumulative_threshold": 0}]), + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN", category_name="Dividends", doctype="Tax Withholding Category", accounts=accounts, - rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, - "single_threshold": 2500, "cumulative_threshold": 0}]) + rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}]) ] def create_gratuity_rule(): From 397fad7eb26b0ffa19e7461a04fe589769da1c36 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 15 Sep 2021 11:49:04 +0530 Subject: [PATCH 187/951] fix: Values with same account and different account number in consolidated balance sheet report (#27493) (#27503) (cherry picked from commit 625626b973b399ccc963370edb940e2e9f84d948) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../consolidated_financial_statement.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) 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 e419727c2d1..b0cfbac9cb1 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -260,7 +260,12 @@ def get_company_currency(filters=None): def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters): for entries in gl_entries_by_account.values(): for entry in entries: - d = accounts_by_name.get(entry.account_name) + if entry.account_number: + account_name = entry.account_number + ' - ' + entry.account_name + else: + account_name = entry.account_name + + d = accounts_by_name.get(account_name) if d: for company in companies: # check if posting date is within the period @@ -307,7 +312,14 @@ def update_parent_account_names(accounts): of account_number and suffix of company abbr. This function adds key called `parent_account_name` which does not have such prefix/suffix. """ - name_to_account_map = { d.name : d.account_name for d in accounts } + name_to_account_map = {} + + for d in accounts: + if d.account_number: + account_name = d.account_number + ' - ' + d.account_name + else: + account_name = d.account_name + name_to_account_map[d.name] = account_name for account in accounts: if account.parent_account: @@ -420,7 +432,11 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) for entry in gl_entries: - account_name = entry.account_name + if entry.account_number: + account_name = entry.account_number + ' - ' + entry.account_name + else: + account_name = entry.account_name + validate_entries(account_name, entry, accounts_by_name, accounts) gl_entries_by_account.setdefault(account_name, []).append(entry) @@ -491,7 +507,12 @@ def filter_accounts(accounts, depth=10): parent_children_map = {} accounts_by_name = {} for d in accounts: - accounts_by_name[d.account_name] = d + if d.account_number: + account_name = d.account_number + ' - ' + d.account_name + else: + account_name = d.account_name + accounts_by_name[account_name] = d + parent_children_map.setdefault(d.parent_account or None, []).append(d) filtered_accounts = [] From 0572c0ae3e327ebed484f0608ddee3cd4c139aac Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 15 Sep 2021 14:00:14 +0530 Subject: [PATCH 188/951] Merge pull request #27508 from marination/shopping-cart-fixes fix: Shopping Cart and Variant Selection (cherry picked from commit 9e0fb74ab2bbfdd0b71489ae4edc43577072cf70) --- .../doctype/website_item/website_item.py | 4 +- erpnext/e_commerce/shopping_cart/cart.py | 6 +- .../variant_selector/item_variants_cache.py | 8 +- erpnext/public/js/shopping_cart.js | 16 ++- erpnext/templates/includes/cart.js | 10 +- .../includes/cart/cart_items_total.html | 10 ++ .../includes/cart/cart_payment_summary.html | 107 +++++++++--------- erpnext/templates/pages/cart.html | 18 +-- 8 files changed, 101 insertions(+), 78 deletions(-) create mode 100644 erpnext/templates/includes/cart/cart_items_total.html diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index fb729641b50..dad9b9bd369 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -12,8 +12,6 @@ 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 - -# SEARCH from erpnext.e_commerce.redisearch import ( delete_item_from_index, insert_item_to_index, @@ -138,10 +136,10 @@ class WebsiteItem(WebsiteGenerator): self.website_image = None def make_thumbnail(self): + """Make a thumbnail of `website_image`""" if frappe.flags.in_import or frappe.flags.in_migrate: return - """Make a thumbnail of `website_image`""" import requests.exceptions if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index b4295d2105b..1b4d68e4f58 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -105,7 +105,7 @@ def place_order(): if is_stock_item: item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse") if not cint(item_stock.in_stock): - throw(_("{1} Not in Stock").format(item.item_code)) + throw(_("{0} Not in Stock").format(item.item_code)) if item.qty > item_stock.stock_qty[0][0]: throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) @@ -168,8 +168,10 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): return { "items": frappe.render_template("templates/includes/cart/cart_items.html", context), - "taxes": frappe.render_template("templates/includes/order/order_taxes.html", + "total": frappe.render_template("templates/includes/cart/cart_items_total.html", context), + "taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html", + context) } else: return { diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 636ae8d4917..39eb9155d5e 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -67,12 +67,16 @@ class ItemVariantsCacheManager: as_list=1 ) - disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})]) + unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])]) attribute_value_item_map = frappe._dict({}) item_attribute_value_map = frappe._dict({}) - item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] + # dont consider variants that are unpublished + # (either have no Website Item or are unpublished in Website Item) + item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items] + item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})] + for row in item_variants_data: item_code, attribute, attribute_value = row # (attr, value) => [item1, item2] diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js index d99063b0454..d14740c1060 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -105,6 +105,8 @@ $.extend(shopping_cart, { }, set_cart_count: function(animate=false) { + $(".intermediate-empty-cart").remove(); + var cart_count = frappe.get_cookie("cart_count"); if(frappe.session.user==="Guest") { cart_count = 0; @@ -119,13 +121,20 @@ $.extend(shopping_cart, { if(parseInt(cart_count) === 0 || cart_count === undefined) { $cart.css("display", "none"); - $(".cart-items").html('Cart is Empty'); $(".cart-tax-items").hide(); $(".btn-place-order").hide(); $(".cart-payment-addresses").hide(); + + let intermediate_empty_cart_msg = ` +
+ ${ __("Cart is Empty") } +
+ `; + $(".cart-table").after(intermediate_empty_cart_msg); } else { $cart.css("display", "inline"); + $("#cart-count").text(cart_count); } if(cart_count) { @@ -152,7 +161,10 @@ $.extend(shopping_cart, { callback: function(r) { if(!r.exc) { $(".cart-items").html(r.message.items); - $(".cart-tax-items").html(r.message.taxes); + $(".cart-tax-items").html(r.message.total); + $(".payment-summary").html(r.message.taxes_and_totals); + shopping_cart.set_cart_count(); + if (cart_dropdown != true) { $(".cart-icon").hide(); } diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js index ee8ec73b42a..0c970450be6 100644 --- a/erpnext/templates/includes/cart.js +++ b/erpnext/templates/includes/cart.js @@ -57,7 +57,7 @@ $.extend(shopping_cart, { callback: function(r) { d.hide(); if (!r.exc) { - $(".cart-tax-items").html(r.message.taxes); + $(".cart-tax-items").html(r.message.total); shopping_cart.parent.find( `.address-container[data-address-type="${address_type}"]` ).html(r.message.address); @@ -214,12 +214,15 @@ $.extend(shopping_cart, { }, place_order: function(btn) { + shopping_cart.freeze(); + return frappe.call({ type: "POST", method: "erpnext.e_commerce.shopping_cart.cart.place_order", btn: btn, callback: function(r) { if(r.exc) { + shopping_cart.unfreeze(); var msg = ""; if(r._server_messages) { msg = JSON.parse(r._server_messages || []).join("
"); @@ -230,7 +233,6 @@ $.extend(shopping_cart, { .html(msg || frappe._("Something went wrong!")) .toggle(true); } else { - $('.cart-container table').hide(); $(btn).hide(); window.location.href = '/orders/' + encodeURIComponent(r.message); } @@ -239,12 +241,15 @@ $.extend(shopping_cart, { }, request_quotation: function(btn) { + shopping_cart.freeze(); + return frappe.call({ type: "POST", method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation", btn: btn, callback: function(r) { if(r.exc) { + shopping_cart.unfreeze(); var msg = ""; if(r._server_messages) { msg = JSON.parse(r._server_messages || []).join("
"); @@ -255,7 +260,6 @@ $.extend(shopping_cart, { .html(msg || frappe._("Something went wrong!")) .toggle(true); } else { - $('.cart-container table').hide(); $(btn).hide(); window.location.href = '/quotations/' + encodeURIComponent(r.message); } diff --git a/erpnext/templates/includes/cart/cart_items_total.html b/erpnext/templates/includes/cart/cart_items_total.html new file mode 100644 index 00000000000..c94fde462b1 --- /dev/null +++ b/erpnext/templates/includes/cart/cart_items_total.html @@ -0,0 +1,10 @@ + + + + + {{ _("Total") }} + + + {{ doc.get_formatted("total") }} + + \ No newline at end of file diff --git a/erpnext/templates/includes/cart/cart_payment_summary.html b/erpnext/templates/includes/cart/cart_payment_summary.html index c08b0c73888..847d45f8ffe 100644 --- a/erpnext/templates/includes/cart/cart_payment_summary.html +++ b/erpnext/templates/includes/cart/cart_payment_summary.html @@ -1,62 +1,61 @@ -
-
- {{ _("Payment Summary") }} -
-
-
- - - - - +
+ {{ _("Payment Summary") }} +
+
+
+
{{ _("Net Total (") + frappe.utils.cstr(doc.items|len) + _(" Items)") }}{{ doc.get_formatted("net_total") }}
+ + {% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %} + + + - - {% for d in doc.taxes %} - {% if d.base_tax_amount %} - - - - - {% endif %} - {% endfor %} -
{{ _("Net Total (") + total_items + _(" Items)") }}{{ doc.get_formatted("net_total") }}
- {{ d.description }} - - {{ d.get_formatted("base_tax_amount") }} -
+ + {% for d in doc.taxes %} + {% if d.base_tax_amount %} + + + {{ d.description }} + + + {{ d.get_formatted("base_tax_amount") }} + + + {% endif %} + {% endfor %} + - - + + - - - - - -
{{ _("Grand Total") }}{{ doc.get_formatted("grand_total") }}
+ + + + + +
{{ _("Grand Total") }}{{ doc.get_formatted("grand_total") }}
- {% if cart_settings.enable_checkout %} - - {% else %} - - {% endif %} -
+ {% if cart_settings.enable_checkout %} + + {% else %} + + {% endif %}
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html index a0aef90461c..fa7b0925599 100644 --- a/erpnext/templates/pages/cart.html +++ b/erpnext/templates/pages/cart.html @@ -45,15 +45,7 @@ {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} - - - - {{ _("Total") }} - - - {{ doc.get_formatted("total") }} - - + {% include "templates/includes/cart/cart_items_total.html" %} {% endif %} @@ -110,7 +102,9 @@ {% endif %} {% if cart_settings.enable_checkout %} - {% include "templates/includes/cart/cart_payment_summary.html" %} +
+ {% include "templates/includes/cart/cart_payment_summary.html" %} +
{% endif %} {% include "templates/includes/cart/cart_address.html" %} @@ -126,11 +120,11 @@
{{ _('Your cart is Empty') }}

{% if cart_settings.enable_checkout %} - + {{ _('See past orders') }} {% else %} - + {{ _('See past quotations') }} {% endif %} From d1a47619559a253a1a50664af770c52286a095b0 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 15 Sep 2021 17:08:41 +0530 Subject: [PATCH 189/951] fix: Maintain same rate in Stock Ledger until stock become positive (#27227) (#27477) * fix: Maintain same rate in Stock Ledger until stock become positive * fix: Maintain same rate in Stock Ledger until stock become positive (cherry picked from commit 10754831c33b3459d5a45c98f875afa48a444627) Co-authored-by: Nabin Hait --- erpnext/stock/stock_ledger.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 6f98e314e2a..8a501a8a5b8 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -673,11 +673,15 @@ class update_entries_after(object): if self.wh_data.stock_queue[-1][1]==incoming_rate: self.wh_data.stock_queue[-1][0] += actual_qty else: + # Item has a positive balance qty, add new entry if self.wh_data.stock_queue[-1][0] > 0: self.wh_data.stock_queue.append([actual_qty, incoming_rate]) - else: + else: # negative balance qty qty = self.wh_data.stock_queue[-1][0] + actual_qty - self.wh_data.stock_queue[-1] = [qty, incoming_rate] + if qty > 0: # new balance qty is positive + self.wh_data.stock_queue[-1] = [qty, incoming_rate] + else: # new balance qty is still negative, maintain same rate + self.wh_data.stock_queue[-1][0] = qty else: qty_to_pop = abs(actual_qty) while qty_to_pop: From 05663268e613f5b3269a1000a6632411725f9449 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 15 Sep 2021 18:15:38 +0530 Subject: [PATCH 190/951] feat: provision to add scrap item in job card (#27483) (#27518) (cherry picked from commit c5a77f60ed362ece3dd7ecd4568c82809f15bf28) Co-authored-by: rohitwaghchaure --- .../doctype/job_card/job_card.json | 17 +++- .../doctype/job_card_scrap_item/__init__.py | 0 .../job_card_scrap_item.json | 82 ++++++++++++++++++ .../job_card_scrap_item.py | 8 ++ .../production_plan/test_production_plan.py | 1 + .../doctype/work_order/test_work_order.py | 56 ++++++++++++- .../stock/doctype/stock_entry/stock_entry.py | 84 ++++++++++++++++++- 7 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json create mode 100644 erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index f5bbac33b81..7dd38f4673d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -38,6 +38,8 @@ "total_time_in_mins", "section_break_8", "items", + "scrap_items_section", + "scrap_items", "corrective_operation_section", "for_job_card", "is_corrective_job_card", @@ -392,11 +394,24 @@ "fieldtype": "Link", "label": "Batch No", "options": "Batch" + }, + { + "fieldname": "scrap_items_section", + "fieldtype": "Section Break", + "label": "Scrap Items" + }, + { + "fieldname": "scrap_items", + "fieldtype": "Table", + "label": "Scrap Items", + "no_copy": 1, + "options": "Job Card Scrap Item", + "print_hide": 1 } ], "is_submittable": 1, "links": [], - "modified": "2021-09-13 21:34:15.177928", + "modified": "2021-09-14 00:38:46.873105", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py b/erpnext/manufacturing/doctype/job_card_scrap_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json new file mode 100644 index 00000000000..9e9f1c4c89f --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.json @@ -0,0 +1,82 @@ +{ + "actions": [], + "creation": "2021-09-14 00:30:28.533884", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "description", + "quantity_and_rate", + "stock_qty", + "column_break_6", + "stock_uom" + ], + "fields": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Scrap Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Scrap Item Name" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "item_code.description", + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "read_only": 1 + }, + { + "fieldname": "quantity_and_rate", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "reqd": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-14 01:20:48.588052", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Scrap Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py new file mode 100644 index 00000000000..372df1b0fad --- /dev/null +++ b/erpnext/manufacturing/doctype/job_card_scrap_item/job_card_scrap_item.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from frappe.model.document import Document + + +class JobCardScrapItem(Document): + pass diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 6a942d54335..707b3f62d4e 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -404,6 +404,7 @@ def make_bom(**args): 'uom': item_doc.stock_uom, 'stock_uom': item_doc.stock_uom, 'rate': item_doc.valuation_rate or args.rate, + 'source_warehouse': args.source_warehouse }) if not args.do_not_save: diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d87b5ec654f..85b5bfb9bfc 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -16,7 +16,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import ( stop_unstop, ) from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_bin @@ -768,6 +768,60 @@ class TestWorkOrder(unittest.TestCase): total_pl_qty ) + def test_job_card_scrap_item(self): + items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', + 'Test RM Item 2 for Scrap Item Test'] + + company = '_Test Company with perpetual inventory' + for item_code in items: + create_item(item_code = item_code, is_stock_item = 1, + is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1') + + item = 'Test FG Item for Scrap Item Test' + raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test'] + if not frappe.db.get_value('BOM', {'item': item}): + bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True) + bom.with_operations = 1 + bom.append('operations', { + 'operation': '_Test Operation 1', + 'workstation': '_Test Workstation 1', + 'hour_rate': 20, + 'time_in_mins': 60 + }) + + bom.submit() + + wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1) + job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + update_job_card(job_card) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + for row in stock_entry.items: + if row.is_scrap_item: + self.assertEqual(row.qty, 1) + +def update_job_card(job_card): + job_card_doc = frappe.get_doc('Job Card', job_card) + job_card_doc.set('scrap_items', [ + { + 'item_code': 'Test RM Item 1 for Scrap Item Test', + 'stock_qty': 2 + }, + { + 'item_code': 'Test RM Item 2 for Scrap Item Test', + 'stock_qty': 2 + }, + ]) + + job_card_doc.append('time_logs', { + 'from_time': now(), + 'time_in_mins': 60, + 'completed_qty': job_card_doc.for_quantity + }) + + job_card_doc.submit() + + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 094ad6f0ae9..2b9bb712171 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import json +from collections import defaultdict import frappe from frappe import _ @@ -684,7 +685,7 @@ class StockEntry(StockController): def validate_bom(self): for d in self.get('items'): - if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse): + if d.bom_no and d.is_finished_item: item_code = d.original_item or d.item_code validate_bom_no(item_code, d.bom_no) @@ -1191,13 +1192,88 @@ class StockEntry(StockController): # item dict = { item_code: {qty, description, stock_uom} } item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = 0, fetch_scrap_items = 1) + fetch_exploded = 0, fetch_scrap_items = 1) or {} for item in itervalues(item_dict): item.from_warehouse = "" item.is_scrap_item = 1 + + for row in self.get_scrap_items_from_job_card(): + if row.stock_qty <= 0: + continue + + item_row = item_dict.get(row.item_code) + if not item_row: + item_row = frappe._dict({}) + + item_row.update({ + 'uom': row.stock_uom, + 'from_warehouse': '', + 'qty': row.stock_qty + flt(item_row.stock_qty), + 'converison_factor': 1, + 'is_scrap_item': 1, + 'item_name': row.item_name, + 'description': row.description, + 'allow_zero_valuation_rate': 1 + }) + + item_dict[row.item_code] = item_row + return item_dict + def get_scrap_items_from_job_card(self): + if not self.pro_doc: + self.set_work_order_details() + + scrap_items = frappe.db.sql(''' + SELECT + JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description + FROM + `tabJob Card` JC, `tabJob Card Scrap Item` JCSI + WHERE + JCSI.parent = JC.name AND JC.docstatus = 1 + AND JCSI.item_code IS NOT NULL AND JC.work_order = %s + GROUP BY + JCSI.item_code + ''', self.work_order, as_dict=1) + + pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty) + if pending_qty <=0: + return [] + + used_scrap_items = self.get_used_scrap_items() + for row in scrap_items: + row.stock_qty -= flt(used_scrap_items.get(row.item_code)) + row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty) + + if used_scrap_items.get(row.item_code): + used_scrap_items[row.item_code] -= row.stock_qty + + if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')): + row.stock_qty = frappe.utils.ceil(row.stock_qty) + + return scrap_items + + def get_used_scrap_items(self): + used_scrap_items = defaultdict(float) + data = frappe.get_all( + 'Stock Entry', + fields = [ + '`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`' + ], + filters = [ + ['Stock Entry', 'work_order', '=', self.work_order], + ['Stock Entry Detail', 'is_scrap_item', '=', 1], + ['Stock Entry', 'docstatus', '=', 1], + ['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']] + ] + ) + + for row in data: + used_scrap_items[row.item_code] += row.qty + + return used_scrap_items + def get_unconsumed_raw_materials(self): wo = frappe.get_doc("Work Order", self.work_order) wo_items = frappe.get_all('Work Order Item', @@ -1417,8 +1493,8 @@ class StockEntry(StockController): se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) se_child.is_process_loss = item_dict[d].get("is_process_loss", 0) - for field in ["idx", "po_detail", "original_item", - "expense_account", "description", "item_name", "serial_no", "batch_no"]: + for field in ["idx", "po_detail", "original_item", "expense_account", + "description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]: if item_dict[d].get(field): se_child.set(field, item_dict[d].get(field)) From 6d2d97bac449db0aba3a78e6e6c1573c2dc51db3 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 15 Sep 2021 19:11:28 +0530 Subject: [PATCH 191/951] fix(minor): Remove b2c limit check from CDNR Invoices (#27516) (#27519) * fix(minor): Remove b2c limit check from CDNR Invoices * fix: Remove unnecessary format (cherry picked from commit 978028c880445362afc0cfca9118424174541cc7) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/regional/report/gstr_1/gstr_1.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index cf4850e2781..23924c5fb66 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -214,7 +214,7 @@ class Gstr1Report(object): if self.filters.get("type_of_business") == "B2B": - conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1" + conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') @@ -223,7 +223,7 @@ class Gstr1Report(object): if self.filters.get("type_of_business") == "B2C Large": conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') - AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit)) + AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit)) elif self.filters.get("type_of_business") == "B2C Small": conditions += """ AND ( @@ -236,8 +236,8 @@ class Gstr1Report(object): elif self.filters.get("type_of_business") == "CDNR-UNREG": b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') - AND ABS(grand_total) > {0} AND (is_return = 1 OR is_debit_note = 1) - AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""".format(flt(b2c_limit)) + AND (is_return = 1 OR is_debit_note = 1) + AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""" elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ From abd3aee5b518bb0d9001dc457804b45e0930e54b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 15 Sep 2021 21:06:13 +0530 Subject: [PATCH 192/951] chore: change log for version 13.11.0 (#27527) --- erpnext/change_log/v13/v13_11_0.md | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 erpnext/change_log/v13/v13_11_0.md diff --git a/erpnext/change_log/v13/v13_11_0.md b/erpnext/change_log/v13/v13_11_0.md new file mode 100644 index 00000000000..d78c932d90e --- /dev/null +++ b/erpnext/change_log/v13/v13_11_0.md @@ -0,0 +1,45 @@ +# Version 13.11.0 Release Notes + +### Features & Enhancements + +- E-commerce Refactor ([#24603](https://github.com/frappe/erpnext/pull/24603)) +- Common party accounting ([#27039](https://github.com/frappe/erpnext/pull/27039)) +- Add provision for process loss in manufacturing. ([#26151](https://github.com/frappe/erpnext/pull/26151)) +- Taxjar Integration update ([#27143](https://github.com/frappe/erpnext/pull/27143)) +- Add Primary Address and Contact section in Supplier ([#27197](https://github.com/frappe/erpnext/pull/27197)) +- Color and Leave Type in leave application calendar ([#27246](https://github.com/frappe/erpnext/pull/27246)) +- Handle Asset on Issuing Credit Note ([#26159](https://github.com/frappe/erpnext/pull/26159)) +- Depreciate Asset after sale ([#26543](https://github.com/frappe/erpnext/pull/26543)) +- Treatment Plan Template ([#26557](https://github.com/frappe/erpnext/pull/26557)) +- Improve Product Bundle handling ([#27319](https://github.com/frappe/erpnext/pull/27124)) + +### Fixes + +- POS payment mode selection issue ([#27409](https://github.com/frappe/erpnext/pull/27409)) +- Customers 'primary_address' not updated automatically ([#26799](https://github.com/frappe/erpnext/pull/26799)) +- Production Plan UX and validation message ([#27278](https://github.com/frappe/erpnext/pull/27278)) +- Job Card overlap unknown column `jc.employee` ([#27403](https://github.com/frappe/erpnext/pull/27403)) +- Stock Ageing report issues for serialized items ([#27228](https://github.com/frappe/erpnext/pull/27228)) +- Shopping Cart and Variant Selection ([#27508](https://github.com/frappe/erpnext/pull/27508)) +- Dont fetch Stopped/Cancelled MRs in Stock Entry Get Items dialog ([#27326](https://github.com/frappe/erpnext/pull/27326)) +- Incorrect component amount calculation if dependent on another payment days based component ([#27349](https://github.com/frappe/erpnext/pull/27349)) +- Stripe's Price API for plan-price information ([#26107](https://github.com/frappe/erpnext/pull/26107)) +- Correct company address not getting copied from Purchase Order to Invoice ([#27217](https://github.com/frappe/erpnext/pull/27217)) +- Don't allow BOM's item code at any level of child items ([#27176](https://github.com/frappe/erpnext/pull/27176)) +- Handle Excess/Multiple Item Transfer against Job Card ([#27486](https://github.com/frappe/erpnext/pull/27486)) +- Fixed issue with accessing last salary slip for new employee ([#27247](https://github.com/frappe/erpnext/pull/27247)) +- Org Chart fixes ([#27290](https://github.com/frappe/erpnext/pull/27290)) +- Calculate operating cost based on BOM Quantity ([#27464](https://github.com/frappe/erpnext/pull/27464)) +- Healthcare Service Unit fixes ([#27273](https://github.com/frappe/erpnext/pull/27273)) +- Presentation currency conversion in reports ([#27316](https://github.com/frappe/erpnext/pull/27316)) +- Added delivery date filters to get sales orders in production plan ([#27367](https://github.com/frappe/erpnext/pull/27367)) +- Manually added weight per unit reset to zero after save ([#27330](https://github.com/frappe/erpnext/pull/27330)) +- Allow to change incoming rate manually in case of stand-alone credit note ([#27036](https://github.com/frappe/erpnext/pull/27036)) +- Cannot reconcile bank transactions against internal transfer payment entries ([#26932](https://github.com/frappe/erpnext/pull/26932)) +- Added item price to default price list ([#27353](https://github.com/frappe/erpnext/pull/27353)) +- Expense Claim reimbursed amount update issue ([#27204](https://github.com/frappe/erpnext/pull/27204)) +- Braintree payment processed twice ([#27300](https://github.com/frappe/erpnext/pull/27300)) +- Fetch from more than one sales order in Maintenance Visit ([#26924](https://github.com/frappe/erpnext/pull/26924)) +- Values with same account name and different account number in consolidated balance sheet report ([#27493](https://github.com/frappe/erpnext/pull/27493)) +- Don't create inward SLE against SI unless is internal customer enabled ([#27086](https://github.com/frappe/erpnext/pull/27086)) +- Paging and Discount filter ([#27332](https://github.com/frappe/erpnext/pull/27332)) \ No newline at end of file From 4c51002cb2ce22f7de0da9faa5b305f6f391b957 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 15 Sep 2021 21:06:31 +0530 Subject: [PATCH 193/951] fix: not able to submit stock entry with 350 items (#27523) (#27525) (cherry picked from commit e6a1ad8016b5e2aa425661978b99bc09c7ca08d1) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8a501a8a5b8..8e364a5062e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -399,7 +399,8 @@ class update_entries_after(object): return # Get dynamic incoming/outgoing rate - self.get_dynamic_incoming_outgoing_rate(sle) + if not self.args.get("sle_id"): + self.get_dynamic_incoming_outgoing_rate(sle) if sle.serial_no: self.get_serialized_values(sle) @@ -439,7 +440,8 @@ class update_entries_after(object): sle.doctype="Stock Ledger Entry" frappe.get_doc(sle).db_update() - self.update_outgoing_rate_on_transaction(sle) + if not self.args.get("sle_id"): + self.update_outgoing_rate_on_transaction(sle) def validate_negative_stock(self, sle): """ From fb55b57f5c5e240efd4a0ff6c42e2a7a70735a95 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 15 Sep 2021 21:31:52 +0550 Subject: [PATCH 194/951] bumped to version 13.11.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 22803c5ef20..ee476f41e20 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -7,7 +7,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.10.2' +__version__ = '13.11.0' def get_default_company(user=None): '''Get default company for user''' From bd1c823aa648c429f6bbde3291e6c701ba918bca Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 17 Sep 2021 13:21:33 +0530 Subject: [PATCH 195/951] fix: unecessary keyword args were passed in mapper functions (#27563) (#27564) (cherry picked from commit e03d9aa8890680baefe0d335dafdbfc5d0445fd4) Co-authored-by: Saqib --- erpnext/public/js/utils.js | 7 +++++-- erpnext/stock/doctype/material_request/material_request.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index ee8a516a148..e1cef614a22 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -714,12 +714,15 @@ erpnext.utils.map_current_doc = function(opts) { child_columns: opts.child_columns, action: function(selections, args) { let values = selections; - if(values.length === 0){ + if (values.length === 0) { frappe.msgprint(__("Please select {0}", [opts.source_doctype])) return; } opts.source_name = values; - opts.args = args; + if (opts.allow_child_item_selection) { + // args contains filtered child docnames + opts.args = args; + } d.dialog.hide(); _map(); }, diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 2569c04251c..cf98b19e7a1 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -272,8 +272,9 @@ def update_status(name, status): material_request.update_status(status) @frappe.whitelist() -def make_purchase_order(source_name, target_doc=None, args={}): - +def make_purchase_order(source_name, target_doc=None, args=None): + if args is None: + args = {} if isinstance(args, string_types): args = json.loads(args) From a741fd1cfe3e23536c1e57a93e2fade49b1768db Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Sat, 18 Sep 2021 13:24:33 +0530 Subject: [PATCH 196/951] fix: PO/PINV - Check if doctype has company_address field before setting the value (#27441) (#27575) Co-authored-by: Vama Mehta (cherry picked from commit 666eaae6ce976c5d820b3b9f91d23a0ed28a263a) Co-authored-by: vama --- erpnext/public/js/controllers/transaction.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 6c1d5f9898e..d4f5cb85ceb 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -864,7 +864,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if (r.message) { me.frm.set_value("billing_address", r.message); } else { - me.frm.set_value("company_address", ""); + if (frappe.meta.get_docfield(me.frm.doctype, 'company_address')) { + me.frm.set_value("company_address", ""); + } } } }); From 5978286b522d89d1433bddfc88adc66f78334df2 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Sat, 18 Sep 2021 15:25:44 +0530 Subject: [PATCH 197/951] fix: Handle `is_search_module_loaded` for redis version < 4.0.0 (#27574) (#27578) - Return False if error occurs (cherry picked from commit d6ed6d53e9cac2f65cd0fbb067ba8cf8cc5e2ef1) Co-authored-by: Marica --- erpnext/e_commerce/redisearch.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/e_commerce/redisearch.py b/erpnext/e_commerce/redisearch.py index 5cfb5ae2920..59c7f32fd46 100644 --- a/erpnext/e_commerce/redisearch.py +++ b/erpnext/e_commerce/redisearch.py @@ -20,14 +20,16 @@ def get_indexable_web_fields(): return [df.fieldname for df in valid_fields] def is_search_module_loaded(): - cache = frappe.cache() - out = cache.execute_command('MODULE LIST') + try: + cache = frappe.cache() + out = cache.execute_command('MODULE LIST') - parsed_output = " ".join( - (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out) - ) - - return "search" in parsed_output + parsed_output = " ".join( + (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out) + ) + return "search" in parsed_output + except Exception: + return False def if_redisearch_loaded(function): "Decorator to check if Redisearch is loaded." From de8b3570f53027f2dfa0307beba9d400519b7c79 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 19 Sep 2021 14:59:45 +0550 Subject: [PATCH 198/951] bumped to version 13.11.1 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index ee476f41e20..e23df3a63e5 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -7,7 +7,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.11.0' +__version__ = '13.11.1' def get_default_company(user=None): '''Get default company for user''' From f572a4e0e5ca7c46d7f20cff6d46b6271400d0d5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Sep 2021 18:44:55 +0530 Subject: [PATCH 199/951] fix(Org Chart): use attribute selectors instead of ID selector for node IDs with special characters (#27717) (#27719) * fix(Org Chart): use attribute selectors instead of ID selector for node IDs with special chars * fix: UI tests (cherry picked from commit 9e08229b7bdcb5bf63146c7effe1e757e862416e) Co-authored-by: Rucha Mahabal --- .../test_organizational_chart_desktop.js | 2 +- .../test_organizational_chart_mobile.js | 2 +- .../hierarchy_chart_desktop.js | 24 +++++++++---------- .../hierarchy_chart/hierarchy_chart_mobile.js | 22 ++++++++--------- erpnext/tests/ui_test_helpers.py | 2 ++ 5 files changed, 27 insertions(+), 25 deletions(-) diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js index 39b00d32635..79e08b3bbad 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -6,7 +6,7 @@ context('Organizational Chart', () => { it('navigates to org chart', () => { cy.visit('/app'); - cy.awesomebar('Organizational Chart'); + cy.visit('/app/organizational-chart'); cy.url().should('include', '/organizational-chart'); cy.window().its('frappe.csrf_token').then(csrf_token => { diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js index 6e751513967..161fae098a2 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -7,7 +7,7 @@ context('Organizational Chart Mobile', () => { it('navigates to org chart', () => { cy.viewport(375, 667); cy.visit('/app'); - cy.awesomebar('Organizational Chart'); + cy.visit('/app/organizational-chart'); cy.url().should('include', '/organizational-chart'); cy.window().its('frappe.csrf_token').then(csrf_token => { diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 62867327537..7b358195c3e 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -63,7 +63,7 @@ erpnext.HierarchyChart = class { }); node.parent.append(node_card); - node.$link = $(`#${node.id}`); + node.$link = $(`[id="${node.id}"]`); } show() { @@ -223,7 +223,7 @@ erpnext.HierarchyChart = class { let node = undefined; $.each(r.message, (_i, data) => { - if ($(`#${data.id}`).length) + if ($(`[id="${data.id}"]`).length) return; node = new me.Node({ @@ -263,7 +263,7 @@ erpnext.HierarchyChart = class { this.refresh_connectors(node.parent_id); // rebuild incoming connections - let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent'); this.refresh_connectors(grandparent); } @@ -282,7 +282,7 @@ erpnext.HierarchyChart = class { show_active_path(node) { // mark node parent on active path - $(`#${node.parent_id}`).addClass('active-path'); + $(`[id="${node.parent_id}"]`).addClass('active-path'); } load_children(node, deep=false) { @@ -317,7 +317,7 @@ erpnext.HierarchyChart = class { render_child_nodes(node, child_nodes) { const last_level = this.$hierarchy.find('.level:last').index(); - const current_level = $(`#${node.id}`).parent().parent().parent().index(); + const current_level = $(`[id="${node.id}"]`).parent().parent().parent().index(); if (last_level === current_level) { this.$hierarchy.append(` @@ -382,7 +382,7 @@ erpnext.HierarchyChart = class { node.$children = $('
    '); const last_level = this.$hierarchy.find('.level:last').index(); - const node_level = $(`#${node.id}`).parent().parent().parent().index(); + const node_level = $(`[id="${node.id}"]`).parent().parent().parent().index(); if (last_level === node_level) { this.$hierarchy.append(` @@ -489,7 +489,7 @@ erpnext.HierarchyChart = class { set_path_attributes(path, parent_id, child_id) { path.setAttribute("data-parent", parent_id); path.setAttribute("data-child", child_id); - const parent = $(`#${parent_id}`); + const parent = $(`[id="${parent_id}"]`); if (parent.hasClass('active')) { path.setAttribute("class", "active-connector"); @@ -513,7 +513,7 @@ erpnext.HierarchyChart = class { } collapse_previous_level_nodes(node) { - let node_parent = $(`#${node.parent_id}`); + let node_parent = $(`[id="${node.parent_id}"]`); let previous_level_nodes = node_parent.parent().parent().children('li'); let node_card = undefined; @@ -545,7 +545,7 @@ erpnext.HierarchyChart = class { setup_node_click_action(node) { let me = this; - let node_element = $(`#${node.id}`); + let node_element = $(`[id="${node.id}"]`); node_element.click(function() { const is_sibling = me.selected_node.parent_id === node.parent_id; @@ -563,7 +563,7 @@ erpnext.HierarchyChart = class { } setup_edit_node_action(node) { - let node_element = $(`#${node.id}`); + let node_element = $(`[id="${node.id}"]`); let me = this; node_element.find('.btn-edit-node').click(function() { @@ -572,7 +572,7 @@ erpnext.HierarchyChart = class { } remove_levels_after_node(node) { - let level = $(`#${node.id}`).parent().parent().parent().index(); + let level = $(`[id="${node.id}"]`).parent().parent().parent().index(); level = $('.hierarchy > li:eq('+ level + ')'); level.nextAll('li').remove(); @@ -595,7 +595,7 @@ erpnext.HierarchyChart = class { const parent = $(path).data('parent'); const child = $(path).data('child'); - if ($(`#${parent}`).length && $(`#${child}`).length) + if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length) return; $(path).remove(); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index b1a88795572..0a8ba78f643 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -54,7 +54,7 @@ erpnext.HierarchyChartMobile = class { }); node.parent.append(node_card); - node.$link = $(`#${node.id}`); + node.$link = $(`[id="${node.id}"]`); node.$link.addClass('mobile-node'); } @@ -184,7 +184,7 @@ erpnext.HierarchyChartMobile = class { this.refresh_connectors(node.parent_id, node.id); // rebuild incoming connections of parent - let grandparent = $(`#${node.parent_id}`).attr('data-parent'); + let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent'); this.refresh_connectors(grandparent, node.parent_id); } @@ -221,7 +221,7 @@ erpnext.HierarchyChartMobile = class { show_active_path(node) { // mark node parent on active path - $(`#${node.parent_id}`).addClass('active-path'); + $(`[id="${node.parent_id}"]`).addClass('active-path'); } load_children(node) { @@ -256,7 +256,7 @@ erpnext.HierarchyChartMobile = class { if (child_nodes) { $.each(child_nodes, (_i, data) => { this.add_node(node, data); - $(`#${data.id}`).addClass('active-child'); + $(`[id="${data.id}"]`).addClass('active-child'); setTimeout(() => { this.add_connector(node.id, data.id); @@ -293,9 +293,9 @@ erpnext.HierarchyChartMobile = class { let connector = undefined; - if ($(`#${parent_id}`).hasClass('active')) { + if ($(`[id="${parent_id}"]`).hasClass('active')) { connector = this.get_connector_for_active_node(parent_node, child_node); - } else if ($(`#${parent_id}`).hasClass('active-path')) { + } else if ($(`[id="${parent_id}"]`).hasClass('active-path')) { connector = this.get_connector_for_collapsed_node(parent_node, child_node); } @@ -351,7 +351,7 @@ erpnext.HierarchyChartMobile = class { set_path_attributes(path, parent_id, child_id) { path.setAttribute("data-parent", parent_id); path.setAttribute("data-child", child_id); - const parent = $(`#${parent_id}`); + const parent = $(`[id="${parent_id}"]`); if (parent.hasClass('active')) { path.setAttribute("class", "active-connector"); @@ -374,7 +374,7 @@ erpnext.HierarchyChartMobile = class { setup_node_click_action(node) { let me = this; - let node_element = $(`#${node.id}`); + let node_element = $(`[id="${node.id}"]`); node_element.click(function() { let el = undefined; @@ -398,7 +398,7 @@ erpnext.HierarchyChartMobile = class { } setup_edit_node_action(node) { - let node_element = $(`#${node.id}`); + let node_element = $(`[id="${node.id}"]`); let me = this; node_element.find('.btn-edit-node').click(function() { @@ -512,7 +512,7 @@ erpnext.HierarchyChartMobile = class { } remove_levels_after_node(node) { - let level = $(`#${node.id}`).parent().parent().index(); + let level = $(`[id="${node.id}"]`).parent().parent().index(); level = $('.hierarchy-mobile > li:eq('+ level + ')'); level.nextAll('li').remove(); @@ -533,7 +533,7 @@ erpnext.HierarchyChartMobile = class { const parent = $(path).data('parent'); const child = $(path).data('child'); - if ($(`#${parent}`).length && $(`#${child}`).length) + if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length) return; $(path).remove(); diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py index 76c7608c91f..9c8c371e051 100644 --- a/erpnext/tests/ui_test_helpers.py +++ b/erpnext/tests/ui_test_helpers.py @@ -7,6 +7,8 @@ def create_employee_records(): create_company() create_missing_designation() + frappe.db.sql("DELETE FROM tabEmployee WHERE company='Test Org Chart'") + emp1 = create_employee('Test Employee 1', 'CEO') emp2 = create_employee('Test Employee 2', 'CTO') emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1) From 3e7a0298697ca2ee2410eb3b681d96993c31915d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 16:50:05 +0530 Subject: [PATCH 200/951] fix: option to limit reposting in certain timeslot (bp #27725) (cherry picked from commit a04f9c904e3d2b4d44c3b3ad750e9fddca552296) Co-authored-by: Ankush Menat --- .../repost_item_valuation.py | 28 +++++++- .../test_repost_item_valuation.py | 71 ++++++++++++++++-- .../stock_reposting_settings/__init__.py | 0 .../stock_reposting_settings.js | 8 +++ .../stock_reposting_settings.json | 72 +++++++++++++++++++ .../stock_reposting_settings.py | 28 ++++++++ .../test_stock_reposting_settings.py | 9 +++ 7 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 erpnext/stock/doctype/stock_reposting_settings/__init__.py create mode 100644 erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js create mode 100644 erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json create mode 100644 erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py create mode 100644 erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py 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 5f97798974c..8f3ae23dcef 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form, now, today +from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException @@ -126,6 +126,9 @@ def notify_error_to_stock_managers(doc, traceback): frappe.sendmail(recipients=recipients, subject=subject, message=message) def repost_entries(): + if not in_configured_timeslot(): + return + riv_entries = get_repost_item_valuation_entries() for row in riv_entries: @@ -144,3 +147,26 @@ def get_repost_item_valuation_entries(): WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc """, now(), as_dict=1) + + +def in_configured_timeslot(repost_settings=None, current_time=None): + """Check if current time is in configured timeslot for reposting.""" + + if repost_settings is None: + repost_settings = frappe.get_cached_doc("Stock Reposting Settings") + + if not repost_settings.limit_reposting_timeslot: + return True + + if get_weekday() == repost_settings.limits_dont_apply_on: + return True + + start_time = repost_settings.start_time + end_time = repost_settings.end_time + + now_time = current_time or nowtime() + + if start_time < end_time: + return end_time >= now_time >= start_time + else: + return now_time >= start_time or now_time <= end_time 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 c70a9ec7a8b..c086f938b5d 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 @@ -1,11 +1,72 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -# import frappe import unittest +import frappe + +from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( + in_configured_timeslot, +) + class TestRepostItemValuation(unittest.TestCase): - pass + def test_repost_time_slot(self): + repost_settings = frappe.get_doc("Stock Reposting Settings") + + positive_cases = [ + {"limit_reposting_timeslot": 0}, + { + "limit_reposting_timeslot": 1, + "start_time": "18:00:00", + "end_time": "09:00:00", + "current_time": "20:00:00", + }, + { + "limit_reposting_timeslot": 1, + "start_time": "09:00:00", + "end_time": "18:00:00", + "current_time": "12:00:00", + }, + { + "limit_reposting_timeslot": 1, + "start_time": "23:00:00", + "end_time": "09:00:00", + "current_time": "2:00:00", + }, + ] + + for case in positive_cases: + repost_settings.update(case) + self.assertTrue( + in_configured_timeslot(repost_settings, case.get("current_time")), + msg=f"Exepcted true from : {case}", + ) + + negative_cases = [ + { + "limit_reposting_timeslot": 1, + "start_time": "18:00:00", + "end_time": "09:00:00", + "current_time": "09:01:00", + }, + { + "limit_reposting_timeslot": 1, + "start_time": "09:00:00", + "end_time": "18:00:00", + "current_time": "19:00:00", + }, + { + "limit_reposting_timeslot": 1, + "start_time": "23:00:00", + "end_time": "09:00:00", + "current_time": "22:00:00", + }, + ] + + for case in negative_cases: + repost_settings.update(case) + self.assertFalse( + in_configured_timeslot(repost_settings, case.get("current_time")), + msg=f"Exepcted false from : {case}", + ) diff --git a/erpnext/stock/doctype/stock_reposting_settings/__init__.py b/erpnext/stock/doctype/stock_reposting_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js new file mode 100644 index 00000000000..42d0723d427 --- /dev/null +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Stock Reposting Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json new file mode 100644 index 00000000000..24740590037 --- /dev/null +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2021-10-01 10:56:30.814787", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "scheduling_section", + "limit_reposting_timeslot", + "start_time", + "end_time", + "limits_dont_apply_on" + ], + "fields": [ + { + "fieldname": "scheduling_section", + "fieldtype": "Section Break", + "label": "Scheduling" + }, + { + "depends_on": "limit_reposting_timeslot", + "fieldname": "start_time", + "fieldtype": "Time", + "label": "Start Time", + "mandatory_depends_on": "limit_reposting_timeslot" + }, + { + "depends_on": "limit_reposting_timeslot", + "fieldname": "end_time", + "fieldtype": "Time", + "label": "End Time", + "mandatory_depends_on": "limit_reposting_timeslot" + }, + { + "depends_on": "limit_reposting_timeslot", + "fieldname": "limits_dont_apply_on", + "fieldtype": "Select", + "label": "Limits don't apply on", + "options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday" + }, + { + "default": "0", + "fieldname": "limit_reposting_timeslot", + "fieldtype": "Check", + "label": "Limit timeslot for Stock Reposting" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-10-01 11:27:28.981594", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Reposting Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py new file mode 100644 index 00000000000..bab521d69fc --- /dev/null +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -0,0 +1,28 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from frappe.model.document import Document +from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours + + +class StockRepostingSettings(Document): + + + def validate(self): + self.set_minimum_reposting_time_slot() + + def set_minimum_reposting_time_slot(self): + """Ensure that timeslot for reposting is at least 12 hours.""" + if not self.limit_reposting_timeslot: + return + + start_time = get_datetime(self.start_time) + end_time = get_datetime(self.end_time) + + if start_time > end_time: + end_time = add_to_date(end_time, days=1, as_datetime=True) + + diff = time_diff_in_hours(end_time, start_time) + + if diff < 10: + self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True)) diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py new file mode 100644 index 00000000000..fad74d355cf --- /dev/null +++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + + +class TestStockRepostingSettings(unittest.TestCase): + pass From 2c9162160aaf2da577ef4afb930d987cd15355c4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 20 Sep 2021 20:52:08 +0530 Subject: [PATCH 201/951] fix: Creating unique hash for slider id instead of slider name (cherry picked from commit 3e8e6ac4e2f78e4030fb71ba10403c3a018935ba) --- .../web_template/hero_slider/hero_slider.html | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html index 1e3d0d069a1..2cff4b3707d 100644 --- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html +++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html @@ -27,12 +27,14 @@
    {%- endmacro -%} -" @@ -751,49 +852,53 @@ def render_doc_as_html(doctype, docname, exclude_fields = None): {0} {1}
    - """.format(section_html, html) + """.format( + section_html, html + ) return {"html": doc_html} def update_address_links(address, method): - ''' + """ Hook validate Address If Patient is linked in Address, also link the associated Customer - ''' - if 'Healthcare' not in frappe.get_active_domains(): + """ + if "Healthcare" not in frappe.get_active_domains(): return - patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', address.links)) + patient_links = list(filter(lambda link: link.get("link_doctype") == "Patient", address.links)) for link in patient_links: - customer = frappe.db.get_value('Patient', link.get('link_name'), 'customer') - if customer and not address.has_link('Customer', customer): - address.append('links', dict(link_doctype = 'Customer', link_name = customer)) + customer = frappe.db.get_value("Patient", link.get("link_name"), "customer") + if customer and not address.has_link("Customer", customer): + address.append("links", dict(link_doctype="Customer", link_name=customer)) def update_patient_email_and_phone_numbers(contact, method): - ''' + """ Hook validate Contact Update linked Patients' primary mobile and phone numbers - ''' - if 'Healthcare' not in frappe.get_active_domains() or contact.flags.skip_patient_update: + """ + if "Healthcare" not in frappe.get_active_domains() or contact.flags.skip_patient_update: return if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone): - patient_links = list(filter(lambda link: link.get('link_doctype') == 'Patient', contact.links)) + patient_links = list(filter(lambda link: link.get("link_doctype") == "Patient", contact.links)) for link in patient_links: - contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1) + contact_details = frappe.db.get_value( + "Patient", link.get("link_name"), ["email", "mobile", "phone"], as_dict=1 + ) new_contact_details = {} - if contact.email_id and contact.email_id != contact_details.get('email'): - new_contact_details.update({'email': contact.email_id}) - if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'): - new_contact_details.update({'mobile': contact.mobile_no}) - if contact.phone and contact.phone != contact_details.get('phone'): - new_contact_details.update({'phone': contact.phone}) + if contact.email_id and contact.email_id != contact_details.get("email"): + new_contact_details.update({"email": contact.email_id}) + if contact.mobile_no and contact.mobile_no != contact_details.get("mobile"): + new_contact_details.update({"mobile": contact.mobile_no}) + if contact.phone and contact.phone != contact_details.get("phone"): + new_contact_details.update({"phone": contact.phone}) if new_contact_details: - frappe.db.set_value('Patient', link.get('link_name'), new_contact_details) + frappe.db.set_value("Patient", link.get("link_name"), new_contact_details) diff --git a/erpnext/healthcare/web_form/lab_test/lab_test.py b/erpnext/healthcare/web_form/lab_test/lab_test.py index 94ffbcfa2f2..1bccbd4f4e7 100644 --- a/erpnext/healthcare/web_form/lab_test/lab_test.py +++ b/erpnext/healthcare/web_form/lab_test/lab_test.py @@ -1,22 +1,31 @@ - import frappe def get_context(context): context.read_only = 1 + def get_list_context(context): context.row_template = "erpnext/templates/includes/healthcare/lab_test_row_template.html" context.get_list = get_lab_test_list -def get_lab_test_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'): + +def get_lab_test_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc" +): patient = get_patient() - lab_tests = frappe.db.sql("""select * from `tabLab Test` - where patient = %s order by result_date""", patient, as_dict = True) + lab_tests = frappe.db.sql( + """select * from `tabLab Test` + where patient = %s order by result_date""", + patient, + as_dict=True, + ) return lab_tests + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.patient == get_patient(): diff --git a/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py b/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py index 9f0903cece7..89e8eb168f3 100644 --- a/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py +++ b/erpnext/healthcare/web_form/patient_appointments/patient_appointments.py @@ -1,22 +1,31 @@ - import frappe def get_context(context): context.read_only = 1 + def get_list_context(context): context.row_template = "erpnext/templates/includes/healthcare/appointment_row_template.html" context.get_list = get_appointment_list -def get_appointment_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'): + +def get_appointment_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc" +): patient = get_patient() - lab_tests = frappe.db.sql("""select * from `tabPatient Appointment` - where patient = %s and (status = 'Open' or status = 'Scheduled') order by appointment_date""", patient, as_dict = True) + lab_tests = frappe.db.sql( + """select * from `tabPatient Appointment` + where patient = %s and (status = 'Open' or status = 'Scheduled') order by appointment_date""", + patient, + as_dict=True, + ) return lab_tests + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.patient == get_patient(): diff --git a/erpnext/healthcare/web_form/patient_registration/patient_registration.py b/erpnext/healthcare/web_form/patient_registration/patient_registration.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/healthcare/web_form/patient_registration/patient_registration.py +++ b/erpnext/healthcare/web_form/patient_registration/patient_registration.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/healthcare/web_form/personal_details/personal_details.py b/erpnext/healthcare/web_form/personal_details/personal_details.py index fc8e8c0a5b7..dfbd73eeec1 100644 --- a/erpnext/healthcare/web_form/personal_details/personal_details.py +++ b/erpnext/healthcare/web_form/personal_details/personal_details.py @@ -1,23 +1,25 @@ - import frappe from frappe import _ no_cache = 1 + def get_context(context): - if frappe.session.user=='Guest': + if frappe.session.user == "Guest": frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) - context.show_sidebar=True + context.show_sidebar = True - if frappe.db.exists("Patient", {'email': frappe.session.user}): - patient = frappe.get_doc("Patient", {'email': frappe.session.user}) + if frappe.db.exists("Patient", {"email": frappe.session.user}): + patient = frappe.get_doc("Patient", {"email": frappe.session.user}) context.doc = patient frappe.form_dict.new = 0 frappe.form_dict.name = patient.name + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.name == get_patient(): diff --git a/erpnext/healthcare/web_form/prescription/prescription.py b/erpnext/healthcare/web_form/prescription/prescription.py index 0e1bc3d5dd6..44caeb00b59 100644 --- a/erpnext/healthcare/web_form/prescription/prescription.py +++ b/erpnext/healthcare/web_form/prescription/prescription.py @@ -1,22 +1,31 @@ - import frappe def get_context(context): context.read_only = 1 + def get_list_context(context): context.row_template = "erpnext/templates/includes/healthcare/prescription_row_template.html" context.get_list = get_encounter_list -def get_encounter_list(doctype, txt, filters, limit_start, limit_page_length = 20, order_by='modified desc'): + +def get_encounter_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified desc" +): patient = get_patient() - encounters = frappe.db.sql("""select * from `tabPatient Encounter` - where patient = %s order by creation desc""", patient, as_dict = True) + encounters = frappe.db.sql( + """select * from `tabPatient Encounter` + where patient = %s order by creation desc""", + patient, + as_dict=True, + ) return encounters + def get_patient(): - return frappe.get_value("Patient",{"email": frappe.session.user}, "name") + return frappe.get_value("Patient", {"email": frappe.session.user}, "name") + def has_website_permission(doc, ptype, user, verbose=False): if doc.patient == get_patient(): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 00e46393590..906eb10c64f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -1,4 +1,3 @@ - from frappe import _ app_name = "erpnext" @@ -13,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext" app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" -develop_version = '13.x.x-develop' +develop_version = "13.x.x-develop" app_include_js = "/assets/js/erpnext.min.js" app_include_css = "/assets/css/erpnext.css" @@ -25,12 +24,10 @@ doctype_js = { "Communication": "public/js/communication.js", "Event": "public/js/event.js", "Newsletter": "public/js/newsletter.js", - "Contact": "public/js/contact.js" + "Contact": "public/js/contact.js", } -override_doctype_class = { - 'Address': 'erpnext.accounts.custom.address.ERPNextAddress' -} +override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} welcome_email = "erpnext.setup.utils.welcome_email" @@ -51,146 +48,266 @@ additional_print_settings = "erpnext.controllers.print_settings.get_print_settin on_session_creation = [ "erpnext.portal.utils.create_customer_or_supplier", - "erpnext.e_commerce.shopping_cart.utils.set_cart_count" + "erpnext.e_commerce.shopping_cart.utils.set_cart_count", ] on_logout = "erpnext.e_commerce.shopping_cart.utils.clear_cart_count" -treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Group', 'Sales Person', 'Territory', 'Assessment Group', 'Department'] +treeviews = [ + "Account", + "Cost Center", + "Warehouse", + "Item Group", + "Customer Group", + "Sales Person", + "Territory", + "Assessment Group", + "Department", +] # website -update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"] +update_website_context = [ + "erpnext.e_commerce.shopping_cart.utils.update_website_context", + "erpnext.education.doctype.education_settings.education_settings.update_website_context", +] my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context" webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context" -calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"] +calendars = [ + "Task", + "Work Order", + "Leave Application", + "Sales Order", + "Holiday List", + "Course Schedule", +] domains = { - 'Agriculture': 'erpnext.domains.agriculture', - 'Distribution': 'erpnext.domains.distribution', - 'Education': 'erpnext.domains.education', - 'Healthcare': 'erpnext.domains.healthcare', - 'Hospitality': 'erpnext.domains.hospitality', - 'Manufacturing': 'erpnext.domains.manufacturing', - 'Non Profit': 'erpnext.domains.non_profit', - 'Retail': 'erpnext.domains.retail', - 'Services': 'erpnext.domains.services', + "Agriculture": "erpnext.domains.agriculture", + "Distribution": "erpnext.domains.distribution", + "Education": "erpnext.domains.education", + "Healthcare": "erpnext.domains.healthcare", + "Hospitality": "erpnext.domains.hospitality", + "Manufacturing": "erpnext.domains.manufacturing", + "Non Profit": "erpnext.domains.non_profit", + "Retail": "erpnext.domains.retail", + "Services": "erpnext.domains.services", } -website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner", - "Job Opening", "Student Admission"] +website_generators = [ + "Item Group", + "Website Item", + "BOM", + "Sales Partner", + "Job Opening", + "Student Admission", +] website_context = { - "favicon": "/assets/erpnext/images/erpnext-favicon.svg", - "splash_image": "/assets/erpnext/images/erpnext-logo.svg" + "favicon": "/assets/erpnext/images/erpnext-favicon.svg", + "splash_image": "/assets/erpnext/images/erpnext-logo.svg", } website_route_rules = [ {"from_route": "/orders", "to_route": "Sales Order"}, - {"from_route": "/orders/", "to_route": "order", - "defaults": { - "doctype": "Sales Order", - "parents": [{"label": _("Orders"), "route": "orders"}] - } + { + "from_route": "/orders/", + "to_route": "order", + "defaults": {"doctype": "Sales Order", "parents": [{"label": _("Orders"), "route": "orders"}]}, }, {"from_route": "/invoices", "to_route": "Sales Invoice"}, - {"from_route": "/invoices/", "to_route": "order", + { + "from_route": "/invoices/", + "to_route": "order", "defaults": { "doctype": "Sales Invoice", - "parents": [{"label": _("Invoices"), "route": "invoices"}] - } + "parents": [{"label": _("Invoices"), "route": "invoices"}], + }, }, {"from_route": "/supplier-quotations", "to_route": "Supplier Quotation"}, - {"from_route": "/supplier-quotations/", "to_route": "order", + { + "from_route": "/supplier-quotations/", + "to_route": "order", "defaults": { "doctype": "Supplier Quotation", - "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}] - } + "parents": [{"label": _("Supplier Quotation"), "route": "supplier-quotations"}], + }, }, {"from_route": "/purchase-orders", "to_route": "Purchase Order"}, - {"from_route": "/purchase-orders/", "to_route": "order", + { + "from_route": "/purchase-orders/", + "to_route": "order", "defaults": { "doctype": "Purchase Order", - "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}] - } + "parents": [{"label": _("Purchase Order"), "route": "purchase-orders"}], + }, }, {"from_route": "/purchase-invoices", "to_route": "Purchase Invoice"}, - {"from_route": "/purchase-invoices/", "to_route": "order", + { + "from_route": "/purchase-invoices/", + "to_route": "order", "defaults": { "doctype": "Purchase Invoice", - "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}] - } + "parents": [{"label": _("Purchase Invoice"), "route": "purchase-invoices"}], + }, }, {"from_route": "/quotations", "to_route": "Quotation"}, - {"from_route": "/quotations/", "to_route": "order", + { + "from_route": "/quotations/", + "to_route": "order", "defaults": { "doctype": "Quotation", - "parents": [{"label": _("Quotations"), "route": "quotations"}] - } + "parents": [{"label": _("Quotations"), "route": "quotations"}], + }, }, {"from_route": "/shipments", "to_route": "Delivery Note"}, - {"from_route": "/shipments/", "to_route": "order", + { + "from_route": "/shipments/", + "to_route": "order", "defaults": { "doctype": "Delivery Note", - "parents": [{"label": _("Shipments"), "route": "shipments"}] - } + "parents": [{"label": _("Shipments"), "route": "shipments"}], + }, }, {"from_route": "/rfq", "to_route": "Request for Quotation"}, - {"from_route": "/rfq/", "to_route": "rfq", + { + "from_route": "/rfq/", + "to_route": "rfq", "defaults": { "doctype": "Request for Quotation", - "parents": [{"label": _("Request for Quotation"), "route": "rfq"}] - } + "parents": [{"label": _("Request for Quotation"), "route": "rfq"}], + }, }, {"from_route": "/addresses", "to_route": "Address"}, - {"from_route": "/addresses/", "to_route": "addresses", - "defaults": { - "doctype": "Address", - "parents": [{"label": _("Addresses"), "route": "addresses"}] - } + { + "from_route": "/addresses/", + "to_route": "addresses", + "defaults": {"doctype": "Address", "parents": [{"label": _("Addresses"), "route": "addresses"}]}, }, {"from_route": "/jobs", "to_route": "Job Opening"}, {"from_route": "/admissions", "to_route": "Student Admission"}, {"from_route": "/boms", "to_route": "BOM"}, {"from_route": "/timesheets", "to_route": "Timesheet"}, {"from_route": "/material-requests", "to_route": "Material Request"}, - {"from_route": "/material-requests/", "to_route": "material_request_info", + { + "from_route": "/material-requests/", + "to_route": "material_request_info", "defaults": { "doctype": "Material Request", - "parents": [{"label": _("Material Request"), "route": "material-requests"}] - } + "parents": [{"label": _("Material Request"), "route": "material-requests"}], + }, }, - {"from_route": "/project", "to_route": "Project"} + {"from_route": "/project", "to_route": "Project"}, ] standard_portal_menu_items = [ - {"title": _("Personal Details"), "route": "/personal-details", "reference_doctype": "Patient", "role": "Patient"}, + { + "title": _("Personal Details"), + "route": "/personal-details", + "reference_doctype": "Patient", + "role": "Patient", + }, {"title": _("Projects"), "route": "/project", "reference_doctype": "Project"}, - {"title": _("Request for Quotations"), "route": "/rfq", "reference_doctype": "Request for Quotation", "role": "Supplier"}, - {"title": _("Supplier Quotation"), "route": "/supplier-quotations", "reference_doctype": "Supplier Quotation", "role": "Supplier"}, - {"title": _("Purchase Orders"), "route": "/purchase-orders", "reference_doctype": "Purchase Order", "role": "Supplier"}, - {"title": _("Purchase Invoices"), "route": "/purchase-invoices", "reference_doctype": "Purchase Invoice", "role": "Supplier"}, - {"title": _("Quotations"), "route": "/quotations", "reference_doctype": "Quotation", "role":"Customer"}, - {"title": _("Orders"), "route": "/orders", "reference_doctype": "Sales Order", "role":"Customer"}, - {"title": _("Invoices"), "route": "/invoices", "reference_doctype": "Sales Invoice", "role":"Customer"}, - {"title": _("Shipments"), "route": "/shipments", "reference_doctype": "Delivery Note", "role":"Customer"}, - {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role":"Customer"}, + { + "title": _("Request for Quotations"), + "route": "/rfq", + "reference_doctype": "Request for Quotation", + "role": "Supplier", + }, + { + "title": _("Supplier Quotation"), + "route": "/supplier-quotations", + "reference_doctype": "Supplier Quotation", + "role": "Supplier", + }, + { + "title": _("Purchase Orders"), + "route": "/purchase-orders", + "reference_doctype": "Purchase Order", + "role": "Supplier", + }, + { + "title": _("Purchase Invoices"), + "route": "/purchase-invoices", + "reference_doctype": "Purchase Invoice", + "role": "Supplier", + }, + { + "title": _("Quotations"), + "route": "/quotations", + "reference_doctype": "Quotation", + "role": "Customer", + }, + { + "title": _("Orders"), + "route": "/orders", + "reference_doctype": "Sales Order", + "role": "Customer", + }, + { + "title": _("Invoices"), + "route": "/invoices", + "reference_doctype": "Sales Invoice", + "role": "Customer", + }, + { + "title": _("Shipments"), + "route": "/shipments", + "reference_doctype": "Delivery Note", + "role": "Customer", + }, + {"title": _("Issues"), "route": "/issues", "reference_doctype": "Issue", "role": "Customer"}, {"title": _("Addresses"), "route": "/addresses", "reference_doctype": "Address"}, - {"title": _("Timesheets"), "route": "/timesheets", "reference_doctype": "Timesheet", "role":"Customer"}, - {"title": _("Lab Test"), "route": "/lab-test", "reference_doctype": "Lab Test", "role":"Patient"}, - {"title": _("Prescription"), "route": "/prescription", "reference_doctype": "Patient Encounter", "role":"Patient"}, - {"title": _("Patient Appointment"), "route": "/patient-appointments", "reference_doctype": "Patient Appointment", "role":"Patient"}, - {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role":"Student"}, + { + "title": _("Timesheets"), + "route": "/timesheets", + "reference_doctype": "Timesheet", + "role": "Customer", + }, + { + "title": _("Lab Test"), + "route": "/lab-test", + "reference_doctype": "Lab Test", + "role": "Patient", + }, + { + "title": _("Prescription"), + "route": "/prescription", + "reference_doctype": "Patient Encounter", + "role": "Patient", + }, + { + "title": _("Patient Appointment"), + "route": "/patient-appointments", + "reference_doctype": "Patient Appointment", + "role": "Patient", + }, + {"title": _("Fees"), "route": "/fees", "reference_doctype": "Fees", "role": "Student"}, {"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"}, - {"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"}, - {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"}, - {"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"}, + { + "title": _("Admission"), + "route": "/admissions", + "reference_doctype": "Student Admission", + "role": "Student", + }, + { + "title": _("Certification"), + "route": "/certification", + "reference_doctype": "Certification Application", + "role": "Non Profit Portal User", + }, + { + "title": _("Material Request"), + "route": "/material-requests", + "reference_doctype": "Material Request", + "role": "Customer", + }, {"title": _("Appointment Booking"), "route": "/book_appointment"}, ] default_roles = [ - {'role': 'Customer', 'doctype':'Contact', 'email_field': 'email_id'}, - {'role': 'Supplier', 'doctype':'Contact', 'email_field': 'email_id'}, - {'role': 'Student', 'doctype':'Student', 'email_field': 'student_email_id'}, + {"role": "Customer", "doctype": "Contact", "email_field": "email_id"}, + {"role": "Supplier", "doctype": "Contact", "email_field": "email_id"}, + {"role": "Student", "doctype": "Student", "email_field": "student_email_id"}, ] sounds = [ @@ -198,9 +315,7 @@ sounds = [ {"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2}, ] -has_upload_permission = { - "Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission" -} +has_upload_permission = {"Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission"} has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", @@ -216,7 +331,7 @@ has_website_permission = { "Lab Test": "erpnext.healthcare.web_form.lab_test.lab_test.has_website_permission", "Patient Encounter": "erpnext.healthcare.web_form.prescription.prescription.has_website_permission", "Patient Appointment": "erpnext.healthcare.web_form.patient_appointments.patient_appointments.has_website_permission", - "Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission" + "Patient": "erpnext.healthcare.web_form.personal_details.personal_details.has_website_permission", } dump_report_map = "erpnext.startup.report_data_map.data_map" @@ -225,112 +340,115 @@ before_tests = "erpnext.setup.utils.before_tests" standard_queries = { "Customer": "erpnext.selling.doctype.customer.customer.get_customer_list", - "Healthcare Practitioner": "erpnext.healthcare.doctype.healthcare_practitioner.healthcare_practitioner.get_practitioner_list" + "Healthcare Practitioner": "erpnext.healthcare.doctype.healthcare_practitioner.healthcare_practitioner.get_practitioner_list", } doc_events = { "*": { "on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record", "on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record", - "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record" + "on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record", }, "Stock Entry": { "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", - "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty" + "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", }, "User": { "after_insert": "frappe.contacts.doctype.contact.contact.update_contact", "validate": "erpnext.hr.doctype.employee.employee.validate_employee_role", - "on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions", - "erpnext.portal.utils.set_default_role"] - }, - "Communication": { "on_update": [ - "erpnext.support.doctype.issue.issue.set_first_response_time" - ] + "erpnext.hr.doctype.employee.employee.update_user_permissions", + "erpnext.portal.utils.set_default_role", + ], }, + "Communication": {"on_update": ["erpnext.support.doctype.issue.issue.set_first_response_time"]}, "Sales Taxes and Charges Template": { "on_update": "erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings.validate_cart_settings" }, - "Tax Category": { - "validate": "erpnext.regional.india.utils.validate_tax_category" - }, + "Tax Category": {"validate": "erpnext.regional.india.utils.validate_tax_category"}, "Sales Invoice": { "on_submit": [ "erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit", "erpnext.regional.saudi_arabia.utils.create_qr_code", - "erpnext.erpnext_integrations.taxjar_integration.create_transaction" + "erpnext.erpnext_integrations.taxjar_integration.create_transaction", ], "on_cancel": [ "erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.erpnext_integrations.taxjar_integration.delete_transaction", - "erpnext.regional.saudi_arabia.utils.delete_qr_code_file" + "erpnext.regional.saudi_arabia.utils.delete_qr_code_file", ], "on_trash": "erpnext.regional.check_deletion_permission", "validate": [ "erpnext.regional.india.utils.validate_document_name", - "erpnext.regional.india.utils.update_taxable_values" - ] - }, - "POS Invoice": { - "on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"] + "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", "erpnext.regional.india.utils.update_itc_availed_fields", "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm", "erpnext.regional.united_arab_emirates.utils.validate_returns", - "erpnext.regional.india.utils.update_taxable_values" + "erpnext.regional.india.utils.update_taxable_values", ] }, "Payment Entry": { "validate": "erpnext.regional.india.utils.update_place_of_supply", - "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], - "on_trash": "erpnext.regional.check_deletion_permission" + "on_submit": [ + "erpnext.regional.create_transaction_log", + "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", + "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", + ], + "on_trash": "erpnext.regional.check_deletion_permission", }, - 'Address': { - 'validate': [ - 'erpnext.regional.india.utils.validate_gstin_for_india', - 'erpnext.regional.italy.utils.set_state_code', - 'erpnext.regional.india.utils.update_gst_category', - 'erpnext.healthcare.utils.update_address_links' + "Address": { + "validate": [ + "erpnext.regional.india.utils.validate_gstin_for_india", + "erpnext.regional.italy.utils.set_state_code", + "erpnext.regional.india.utils.update_gst_category", + "erpnext.healthcare.utils.update_address_links", ], }, - 'Supplier': { - 'validate': 'erpnext.regional.india.utils.validate_pan_for_india' - }, - ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { - 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] - }, + "Supplier": {"validate": "erpnext.regional.india.utils.validate_pan_for_india"}, + ( + "Sales Invoice", + "Sales Order", + "Delivery Note", + "Purchase Invoice", + "Purchase Order", + "Purchase Receipt", + ): {"validate": ["erpnext.regional.india.utils.set_place_of_supply"]}, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations", - "validate": ["erpnext.crm.utils.update_lead_phone_numbers", "erpnext.healthcare.utils.update_patient_email_and_phone_numbers"] + "validate": [ + "erpnext.crm.utils.update_lead_phone_numbers", + "erpnext.healthcare.utils.update_patient_email_and_phone_numbers", + ], }, "Email Unsubscribe": { "after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient" }, - ('Quotation', 'Sales Order', 'Sales Invoice'): { - 'validate': ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"] + ("Quotation", "Sales Order", "Sales Invoice"): { + "validate": ["erpnext.erpnext_integrations.taxjar_integration.set_sales_tax"] }, "Company": { - "on_trash": ["erpnext.regional.india.utils.delete_gst_settings_for_company", - "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"] + "on_trash": [ + "erpnext.regional.india.utils.delete_gst_settings_for_company", + "erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company", + ] }, "Integration Request": { "validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment" - } + }, } # On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled. # to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled. # if payment entry not in auto cancel exempted doctypes it will cancel payment entry. -auto_cancel_exempted_doctypes= [ - "Payment Entry", - "Inpatient Medication Entry" -] +auto_cancel_exempted_doctypes = ["Payment Entry", "Inpatient Medication Entry"] after_migrate = ["erpnext.setup.install.update_select_perm_after_install"] @@ -344,10 +462,10 @@ scheduler_events = { "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", "erpnext.hr.doctype.interview.interview.send_interview_reminder", - "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts" + "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts", ], "hourly": [ - 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', + "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails", "erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details", "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", @@ -356,7 +474,7 @@ scheduler_events = { "erpnext.projects.doctype.project.project.collect_project_status", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", - "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders" + "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", ], "hourly_long": [ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" @@ -389,7 +507,7 @@ scheduler_events = { "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", "erpnext.non_profit.doctype.membership.membership.set_expired_status", - "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder" + "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder", ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", @@ -399,18 +517,14 @@ scheduler_events = { "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", - "erpnext.crm.doctype.lead.lead.daily_open_lead" - ], - "weekly": [ - "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly" - ], - "monthly": [ - "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly" + "erpnext.crm.doctype.lead.lead.daily_open_lead", ], + "weekly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"], + "monthly": ["erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", - "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" - ] + "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans", + ], } email_brand_image = "assets/erpnext/images/erpnext-logo.jpg" @@ -429,63 +543,96 @@ get_translated_dict = { } bot_parsers = [ - 'erpnext.utilities.bot.FindItemBot', + "erpnext.utilities.bot.FindItemBot", ] -get_site_info = 'erpnext.utilities.get_site_info' +get_site_info = "erpnext.utilities.get_site_info" payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account" communication_doctypes = ["Customer", "Supplier"] -accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", - "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", - "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", - "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", - "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", - "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", - "Subscription Plan", "POS Invoice", "POS Invoice Item" +accounting_dimension_doctypes = [ + "GL Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Asset", + "Expense Claim", + "Expense Claim Detail", + "Expense Taxes and Charges", + "Stock Entry", + "Budget", + "Payroll Entry", + "Delivery Note", + "Sales Invoice Item", + "Purchase Invoice Item", + "Purchase Order Item", + "Journal Entry Account", + "Material Request Item", + "Delivery Note Item", + "Purchase Receipt Item", + "Stock Entry Detail", + "Payment Entry Deduction", + "Sales Taxes and Charges", + "Purchase Taxes and Charges", + "Shipping Rule", + "Landed Cost Item", + "Asset Value Adjustment", + "Loyalty Program", + "Fee Schedule", + "Fee Structure", + "Stock Reconciliation", + "Travel Request", + "Fees", + "POS Profile", + "Opening Invoice Creation Tool", + "Opening Invoice Creation Tool Item", + "Subscription", + "Subscription Plan", + "POS Invoice", + "POS Invoice Item", ] regional_overrides = { - 'France': { - 'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method' + "France": { + "erpnext.tests.test_regional.test_method": "erpnext.regional.france.utils.test_method" }, - 'India': { - 'erpnext.tests.test_regional.test_method': 'erpnext.regional.india.utils.test_method', - 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header', - 'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data', - 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details', - 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', - 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', - 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', - 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields', - 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', - 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' + "India": { + "erpnext.tests.test_regional.test_method": "erpnext.regional.india.utils.test_method", + "erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header": "erpnext.regional.india.utils.get_itemised_tax_breakup_header", + "erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data": "erpnext.regional.india.utils.get_itemised_tax_breakup_data", + "erpnext.accounts.party.get_regional_address_details": "erpnext.regional.india.utils.get_regional_address_details", + "erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts": "erpnext.regional.india.utils.get_regional_round_off_accounts", + "erpnext.hr.utils.calculate_annual_eligible_hra_exemption": "erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption", + "erpnext.hr.utils.calculate_hra_exemption_for_period": "erpnext.regional.india.utils.calculate_hra_exemption_for_period", + "erpnext.controllers.accounts_controller.validate_einvoice_fields": "erpnext.regional.india.e_invoice.utils.validate_einvoice_fields", + "erpnext.assets.doctype.asset.asset.get_depreciation_amount": "erpnext.regional.india.utils.get_depreciation_amount", + "erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code": "erpnext.regional.india.utils.set_item_tax_from_hsn_code", }, - 'United Arab Emirates': { - 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', - 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries', + "United Arab Emirates": { + "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data", + "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries": "erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries", }, - 'Saudi Arabia': { - 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' + "Saudi Arabia": { + "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data" + }, + "Italy": { + "erpnext.controllers.taxes_and_totals.update_itemised_tax_data": "erpnext.regional.italy.utils.update_itemised_tax_data", + "erpnext.controllers.accounts_controller.validate_regional": "erpnext.regional.italy.utils.sales_invoice_validate", }, - 'Italy': { - 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', - 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', - } } user_privacy_documents = [ { - 'doctype': 'Lead', - 'match_field': 'email_id', - 'personal_fields': ['phone', 'mobile_no', 'fax', 'website', 'lead_name'], + "doctype": "Lead", + "match_field": "email_id", + "personal_fields": ["phone", "mobile_no", "fax", "website", "lead_name"], }, { - 'doctype': 'Opportunity', - 'match_field': 'contact_email', - 'personal_fields': ['contact_mobile', 'contact_display', 'customer_name'], - } + "doctype": "Opportunity", + "match_field": "contact_email", + "personal_fields": ["contact_mobile", "contact_display", "customer_name"], + }, ] # ERPNext doctypes for Global Search @@ -541,107 +688,107 @@ global_search_doctypes = { {"doctype": "Warranty Claim", "index": 47}, ], "Healthcare": [ - {'doctype': 'Patient', 'index': 1}, - {'doctype': 'Medical Department', 'index': 2}, - {'doctype': 'Vital Signs', 'index': 3}, - {'doctype': 'Healthcare Practitioner', 'index': 4}, - {'doctype': 'Patient Appointment', 'index': 5}, - {'doctype': 'Healthcare Service Unit', 'index': 6}, - {'doctype': 'Patient Encounter', 'index': 7}, - {'doctype': 'Antibiotic', 'index': 8}, - {'doctype': 'Diagnosis', 'index': 9}, - {'doctype': 'Lab Test', 'index': 10}, - {'doctype': 'Clinical Procedure', 'index': 11}, - {'doctype': 'Inpatient Record', 'index': 12}, - {'doctype': 'Sample Collection', 'index': 13}, - {'doctype': 'Patient Medical Record', 'index': 14}, - {'doctype': 'Appointment Type', 'index': 15}, - {'doctype': 'Fee Validity', 'index': 16}, - {'doctype': 'Practitioner Schedule', 'index': 17}, - {'doctype': 'Dosage Form', 'index': 18}, - {'doctype': 'Lab Test Sample', 'index': 19}, - {'doctype': 'Prescription Duration', 'index': 20}, - {'doctype': 'Prescription Dosage', 'index': 21}, - {'doctype': 'Sensitivity', 'index': 22}, - {'doctype': 'Complaint', 'index': 23}, - {'doctype': 'Medical Code', 'index': 24}, + {"doctype": "Patient", "index": 1}, + {"doctype": "Medical Department", "index": 2}, + {"doctype": "Vital Signs", "index": 3}, + {"doctype": "Healthcare Practitioner", "index": 4}, + {"doctype": "Patient Appointment", "index": 5}, + {"doctype": "Healthcare Service Unit", "index": 6}, + {"doctype": "Patient Encounter", "index": 7}, + {"doctype": "Antibiotic", "index": 8}, + {"doctype": "Diagnosis", "index": 9}, + {"doctype": "Lab Test", "index": 10}, + {"doctype": "Clinical Procedure", "index": 11}, + {"doctype": "Inpatient Record", "index": 12}, + {"doctype": "Sample Collection", "index": 13}, + {"doctype": "Patient Medical Record", "index": 14}, + {"doctype": "Appointment Type", "index": 15}, + {"doctype": "Fee Validity", "index": 16}, + {"doctype": "Practitioner Schedule", "index": 17}, + {"doctype": "Dosage Form", "index": 18}, + {"doctype": "Lab Test Sample", "index": 19}, + {"doctype": "Prescription Duration", "index": 20}, + {"doctype": "Prescription Dosage", "index": 21}, + {"doctype": "Sensitivity", "index": 22}, + {"doctype": "Complaint", "index": 23}, + {"doctype": "Medical Code", "index": 24}, ], "Education": [ - {'doctype': 'Article', 'index': 1}, - {'doctype': 'Video', 'index': 2}, - {'doctype': 'Topic', 'index': 3}, - {'doctype': 'Course', 'index': 4}, - {'doctype': 'Program', 'index': 5}, - {'doctype': 'Quiz', 'index': 6}, - {'doctype': 'Question', 'index': 7}, - {'doctype': 'Fee Schedule', 'index': 8}, - {'doctype': 'Fee Structure', 'index': 9}, - {'doctype': 'Fees', 'index': 10}, - {'doctype': 'Student Group', 'index': 11}, - {'doctype': 'Student', 'index': 12}, - {'doctype': 'Instructor', 'index': 13}, - {'doctype': 'Course Activity', 'index': 14}, - {'doctype': 'Quiz Activity', 'index': 15}, - {'doctype': 'Course Enrollment', 'index': 16}, - {'doctype': 'Program Enrollment', 'index': 17}, - {'doctype': 'Student Language', 'index': 18}, - {'doctype': 'Student Applicant', 'index': 19}, - {'doctype': 'Assessment Result', 'index': 20}, - {'doctype': 'Assessment Plan', 'index': 21}, - {'doctype': 'Grading Scale', 'index': 22}, - {'doctype': 'Guardian', 'index': 23}, - {'doctype': 'Student Leave Application', 'index': 24}, - {'doctype': 'Student Log', 'index': 25}, - {'doctype': 'Room', 'index': 26}, - {'doctype': 'Course Schedule', 'index': 27}, - {'doctype': 'Student Attendance', 'index': 28}, - {'doctype': 'Announcement', 'index': 29}, - {'doctype': 'Student Category', 'index': 30}, - {'doctype': 'Assessment Group', 'index': 31}, - {'doctype': 'Student Batch Name', 'index': 32}, - {'doctype': 'Assessment Criteria', 'index': 33}, - {'doctype': 'Academic Year', 'index': 34}, - {'doctype': 'Academic Term', 'index': 35}, - {'doctype': 'School House', 'index': 36}, - {'doctype': 'Student Admission', 'index': 37}, - {'doctype': 'Fee Category', 'index': 38}, - {'doctype': 'Assessment Code', 'index': 39}, - {'doctype': 'Discussion', 'index': 40}, + {"doctype": "Article", "index": 1}, + {"doctype": "Video", "index": 2}, + {"doctype": "Topic", "index": 3}, + {"doctype": "Course", "index": 4}, + {"doctype": "Program", "index": 5}, + {"doctype": "Quiz", "index": 6}, + {"doctype": "Question", "index": 7}, + {"doctype": "Fee Schedule", "index": 8}, + {"doctype": "Fee Structure", "index": 9}, + {"doctype": "Fees", "index": 10}, + {"doctype": "Student Group", "index": 11}, + {"doctype": "Student", "index": 12}, + {"doctype": "Instructor", "index": 13}, + {"doctype": "Course Activity", "index": 14}, + {"doctype": "Quiz Activity", "index": 15}, + {"doctype": "Course Enrollment", "index": 16}, + {"doctype": "Program Enrollment", "index": 17}, + {"doctype": "Student Language", "index": 18}, + {"doctype": "Student Applicant", "index": 19}, + {"doctype": "Assessment Result", "index": 20}, + {"doctype": "Assessment Plan", "index": 21}, + {"doctype": "Grading Scale", "index": 22}, + {"doctype": "Guardian", "index": 23}, + {"doctype": "Student Leave Application", "index": 24}, + {"doctype": "Student Log", "index": 25}, + {"doctype": "Room", "index": 26}, + {"doctype": "Course Schedule", "index": 27}, + {"doctype": "Student Attendance", "index": 28}, + {"doctype": "Announcement", "index": 29}, + {"doctype": "Student Category", "index": 30}, + {"doctype": "Assessment Group", "index": 31}, + {"doctype": "Student Batch Name", "index": 32}, + {"doctype": "Assessment Criteria", "index": 33}, + {"doctype": "Academic Year", "index": 34}, + {"doctype": "Academic Term", "index": 35}, + {"doctype": "School House", "index": 36}, + {"doctype": "Student Admission", "index": 37}, + {"doctype": "Fee Category", "index": 38}, + {"doctype": "Assessment Code", "index": 39}, + {"doctype": "Discussion", "index": 40}, ], "Agriculture": [ - {'doctype': 'Weather', 'index': 1}, - {'doctype': 'Soil Texture', 'index': 2}, - {'doctype': 'Water Analysis', 'index': 3}, - {'doctype': 'Soil Analysis', 'index': 4}, - {'doctype': 'Plant Analysis', 'index': 5}, - {'doctype': 'Agriculture Analysis Criteria', 'index': 6}, - {'doctype': 'Disease', 'index': 7}, - {'doctype': 'Crop', 'index': 8}, - {'doctype': 'Fertilizer', 'index': 9}, - {'doctype': 'Crop Cycle', 'index': 10} + {"doctype": "Weather", "index": 1}, + {"doctype": "Soil Texture", "index": 2}, + {"doctype": "Water Analysis", "index": 3}, + {"doctype": "Soil Analysis", "index": 4}, + {"doctype": "Plant Analysis", "index": 5}, + {"doctype": "Agriculture Analysis Criteria", "index": 6}, + {"doctype": "Disease", "index": 7}, + {"doctype": "Crop", "index": 8}, + {"doctype": "Fertilizer", "index": 9}, + {"doctype": "Crop Cycle", "index": 10}, ], "Non Profit": [ - {'doctype': 'Certified Consultant', 'index': 1}, - {'doctype': 'Certification Application', 'index': 2}, - {'doctype': 'Volunteer', 'index': 3}, - {'doctype': 'Membership', 'index': 4}, - {'doctype': 'Member', 'index': 5}, - {'doctype': 'Donor', 'index': 6}, - {'doctype': 'Chapter', 'index': 7}, - {'doctype': 'Grant Application', 'index': 8}, - {'doctype': 'Volunteer Type', 'index': 9}, - {'doctype': 'Donor Type', 'index': 10}, - {'doctype': 'Membership Type', 'index': 11} + {"doctype": "Certified Consultant", "index": 1}, + {"doctype": "Certification Application", "index": 2}, + {"doctype": "Volunteer", "index": 3}, + {"doctype": "Membership", "index": 4}, + {"doctype": "Member", "index": 5}, + {"doctype": "Donor", "index": 6}, + {"doctype": "Chapter", "index": 7}, + {"doctype": "Grant Application", "index": 8}, + {"doctype": "Volunteer Type", "index": 9}, + {"doctype": "Donor Type", "index": 10}, + {"doctype": "Membership Type", "index": 11}, ], "Hospitality": [ - {'doctype': 'Hotel Room', 'index': 0}, - {'doctype': 'Hotel Room Reservation', 'index': 1}, - {'doctype': 'Hotel Room Pricing', 'index': 2}, - {'doctype': 'Hotel Room Package', 'index': 3}, - {'doctype': 'Hotel Room Type', 'index': 4} - ] + {"doctype": "Hotel Room", "index": 0}, + {"doctype": "Hotel Room Reservation", "index": 1}, + {"doctype": "Hotel Room Pricing", "index": 2}, + {"doctype": "Hotel Room Package", "index": 3}, + {"doctype": "Hotel Room Type", "index": 4}, + ], } additional_timeline_content = { - '*': ['erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs'] + "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"] } diff --git a/erpnext/hotels/doctype/hotel_room/hotel_room.py b/erpnext/hotels/doctype/hotel_room/hotel_room.py index e4bd1c88462..b4f826f6487 100644 --- a/erpnext/hotels/doctype/hotel_room/hotel_room.py +++ b/erpnext/hotels/doctype/hotel_room/hotel_room.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class HotelRoom(Document): def validate(self): if not self.capacity: - self.capacity, self.extra_bed_capacity = frappe.db.get_value('Hotel Room Type', - self.hotel_room_type, ['capacity', 'extra_bed_capacity']) + self.capacity, self.extra_bed_capacity = frappe.db.get_value( + "Hotel Room Type", self.hotel_room_type, ["capacity", "extra_bed_capacity"] + ) diff --git a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py index 95efe2c6068..0fa211c40cc 100644 --- a/erpnext/hotels/doctype/hotel_room/test_hotel_room.py +++ b/erpnext/hotels/doctype/hotel_room/test_hotel_room.py @@ -5,19 +5,14 @@ import unittest test_dependencies = ["Hotel Room Package"] test_records = [ - dict(doctype="Hotel Room", name="1001", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1002", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1003", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1004", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1005", - hotel_room_type="Basic Room"), - dict(doctype="Hotel Room", name="1006", - hotel_room_type="Basic Room") + dict(doctype="Hotel Room", name="1001", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1002", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1003", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1004", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1005", hotel_room_type="Basic Room"), + dict(doctype="Hotel Room", name="1006", hotel_room_type="Basic Room"), ] + class TestHotelRoom(unittest.TestCase): pass diff --git a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py index aedc83a8468..160a1d368ad 100644 --- a/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py +++ b/erpnext/hotels/doctype/hotel_room_package/hotel_room_package.py @@ -9,12 +9,10 @@ from frappe.model.document import Document class HotelRoomPackage(Document): def validate(self): if not self.item: - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = self.name, - item_group = 'Products', - is_stock_item = 0, - stock_uom = 'Unit' - )) + item = frappe.get_doc( + dict( + doctype="Item", item_code=self.name, item_group="Products", is_stock_item=0, stock_uom="Unit" + ) + ) item.insert() self.item = item.name diff --git a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py index 749731f4918..06f992106f3 100644 --- a/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py +++ b/erpnext/hotels/doctype/hotel_room_package/test_hotel_room_package.py @@ -4,44 +4,44 @@ import unittest test_records = [ - dict(doctype='Item', item_code='Breakfast', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Lunch', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Dinner', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='WiFi', - item_group='Products', is_stock_item=0), - dict(doctype='Hotel Room Type', name="Delux Room", + dict(doctype="Item", item_code="Breakfast", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Lunch", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Dinner", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="WiFi", item_group="Products", is_stock_item=0), + dict( + doctype="Hotel Room Type", + name="Delux Room", capacity=4, extra_bed_capacity=2, - amenities = [ - dict(item='WiFi', billable=0) - ]), - dict(doctype='Hotel Room Type', name="Basic Room", + amenities=[dict(item="WiFi", billable=0)], + ), + dict( + doctype="Hotel Room Type", + name="Basic Room", capacity=4, extra_bed_capacity=2, - amenities = [ - dict(item='Breakfast', billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Breakfast", + amenities=[dict(item="Breakfast", billable=0)], + ), + dict( + doctype="Hotel Room Package", + name="Basic Room with Breakfast", hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Lunch", + amenities=[dict(item="Breakfast", billable=0)], + ), + dict( + doctype="Hotel Room Package", + name="Basic Room with Lunch", hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0), - dict(item="Lunch", billable=0) - ]), - dict(doctype="Hotel Room Package", name="Basic Room with Dinner", + amenities=[dict(item="Breakfast", billable=0), dict(item="Lunch", billable=0)], + ), + dict( + doctype="Hotel Room Package", + name="Basic Room with Dinner", hotel_room_type="Basic Room", - amenities = [ - dict(item="Breakfast", billable=0), - dict(item="Dinner", billable=0) - ]) + amenities=[dict(item="Breakfast", billable=0), dict(item="Dinner", billable=0)], + ), ] + class TestHotelRoomPackage(unittest.TestCase): pass diff --git a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py index 34550096dd9..15752b5b1af 100644 --- a/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py +++ b/erpnext/hotels/doctype/hotel_room_pricing/test_hotel_room_pricing.py @@ -5,15 +5,20 @@ import unittest test_dependencies = ["Hotel Room Package"] test_records = [ - dict(doctype="Hotel Room Pricing", enabled=1, + dict( + doctype="Hotel Room Pricing", + enabled=1, name="Winter 2017", - from_date="2017-01-01", to_date="2017-01-10", - items = [ + from_date="2017-01-01", + to_date="2017-01-10", + items=[ dict(item="Basic Room with Breakfast", rate=10000), dict(item="Basic Room with Lunch", rate=11000), - dict(item="Basic Room with Dinner", rate=12000) - ]) + dict(item="Basic Room with Dinner", rate=12000), + ], + ) ] + class TestHotelRoomPricing(unittest.TestCase): pass diff --git a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py index 7725955396b..eaad6898a09 100644 --- a/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py +++ b/erpnext/hotels/doctype/hotel_room_reservation/hotel_room_reservation.py @@ -10,8 +10,13 @@ from frappe.model.document import Document from frappe.utils import add_days, date_diff, flt -class HotelRoomUnavailableError(frappe.ValidationError): pass -class HotelRoomPricingNotSetError(frappe.ValidationError): pass +class HotelRoomUnavailableError(frappe.ValidationError): + pass + + +class HotelRoomPricingNotSetError(frappe.ValidationError): + pass + class HotelRoomReservation(Document): def validate(self): @@ -28,27 +33,39 @@ class HotelRoomReservation(Document): if not d.item in self.rooms_booked: self.rooms_booked[d.item] = 0 - room_type = frappe.db.get_value("Hotel Room Package", - d.item, 'hotel_room_type') - rooms_booked = get_rooms_booked(room_type, day, exclude_reservation=self.name) \ - + d.qty + self.rooms_booked.get(d.item) + room_type = frappe.db.get_value("Hotel Room Package", d.item, "hotel_room_type") + rooms_booked = ( + get_rooms_booked(room_type, day, exclude_reservation=self.name) + + d.qty + + self.rooms_booked.get(d.item) + ) total_rooms = self.get_total_rooms(d.item) if total_rooms < rooms_booked: - frappe.throw(_("Hotel Rooms of type {0} are unavailable on {1}").format(d.item, - frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomUnavailableError) + frappe.throw( + _("Hotel Rooms of type {0} are unavailable on {1}").format( + d.item, frappe.format(day, dict(fieldtype="Date")) + ), + exc=HotelRoomUnavailableError, + ) self.rooms_booked[d.item] += rooms_booked def get_total_rooms(self, item): if not item in self.total_rooms: - self.total_rooms[item] = frappe.db.sql(""" + self.total_rooms[item] = ( + frappe.db.sql( + """ select count(*) from `tabHotel Room Package` package inner join `tabHotel Room` room on package.hotel_room_type = room.hotel_room_type where - package.item = %s""", item)[0][0] or 0 + package.item = %s""", + item, + )[0][0] + or 0 + ) return self.total_rooms[item] @@ -60,7 +77,8 @@ class HotelRoomReservation(Document): day = add_days(self.from_date, i) if not d.item: continue - day_rate = frappe.db.sql(""" + day_rate = frappe.db.sql( + """ select item.rate from @@ -70,18 +88,22 @@ class HotelRoomReservation(Document): item.parent = pricing.name and item.item = %s and %s between pricing.from_date - and pricing.to_date""", (d.item, day)) + and pricing.to_date""", + (d.item, day), + ) if day_rate: net_rate += day_rate[0][0] else: frappe.throw( - _("Please set Hotel Room Rate on {}").format( - frappe.format(day, dict(fieldtype="Date"))), exc=HotelRoomPricingNotSetError) + _("Please set Hotel Room Rate on {}").format(frappe.format(day, dict(fieldtype="Date"))), + exc=HotelRoomPricingNotSetError, + ) d.rate = net_rate d.amount = net_rate * flt(d.qty) self.net_total += d.amount + @frappe.whitelist() def get_room_rate(hotel_room_reservation): """Calculate rate for each day as it may belong to different Hotel Room Pricing Item""" @@ -89,12 +111,15 @@ def get_room_rate(hotel_room_reservation): doc.set_rates() return doc.as_dict() -def get_rooms_booked(room_type, day, exclude_reservation=None): - exclude_condition = '' - if exclude_reservation: - exclude_condition = 'and reservation.name != {0}'.format(frappe.db.escape(exclude_reservation)) - return frappe.db.sql(""" +def get_rooms_booked(room_type, day, exclude_reservation=None): + exclude_condition = "" + if exclude_reservation: + exclude_condition = "and reservation.name != {0}".format(frappe.db.escape(exclude_reservation)) + + return ( + frappe.db.sql( + """ select sum(item.qty) from `tabHotel Room Package` room_package, @@ -107,5 +132,10 @@ def get_rooms_booked(room_type, day, exclude_reservation=None): and reservation.docstatus = 1 {exclude_condition} and %s between reservation.from_date - and reservation.to_date""".format(exclude_condition=exclude_condition), - (room_type, day))[0][0] or 0 + and reservation.to_date""".format( + exclude_condition=exclude_condition + ), + (room_type, day), + )[0][0] + or 0 + ) diff --git a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py index bb32a27fa7c..52e7d69f79f 100644 --- a/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py +++ b/erpnext/hotels/doctype/hotel_room_reservation/test_hotel_room_reservation.py @@ -12,6 +12,7 @@ from erpnext.hotels.doctype.hotel_room_reservation.hotel_room_reservation import test_dependencies = ["Hotel Room Package", "Hotel Room Pricing", "Hotel Room"] + class TestHotelRoomReservation(unittest.TestCase): def setUp(self): frappe.db.sql("delete from `tabHotel Room Reservation`") @@ -19,22 +20,14 @@ class TestHotelRoomReservation(unittest.TestCase): def test_reservation(self): reservation = make_reservation( - from_date="2017-01-01", - to_date="2017-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=2) - ] + from_date="2017-01-01", to_date="2017-01-03", items=[dict(item="Basic Room with Dinner", qty=2)] ) reservation.insert() self.assertEqual(reservation.net_total, 48000) def test_price_not_set(self): reservation = make_reservation( - from_date="2016-01-01", - to_date="2016-01-03", - items=[ - dict(item="Basic Room with Dinner", qty=2) - ] + from_date="2016-01-01", to_date="2016-01-03", items=[dict(item="Basic Room with Dinner", qty=2)] ) self.assertRaises(HotelRoomPricingNotSetError, reservation.insert) @@ -44,7 +37,7 @@ class TestHotelRoomReservation(unittest.TestCase): to_date="2017-01-03", items=[ dict(item="Basic Room with Dinner", qty=2), - ] + ], ) reservation.insert() @@ -53,10 +46,11 @@ class TestHotelRoomReservation(unittest.TestCase): to_date="2017-01-03", items=[ dict(item="Basic Room with Dinner", qty=20), - ] + ], ) self.assertRaises(HotelRoomUnavailableError, reservation.insert) + def make_reservation(**kwargs): kwargs["doctype"] = "Hotel Room Reservation" if not "guest_name" in kwargs: diff --git a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py index c43589d2a8d..ada5332bce0 100644 --- a/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py +++ b/erpnext/hotels/report/hotel_room_occupancy/hotel_room_occupancy.py @@ -14,16 +14,18 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): columns = [ dict(label=_("Room Type"), fieldname="room_type"), - dict(label=_("Rooms Booked"), fieldtype="Int") + dict(label=_("Rooms Booked"), fieldtype="Int"), ] return columns + def get_data(filters): out = [] - for room_type in frappe.get_all('Hotel Room Type'): + for room_type in frappe.get_all("Hotel Room Type"): total_booked = 0 for i in range(date_diff(filters.to_date, filters.from_date)): day = add_days(filters.from_date, i) diff --git a/erpnext/hr/doctype/appointment_letter/appointment_letter.py b/erpnext/hr/doctype/appointment_letter/appointment_letter.py index 71327bf1b01..a58589af210 100644 --- a/erpnext/hr/doctype/appointment_letter/appointment_letter.py +++ b/erpnext/hr/doctype/appointment_letter/appointment_letter.py @@ -9,18 +9,21 @@ from frappe.model.document import Document class AppointmentLetter(Document): pass + @frappe.whitelist() def get_appointment_letter_details(template): body = [] - intro = frappe.get_list('Appointment Letter Template', - fields=['introduction', 'closing_notes'], - filters={'name': template} + intro = frappe.get_list( + "Appointment Letter Template", + fields=["introduction", "closing_notes"], + filters={"name": template}, )[0] - content = frappe.get_all('Appointment Letter content', - fields=['title', 'description'], - filters={'parent': template}, - order_by='idx' + content = frappe.get_all( + "Appointment Letter content", + fields=["title", "description"], + filters={"parent": template}, + order_by="idx", ) body.append(intro) - body.append({'description': content}) + body.append({"description": content}) return body diff --git a/erpnext/hr/doctype/appraisal/appraisal.py b/erpnext/hr/doctype/appraisal/appraisal.py index 83273f86544..382c643abae 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.py +++ b/erpnext/hr/doctype/appraisal/appraisal.py @@ -34,46 +34,61 @@ class Appraisal(Document): frappe.throw(_("End Date can not be less than Start Date")) def validate_existing_appraisal(self): - chk = frappe.db.sql("""select name from `tabAppraisal` where employee=%s + chk = frappe.db.sql( + """select name from `tabAppraisal` where employee=%s and (status='Submitted' or status='Completed') and ((start_date>=%s and start_date<=%s) or (end_date>=%s and end_date<=%s))""", - (self.employee,self.start_date,self.end_date,self.start_date,self.end_date)) + (self.employee, self.start_date, self.end_date, self.start_date, self.end_date), + ) if chk: - frappe.throw(_("Appraisal {0} created for Employee {1} in the given date range").format(chk[0][0], self.employee_name)) + frappe.throw( + _("Appraisal {0} created for Employee {1} in the given date range").format( + chk[0][0], self.employee_name + ) + ) def calculate_total(self): - total, total_w = 0, 0 - for d in self.get('goals'): + total, total_w = 0, 0 + for d in self.get("goals"): if d.score: d.score_earned = flt(d.score) * flt(d.per_weightage) / 100 total = total + d.score_earned total_w += flt(d.per_weightage) if int(total_w) != 100: - frappe.throw(_("Total weightage assigned should be 100%.
    It is {0}").format(str(total_w) + "%")) + frappe.throw( + _("Total weightage assigned should be 100%.
    It is {0}").format(str(total_w) + "%") + ) - if frappe.db.get_value("Employee", self.employee, "user_id") != \ - frappe.session.user and total == 0: + if ( + frappe.db.get_value("Employee", self.employee, "user_id") != frappe.session.user and total == 0 + ): frappe.throw(_("Total cannot be zero")) self.total_score = total def on_submit(self): - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") def on_cancel(self): - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") + @frappe.whitelist() def fetch_appraisal_template(source_name, target_doc=None): - target_doc = get_mapped_doc("Appraisal Template", source_name, { - "Appraisal Template": { - "doctype": "Appraisal", + target_doc = get_mapped_doc( + "Appraisal Template", + source_name, + { + "Appraisal Template": { + "doctype": "Appraisal", + }, + "Appraisal Template Goal": { + "doctype": "Appraisal Goal", + }, }, - "Appraisal Template Goal": { - "doctype": "Appraisal Goal", - } - }, target_doc) + target_doc, + ) return target_doc diff --git a/erpnext/hr/doctype/appraisal/test_appraisal.py b/erpnext/hr/doctype/appraisal/test_appraisal.py index 90c30ef3475..13a39f38200 100644 --- a/erpnext/hr/doctype/appraisal/test_appraisal.py +++ b/erpnext/hr/doctype/appraisal/test_appraisal.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Appraisal') + class TestAppraisal(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py index f52e2b027ca..476de4f51b4 100644 --- a/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py +++ b/erpnext/hr/doctype/appraisal_template/appraisal_template_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'kra_template', - 'transactions': [ - { - 'items': ['Appraisal'] - }, - ], - } + return { + "fieldname": "kra_template", + "transactions": [ + {"items": ["Appraisal"]}, + ], + } diff --git a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py index d0e81a7dc5b..560e992e8a1 100644 --- a/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py +++ b/erpnext/hr/doctype/appraisal_template/test_appraisal_template.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Appraisal Template') + class TestAppraisalTemplate(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index b1e373e2181..7f4bd836854 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -13,6 +13,7 @@ from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_emp class Attendance(Document): def validate(self): from erpnext.controllers.status_updater import validate_status + validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_active_employee(self.employee) self.validate_attendance_date() @@ -24,62 +25,84 @@ class Attendance(Document): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") # leaves can be marked for future dates - if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()): + if ( + self.status != "On Leave" + and not self.leave_application + and getdate(self.attendance_date) > getdate(nowdate()) + ): frappe.throw(_("Attendance can not be marked for future dates")) elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining): frappe.throw(_("Attendance date can not be less than employee's joining date")) def validate_duplicate_record(self): - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select name from `tabAttendance` where employee = %s and attendance_date = %s and name != %s and docstatus != 2 - """, (self.employee, getdate(self.attendance_date), self.name)) + """, + (self.employee, getdate(self.attendance_date), self.name), + ) if res: - frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( - frappe.bold(self.employee), frappe.bold(self.attendance_date))) + frappe.throw( + _("Attendance for employee {0} is already marked for the date {1}").format( + frappe.bold(self.employee), frappe.bold(self.attendance_date) + ) + ) def validate_employee_status(self): if frappe.db.get_value("Employee", self.employee, "status") == "Inactive": frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee)) def check_leave_record(self): - leave_record = frappe.db.sql(""" + leave_record = frappe.db.sql( + """ select leave_type, half_day, half_day_date from `tabLeave Application` where employee = %s and %s between from_date and to_date and status = 'Approved' and docstatus = 1 - """, (self.employee, self.attendance_date), as_dict=True) + """, + (self.employee, self.attendance_date), + as_dict=True, + ) if leave_record: for d in leave_record: self.leave_type = d.leave_type if d.half_day_date == getdate(self.attendance_date): - self.status = 'Half Day' - frappe.msgprint(_("Employee {0} on Half day on {1}") - .format(self.employee, formatdate(self.attendance_date))) + self.status = "Half Day" + frappe.msgprint( + _("Employee {0} on Half day on {1}").format(self.employee, formatdate(self.attendance_date)) + ) else: - self.status = 'On Leave' - frappe.msgprint(_("Employee {0} is on Leave on {1}") - .format(self.employee, formatdate(self.attendance_date))) + self.status = "On Leave" + frappe.msgprint( + _("Employee {0} is on Leave on {1}").format(self.employee, formatdate(self.attendance_date)) + ) if self.status in ("On Leave", "Half Day"): if not leave_record: - frappe.msgprint(_("No leave record found for employee {0} on {1}") - .format(self.employee, formatdate(self.attendance_date)), alert=1) + frappe.msgprint( + _("No leave record found for employee {0} on {1}").format( + self.employee, formatdate(self.attendance_date) + ), + alert=1, + ) elif self.leave_type: self.leave_type = None self.leave_application = None def validate_employee(self): - emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'", - self.employee) + emp = frappe.db.sql( + "select name from `tabEmployee` where name = %s and status = 'Active'", self.employee + ) if not emp: frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) + @frappe.whitelist() def get_events(start, end, filters=None): events = [] @@ -90,10 +113,12 @@ def get_events(start, end, filters=None): return events from frappe.desk.reportview import get_filters_cond + conditions = get_filters_cond("Attendance", filters, []) add_attendance(events, start, end, conditions=conditions) return events + def add_attendance(events, start, end, conditions=None): query = """select name, attendance_date, status from `tabAttendance` where @@ -102,81 +127,97 @@ def add_attendance(events, start, end, conditions=None): if conditions: query += conditions - for d in frappe.db.sql(query, {"from_date":start, "to_date":end}, as_dict=True): + for d in frappe.db.sql(query, {"from_date": start, "to_date": end}, as_dict=True): e = { "name": d.name, "doctype": "Attendance", "start": d.attendance_date, "end": d.attendance_date, "title": cstr(d.status), - "docstatus": d.docstatus + "docstatus": d.docstatus, } if e not in events: events.append(e) -def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False): - if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): - company = frappe.db.get_value('Employee', employee, 'company') - attendance = frappe.get_doc({ - 'doctype': 'Attendance', - 'employee': employee, - 'attendance_date': attendance_date, - 'status': status, - 'company': company, - 'shift': shift, - 'leave_type': leave_type - }) + +def mark_attendance( + employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False +): + if not frappe.db.exists( + "Attendance", + {"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")}, + ): + company = frappe.db.get_value("Employee", employee, "company") + attendance = frappe.get_doc( + { + "doctype": "Attendance", + "employee": employee, + "attendance_date": attendance_date, + "status": status, + "company": company, + "shift": shift, + "leave_type": leave_type, + } + ) attendance.flags.ignore_validate = ignore_validate attendance.insert() attendance.submit() return attendance.name + @frappe.whitelist() def mark_bulk_attendance(data): import json + if isinstance(data, str): data = json.loads(data) data = frappe._dict(data) - company = frappe.get_value('Employee', data.employee, 'company') + company = frappe.get_value("Employee", data.employee, "company") if not data.unmarked_days: frappe.throw(_("Please select a date.")) return for date in data.unmarked_days: doc_dict = { - 'doctype': 'Attendance', - 'employee': data.employee, - 'attendance_date': get_datetime(date), - 'status': data.status, - 'company': company, + "doctype": "Attendance", + "employee": data.employee, + "attendance_date": get_datetime(date), + "status": data.status, + "company": company, } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() def get_month_map(): - return frappe._dict({ - "January": 1, - "February": 2, - "March": 3, - "April": 4, - "May": 5, - "June": 6, - "July": 7, - "August": 8, - "September": 9, - "October": 10, - "November": 11, - "December": 12 - }) + return frappe._dict( + { + "January": 1, + "February": 2, + "March": 3, + "April": 4, + "May": 5, + "June": 6, + "July": 7, + "August": 8, + "September": 9, + "October": 10, + "November": 11, + "December": 12, + } + ) + @frappe.whitelist() def get_unmarked_days(employee, month, exclude_holidays=0): import calendar + month_map = get_month_map() today = get_datetime() - joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.get_cached_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) start_day = 1 end_day = calendar.monthrange(today.year, month_map[month])[1] + 1 @@ -186,15 +227,21 @@ def get_unmarked_days(employee, month, exclude_holidays=0): if relieving_date and relieving_date.month == month_map[month]: end_day = relieving_date.day + 1 - dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)] + dates_of_month = [ + "{}-{}-{}".format(today.year, month_map[month], r) for r in range(start_day, end_day) + ] month_start, month_end = dates_of_month[0], dates_of_month[-1] - records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[ - ["attendance_date", ">=", month_start], - ["attendance_date", "<=", month_end], - ["employee", "=", employee], - ["docstatus", "!=", 2] - ]) + records = frappe.get_all( + "Attendance", + fields=["attendance_date", "employee"], + filters=[ + ["attendance_date", ">=", month_start], + ["attendance_date", "<=", month_end], + ["employee", "=", employee], + ["docstatus", "!=", 2], + ], + ) marked_days = [get_datetime(record.attendance_date) for record in records] if cint(exclude_holidays): diff --git a/erpnext/hr/doctype/attendance/attendance_dashboard.py b/erpnext/hr/doctype/attendance/attendance_dashboard.py index f466534d2c7..abed78fbbbe 100644 --- a/erpnext/hr/doctype/attendance/attendance_dashboard.py +++ b/erpnext/hr/doctype/attendance/attendance_dashboard.py @@ -1,12 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'attendance', - 'transactions': [ - { - 'label': '', - 'items': ['Employee Checkin'] - } - ] - } + return {"fieldname": "attendance", "transactions": [{"label": "", "items": ["Employee Checkin"]}]} diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index 585059ff479..058bc93d72a 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -13,7 +13,8 @@ from erpnext.hr.doctype.attendance.attendance import ( from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday -test_records = frappe.get_test_records('Attendance') +test_records = frappe.get_test_records("Attendance") + class TestAttendance(FrappeTestCase): def setUp(self): @@ -26,9 +27,11 @@ class TestAttendance(FrappeTestCase): def test_mark_absent(self): employee = make_employee("test_mark_absent@example.com") date = nowdate() - frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date}) - attendance = mark_attendance(employee, date, 'Absent') - fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'}) + frappe.db.delete("Attendance", {"employee": employee, "attendance_date": date}) + attendance = mark_attendance(employee, date, "Absent") + fetch_attendance = frappe.get_value( + "Attendance", {"employee": employee, "attendance_date": date, "status": "Absent"} + ) self.assertEqual(attendance, fetch_attendance) def test_unmarked_days(self): @@ -36,12 +39,14 @@ class TestAttendance(FrappeTestCase): previous_month = now.month - 1 first_day = now.replace(day=1).replace(month=previous_month).date() - employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) - frappe.db.delete('Attendance', {'employee': employee}) - frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list) + employee = make_employee( + "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + ) + frappe.db.delete("Attendance", {"employee": employee}) + frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) - mark_attendance(employee, first_day, 'Present') + mark_attendance(employee, first_day, "Present") month_name = get_month_name(first_day) unmarked_days = get_unmarked_days(employee, month_name) @@ -59,13 +64,15 @@ class TestAttendance(FrappeTestCase): previous_month = now.month - 1 first_day = now.replace(day=1).replace(month=previous_month).date() - employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) - frappe.db.delete('Attendance', {'employee': employee}) + employee = make_employee( + "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + ) + frappe.db.delete("Attendance", {"employee": employee}) - frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list) + frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) - mark_attendance(employee, first_day, 'Present') + mark_attendance(employee, first_day, "Present") month_name = get_month_name(first_day) unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True) @@ -85,14 +92,15 @@ class TestAttendance(FrappeTestCase): doj = add_days(first_day, 1) relieving_date = add_days(first_day, 5) - employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj, - relieving_date=relieving_date) - frappe.db.delete('Attendance', {'employee': employee}) + employee = make_employee( + "test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date + ) + frappe.db.delete("Attendance", {"employee": employee}) - frappe.db.set_value('Employee', employee, 'holiday_list', self.holiday_list) + frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) attendance_date = add_days(first_day, 2) - mark_attendance(employee, attendance_date, 'Present') + mark_attendance(employee, attendance_date, "Present") month_name = get_month_name(first_day) unmarked_days = get_unmarked_days(employee, month_name) diff --git a/erpnext/hr/doctype/attendance_request/attendance_request.py b/erpnext/hr/doctype/attendance_request/attendance_request.py index 8fbe7c7a9ad..78652f669d7 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/attendance_request.py @@ -16,17 +16,19 @@ class AttendanceRequest(Document): validate_active_employee(self.employee) validate_dates(self, self.from_date, self.to_date) if self.half_day: - if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date): + if not getdate(self.from_date) <= getdate(self.half_day_date) <= getdate(self.to_date): frappe.throw(_("Half day date should be in between from date and to date")) def on_submit(self): self.create_attendance() def on_cancel(self): - attendance_list = frappe.get_list("Attendance", {'employee': self.employee, 'attendance_request': self.name}) + attendance_list = frappe.get_list( + "Attendance", {"employee": self.employee, "attendance_request": self.name} + ) if attendance_list: for attendance in attendance_list: - attendance_obj = frappe.get_doc("Attendance", attendance['name']) + attendance_obj = frappe.get_doc("Attendance", attendance["name"]) attendance_obj.cancel() def create_attendance(self): @@ -53,15 +55,24 @@ class AttendanceRequest(Document): def validate_if_attendance_not_applicable(self, attendance_date): # Check if attendance_date is a Holiday if is_holiday(self.employee, attendance_date): - frappe.msgprint(_("Attendance not submitted for {0} as it is a Holiday.").format(attendance_date), alert=1) + frappe.msgprint( + _("Attendance not submitted for {0} as it is a Holiday.").format(attendance_date), alert=1 + ) return True # Check if employee on Leave - leave_record = frappe.db.sql("""select half_day from `tabLeave Application` + leave_record = frappe.db.sql( + """select half_day from `tabLeave Application` where employee = %s and %s between from_date and to_date - and docstatus = 1""", (self.employee, attendance_date), as_dict=True) + and docstatus = 1""", + (self.employee, attendance_date), + as_dict=True, + ) if leave_record: - frappe.msgprint(_("Attendance not submitted for {0} as {1} on leave.").format(attendance_date, self.employee), alert=1) + frappe.msgprint( + _("Attendance not submitted for {0} as {1} on leave.").format(attendance_date, self.employee), + alert=1, + ) return True return False diff --git a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py index b23e0fd93a2..059725cb44a 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py +++ b/erpnext/hr/doctype/attendance_request/attendance_request_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'attendance_request', - 'transactions': [ - { - 'items': ['Attendance'] - } - ] - } + return {"fieldname": "attendance_request", "transactions": [{"items": ["Attendance"]}]} diff --git a/erpnext/hr/doctype/attendance_request/test_attendance_request.py b/erpnext/hr/doctype/attendance_request/test_attendance_request.py index 3f0442c7d69..ee436f50687 100644 --- a/erpnext/hr/doctype/attendance_request/test_attendance_request.py +++ b/erpnext/hr/doctype/attendance_request/test_attendance_request.py @@ -9,6 +9,7 @@ from frappe.utils import nowdate test_dependencies = ["Employee"] + class TestAttendanceRequest(unittest.TestCase): def setUp(self): for doctype in ["Attendance Request", "Attendance"]: @@ -34,10 +35,10 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, fieldname=["status", "docstatus"], - as_dict=True + as_dict=True, ) self.assertEqual(attendance.status, "Present") self.assertEqual(attendance.docstatus, 1) @@ -51,9 +52,9 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, - fieldname="docstatus" + fieldname="docstatus", ) self.assertEqual(attendance_docstatus, 2) @@ -74,11 +75,11 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, - fieldname="status" + fieldname="status", ) - self.assertEqual(attendance_status, 'Work From Home') + self.assertEqual(attendance_status, "Work From Home") attendance_request.cancel() @@ -88,11 +89,12 @@ class TestAttendanceRequest(unittest.TestCase): "Attendance", filters={ "attendance_request": attendance_request.name, - "attendance_date": date(date.today().year, 1, 1) + "attendance_date": date(date.today().year, 1, 1), }, - fieldname="docstatus" + fieldname="docstatus", ) self.assertEqual(attendance_docstatus, 2) + def get_employee(): return frappe.get_doc("Employee", "_T-Employee-00001") diff --git a/erpnext/hr/doctype/branch/test_branch.py b/erpnext/hr/doctype/branch/test_branch.py index e84c6e4c60e..c14d4aad690 100644 --- a/erpnext/hr/doctype/branch/test_branch.py +++ b/erpnext/hr/doctype/branch/test_branch.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Branch') +test_records = frappe.get_test_records("Branch") diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index 7d6051508ad..d233226e663 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -18,14 +18,15 @@ from erpnext.hr.utils import ( class CompensatoryLeaveRequest(Document): - def validate(self): validate_active_employee(self.employee) validate_dates(self, self.work_from_date, self.work_end_date) if self.half_day: if not self.half_day_date: frappe.throw(_("Half Day Date is mandatory")) - if not getdate(self.work_from_date)<=getdate(self.half_day_date)<=getdate(self.work_end_date): + if ( + not getdate(self.work_from_date) <= getdate(self.half_day_date) <= getdate(self.work_end_date) + ): frappe.throw(_("Half Day Date should be in between Work From Date and Work End Date")) validate_overlap(self, self.work_from_date, self.work_end_date) self.validate_holidays() @@ -34,13 +35,16 @@ class CompensatoryLeaveRequest(Document): frappe.throw(_("Leave Type is madatory")) def validate_attendance(self): - attendance = frappe.get_all('Attendance', + attendance = frappe.get_all( + "Attendance", filters={ - 'attendance_date': ['between', (self.work_from_date, self.work_end_date)], - 'status': 'Present', - 'docstatus': 1, - 'employee': self.employee - }, fields=['attendance_date', 'status']) + "attendance_date": ["between", (self.work_from_date, self.work_end_date)], + "status": "Present", + "docstatus": 1, + "employee": self.employee, + }, + fields=["attendance_date", "status"], + ) if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1: frappe.throw(_("You are not present all day(s) between compensatory leave request days")) @@ -49,7 +53,9 @@ class CompensatoryLeaveRequest(Document): holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date) if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: if date_diff(self.work_end_date, self.work_from_date): - msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))) + msg = _("The days between {0} to {1} are not valid holidays.").format( + frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)) + ) else: msg = _("{0} is not a holiday.").format(frappe.bold(format_date(self.work_from_date))) @@ -70,13 +76,19 @@ class CompensatoryLeaveRequest(Document): leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) # generate additional ledger entry for the new compensatory leaves off - create_additional_leave_ledger_entry(leave_allocation, date_difference, add_days(self.work_end_date, 1)) + create_additional_leave_ledger_entry( + leave_allocation, date_difference, add_days(self.work_end_date, 1) + ) else: leave_allocation = self.create_leave_allocation(leave_period, date_difference) self.db_set("leave_allocation", leave_allocation.name) else: - frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date))) + frappe.throw( + _("There is no leave period in between {0} and {1}").format( + format_date(self.work_from_date), format_date(self.work_end_date) + ) + ) def on_cancel(self): if self.leave_allocation: @@ -93,10 +105,13 @@ class CompensatoryLeaveRequest(Document): leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) # create reverse entry on cancelation - create_additional_leave_ledger_entry(leave_allocation, date_difference * -1, add_days(self.work_end_date, 1)) + create_additional_leave_ledger_entry( + leave_allocation, date_difference * -1, add_days(self.work_end_date, 1) + ) def get_existing_allocation_for_period(self, leave_period): - leave_allocation = frappe.db.sql(""" + leave_allocation = frappe.db.sql( + """ select name from `tabLeave Allocation` where employee=%(employee)s and leave_type=%(leave_type)s @@ -104,12 +119,15 @@ class CompensatoryLeaveRequest(Document): and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, { - "from_date": leave_period[0].from_date, - "to_date": leave_period[0].to_date, - "employee": self.employee, - "leave_type": self.leave_type - }, as_dict=1) + """, + { + "from_date": leave_period[0].from_date, + "to_date": leave_period[0].to_date, + "employee": self.employee, + "leave_type": self.leave_type, + }, + as_dict=1, + ) if leave_allocation: return frappe.get_doc("Leave Allocation", leave_allocation[0].name) @@ -118,18 +136,20 @@ class CompensatoryLeaveRequest(Document): def create_leave_allocation(self, leave_period, date_difference): is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") - allocation = frappe.get_doc(dict( - doctype="Leave Allocation", - employee=self.employee, - employee_name=self.employee_name, - leave_type=self.leave_type, - from_date=add_days(self.work_end_date, 1), - to_date=leave_period[0].to_date, - carry_forward=cint(is_carry_forward), - new_leaves_allocated=date_difference, - total_leaves_allocated=date_difference, - description=self.reason - )) + allocation = frappe.get_doc( + dict( + doctype="Leave Allocation", + employee=self.employee, + employee_name=self.employee_name, + leave_type=self.leave_type, + from_date=add_days(self.work_end_date, 1), + to_date=leave_period[0].to_date, + carry_forward=cint(is_carry_forward), + new_leaves_allocated=date_difference, + total_leaves_allocated=date_difference, + description=self.reason, + ) + ) allocation.insert(ignore_permissions=True) allocation.submit() return allocation diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py index 5e51879328b..7bbec293f41 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py @@ -12,12 +12,17 @@ from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_perio test_dependencies = ["Employee"] + class TestCompensatoryLeaveRequest(unittest.TestCase): def setUp(self): - frappe.db.sql(''' delete from `tabCompensatory Leave Request`''') - frappe.db.sql(''' delete from `tabLeave Ledger Entry`''') - frappe.db.sql(''' delete from `tabLeave Allocation`''') - frappe.db.sql(''' delete from `tabAttendance` where attendance_date in {0} '''.format((today(), add_days(today(), -1)))) #nosec + frappe.db.sql(""" delete from `tabCompensatory Leave Request`""") + frappe.db.sql(""" delete from `tabLeave Ledger Entry`""") + frappe.db.sql(""" delete from `tabLeave Allocation`""") + frappe.db.sql( + """ delete from `tabAttendance` where attendance_date in {0} """.format( + (today(), add_days(today(), -1)) + ) + ) # nosec create_leave_period(add_months(today(), -3), add_months(today(), 3), "_Test Company") create_holiday_list() @@ -26,7 +31,7 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): employee.save() def test_leave_balance_on_submit(self): - ''' check creation of leave allocation on submission of compensatory leave request ''' + """check creation of leave allocation on submission of compensatory leave request""" employee = get_employee() mark_attendance(employee) compensatory_leave_request = get_compensatory_leave_request(employee.name) @@ -34,18 +39,27 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): before = get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, today()) compensatory_leave_request.submit() - self.assertEqual(get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)), before + 1) + self.assertEqual( + get_leave_balance_on( + employee.name, compensatory_leave_request.leave_type, add_days(today(), 1) + ), + before + 1, + ) def test_leave_allocation_update_on_submit(self): employee = get_employee() mark_attendance(employee, date=add_days(today(), -1)) - compensatory_leave_request = get_compensatory_leave_request(employee.name, leave_date=add_days(today(), -1)) + compensatory_leave_request = get_compensatory_leave_request( + employee.name, leave_date=add_days(today(), -1) + ) compensatory_leave_request.submit() # leave allocation creation on submit - leaves_allocated = frappe.db.get_value('Leave Allocation', { - 'name': compensatory_leave_request.leave_allocation - }, ['total_leaves_allocated']) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"name": compensatory_leave_request.leave_allocation}, + ["total_leaves_allocated"], + ) self.assertEqual(leaves_allocated, 1) mark_attendance(employee) @@ -53,20 +67,22 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): compensatory_leave_request.submit() # leave allocation updates on submission of second compensatory leave request - leaves_allocated = frappe.db.get_value('Leave Allocation', { - 'name': compensatory_leave_request.leave_allocation - }, ['total_leaves_allocated']) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"name": compensatory_leave_request.leave_allocation}, + ["total_leaves_allocated"], + ) self.assertEqual(leaves_allocated, 2) def test_creation_of_leave_ledger_entry_on_submit(self): - ''' check creation of leave ledger entry on submission of leave request ''' + """check creation of leave ledger entry on submission of leave request""" employee = get_employee() mark_attendance(employee) compensatory_leave_request = get_compensatory_leave_request(employee.name) compensatory_leave_request.submit() filters = dict(transaction_name=compensatory_leave_request.leave_allocation) - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters) + leave_ledger_entry = frappe.get_all("Leave Ledger Entry", fields="*", filters=filters) self.assertEqual(len(leave_ledger_entry), 1) self.assertEqual(leave_ledger_entry[0].employee, compensatory_leave_request.employee) @@ -75,60 +91,67 @@ class TestCompensatoryLeaveRequest(unittest.TestCase): # check reverse leave ledger entry on cancellation compensatory_leave_request.cancel() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters, order_by = 'creation desc') + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=filters, order_by="creation desc" + ) self.assertEqual(len(leave_ledger_entry), 2) self.assertEqual(leave_ledger_entry[0].employee, compensatory_leave_request.employee) self.assertEqual(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) self.assertEqual(leave_ledger_entry[0].leaves, -1) + def get_compensatory_leave_request(employee, leave_date=today()): - prev_comp_leave_req = frappe.db.get_value('Compensatory Leave Request', - dict(leave_type='Compensatory Off', + prev_comp_leave_req = frappe.db.get_value( + "Compensatory Leave Request", + dict( + leave_type="Compensatory Off", work_from_date=leave_date, work_end_date=leave_date, - employee=employee), 'name') - if prev_comp_leave_req: - return frappe.get_doc('Compensatory Leave Request', prev_comp_leave_req) - - return frappe.get_doc(dict( - doctype='Compensatory Leave Request', employee=employee, - leave_type='Compensatory Off', + ), + "name", + ) + if prev_comp_leave_req: + return frappe.get_doc("Compensatory Leave Request", prev_comp_leave_req) + + return frappe.get_doc( + dict( + doctype="Compensatory Leave Request", + employee=employee, + leave_type="Compensatory Off", work_from_date=leave_date, work_end_date=leave_date, - reason='test' - )).insert() + reason="test", + ) + ).insert() -def mark_attendance(employee, date=today(), status='Present'): - if not frappe.db.exists(dict(doctype='Attendance', employee=employee.name, attendance_date=date, status='Present')): - attendance = frappe.get_doc({ - "doctype": "Attendance", - "employee": employee.name, - "attendance_date": date, - "status": status - }) + +def mark_attendance(employee, date=today(), status="Present"): + if not frappe.db.exists( + dict(doctype="Attendance", employee=employee.name, attendance_date=date, status="Present") + ): + attendance = frappe.get_doc( + {"doctype": "Attendance", "employee": employee.name, "attendance_date": date, "status": status} + ) attendance.save() attendance.submit() + def create_holiday_list(): if frappe.db.exists("Holiday List", "_Test Compensatory Leave"): return - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "from_date": add_months(today(), -3), - "to_date": add_months(today(), 3), - "holidays": [ - { - "description": "Test Holiday", - "holiday_date": today() - }, - { - "description": "Test Holiday 1", - "holiday_date": add_days(today(), -1) - } - ], - "holiday_list_name": "_Test Compensatory Leave" - }) + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "from_date": add_months(today(), -3), + "to_date": add_months(today(), 3), + "holidays": [ + {"description": "Test Holiday", "holiday_date": today()}, + {"description": "Test Holiday 1", "holiday_date": add_days(today(), -1)}, + ], + "holiday_list_name": "_Test Compensatory Leave", + } + ) holiday_list.save() diff --git a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py index 38e1f54ba96..d311188090d 100644 --- a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py +++ b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py @@ -12,53 +12,59 @@ from six import string_types class DailyWorkSummary(Document): def send_mails(self, dws_group, emails): - '''Send emails to get daily work summary to all users \ - in selected daily work summary group''' - incoming_email_account = frappe.db.get_value('Email Account', - dict(enable_incoming=1, default_incoming=1), - 'email_id') + """Send emails to get daily work summary to all users \ + in selected daily work summary group""" + incoming_email_account = frappe.db.get_value( + "Email Account", dict(enable_incoming=1, default_incoming=1), "email_id" + ) - self.db_set('email_sent_to', '\n'.join(emails)) - frappe.sendmail(recipients=emails, + self.db_set("email_sent_to", "\n".join(emails)) + frappe.sendmail( + recipients=emails, message=dws_group.message, subject=dws_group.subject, reference_doctype=self.doctype, reference_name=self.name, - reply_to=incoming_email_account) + reply_to=incoming_email_account, + ) def send_summary(self): - '''Send summary of all replies. Called at midnight''' + """Send summary of all replies. Called at midnight""" args = self.get_message_details() emails = get_user_emails_from_group(self.daily_work_summary_group) - frappe.sendmail(recipients=emails, - template='daily_work_summary', + frappe.sendmail( + recipients=emails, + template="daily_work_summary", args=args, subject=_(self.daily_work_summary_group), reference_doctype=self.doctype, - reference_name=self.name) + reference_name=self.name, + ) - self.db_set('status', 'Sent') + self.db_set("status", "Sent") def get_message_details(self): - '''Return args for template''' - dws_group = frappe.get_doc('Daily Work Summary Group', - self.daily_work_summary_group) + """Return args for template""" + dws_group = frappe.get_doc("Daily Work Summary Group", self.daily_work_summary_group) - replies = frappe.get_all('Communication', - fields=['content', 'text_content', 'sender'], - filters=dict(reference_doctype=self.doctype, + replies = frappe.get_all( + "Communication", + fields=["content", "text_content", "sender"], + filters=dict( + reference_doctype=self.doctype, reference_name=self.name, - communication_type='Communication', - sent_or_received='Received'), - order_by='creation asc') + communication_type="Communication", + sent_or_received="Received", + ), + order_by="creation asc", + ) did_not_reply = self.email_sent_to.split() for d in replies: - user = frappe.db.get_values("User", - {"email": d.sender}, - ["full_name", "user_image"], - as_dict=True) + user = frappe.db.get_values( + "User", {"email": d.sender}, ["full_name", "user_image"], as_dict=True + ) d.sender_name = user[0].full_name if user else d.sender d.image = user[0].image if user and user[0].image else None @@ -67,17 +73,13 @@ class DailyWorkSummary(Document): # make thumbnail image try: if original_image: - file_name = frappe.get_list('File', - {'file_url': original_image}) + file_name = frappe.get_list("File", {"file_url": original_image}) if file_name: file_name = file_name[0].name - file_doc = frappe.get_doc('File', file_name) + file_doc = frappe.get_doc("File", file_name) thumbnail_image = file_doc.make_thumbnail( - set_as_thumbnail=False, - width=100, - height=100, - crop=True + set_as_thumbnail=False, width=100, height=100, crop=True ) d.image = thumbnail_image except Exception: @@ -86,34 +88,33 @@ class DailyWorkSummary(Document): if d.sender in did_not_reply: did_not_reply.remove(d.sender) if d.text_content: - d.content = frappe.utils.md_to_html( - EmailReplyParser.parse_reply(d.text_content) - ) + d.content = frappe.utils.md_to_html(EmailReplyParser.parse_reply(d.text_content)) - did_not_reply = [(frappe.db.get_value("User", {"email": email}, "full_name") or email) - for email in did_not_reply] + did_not_reply = [ + (frappe.db.get_value("User", {"email": email}, "full_name") or email) for email in did_not_reply + ] - return dict(replies=replies, + return dict( + replies=replies, original_message=dws_group.message, - title=_('Work Summary for {0}').format( - global_date_format(self.creation) - ), - did_not_reply=', '.join(did_not_reply) or '', - did_not_reply_title=_('No replies from')) + title=_("Work Summary for {0}").format(global_date_format(self.creation)), + did_not_reply=", ".join(did_not_reply) or "", + did_not_reply_title=_("No replies from"), + ) def get_user_emails_from_group(group): - '''Returns list of email of enabled users from the given group + """Returns list of email of enabled users from the given group - :param group: Daily Work Summary Group `name`''' + :param group: Daily Work Summary Group `name`""" group_doc = group if isinstance(group_doc, string_types): - group_doc = frappe.get_doc('Daily Work Summary Group', group) + group_doc = frappe.get_doc("Daily Work Summary Group", group) emails = get_users_email(group_doc) return emails + def get_users_email(doc): - return [d.email for d in doc.users - if frappe.db.get_value("User", d.user, "enabled")] + return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")] diff --git a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py index 5edfb315564..703436529d0 100644 --- a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py +++ b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py @@ -9,82 +9,96 @@ import frappe.utils # test_records = frappe.get_test_records('Daily Work Summary') + class TestDailyWorkSummary(unittest.TestCase): def test_email_trigger(self): self.setup_and_prepare_test() for d in self.users: # check that email is sent to users if d.message: - self.assertTrue(d.email in [d.recipient for d in self.emails - if self.groups.subject in d.message]) + self.assertTrue( + d.email in [d.recipient for d in self.emails if self.groups.subject in d.message] + ) def test_email_trigger_failed(self): - hour = '00:00' - if frappe.utils.nowtime().split(':')[0] == '00': - hour = '01:00' + hour = "00:00" + if frappe.utils.nowtime().split(":")[0] == "00": + hour = "01:00" self.setup_and_prepare_test(hour) for d in self.users: # check that email is not sent to users - self.assertFalse(d.email in [d.recipient for d in self.emails - if self.groups.subject in d.message]) + self.assertFalse( + d.email in [d.recipient for d in self.emails if self.groups.subject in d.message] + ) def test_incoming(self): # get test mail with message-id as in-reply-to self.setup_and_prepare_test() with open(os.path.join(os.path.dirname(__file__), "test_data", "test-reply.raw"), "r") as f: - if not self.emails: return - test_mails = [f.read().replace('{{ sender }}', - self.users[-1].email).replace('{{ message_id }}', - self.emails[-1].message_id)] + if not self.emails: + return + test_mails = [ + f.read() + .replace("{{ sender }}", self.users[-1].email) + .replace("{{ message_id }}", self.emails[-1].message_id) + ] # pull the mail email_account = frappe.get_doc("Email Account", "_Test Email Account 1") - email_account.db_set('enable_incoming', 1) + email_account.db_set("enable_incoming", 1) email_account.receive(test_mails=test_mails) - daily_work_summary = frappe.get_doc('Daily Work Summary', - frappe.get_all('Daily Work Summary')[0].name) + daily_work_summary = frappe.get_doc( + "Daily Work Summary", frappe.get_all("Daily Work Summary")[0].name + ) args = daily_work_summary.get_message_details() - self.assertTrue('I built Daily Work Summary!' in args.get('replies')[0].content) + self.assertTrue("I built Daily Work Summary!" in args.get("replies")[0].content) def setup_and_prepare_test(self, hour=None): - frappe.db.sql('delete from `tabDaily Work Summary`') - frappe.db.sql('delete from `tabEmail Queue`') - frappe.db.sql('delete from `tabEmail Queue Recipient`') - frappe.db.sql('delete from `tabCommunication`') - frappe.db.sql('delete from `tabDaily Work Summary Group`') + frappe.db.sql("delete from `tabDaily Work Summary`") + frappe.db.sql("delete from `tabEmail Queue`") + frappe.db.sql("delete from `tabEmail Queue Recipient`") + frappe.db.sql("delete from `tabCommunication`") + frappe.db.sql("delete from `tabDaily Work Summary Group`") - self.users = frappe.get_all('User', - fields=['email'], - filters=dict(email=('!=', 'test@example.com'))) + self.users = frappe.get_all( + "User", fields=["email"], filters=dict(email=("!=", "test@example.com")) + ) self.setup_groups(hour) from erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group import trigger_emails + trigger_emails() # check if emails are created - self.emails = frappe.db.sql("""select r.recipient, q.message, q.message_id \ + self.emails = frappe.db.sql( + """select r.recipient, q.message, q.message_id \ from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \ - where q.name = r.parent""", as_dict=1) - + where q.name = r.parent""", + as_dict=1, + ) def setup_groups(self, hour=None): # setup email to trigger at this hour if not hour: - hour = frappe.utils.nowtime().split(':')[0] - hour = hour+':00' + hour = frappe.utils.nowtime().split(":")[0] + hour = hour + ":00" - groups = frappe.get_doc(dict(doctype="Daily Work Summary Group", - name="Daily Work Summary", - users=self.users, - send_emails_at=hour, - subject="this is a subject for testing summary emails", - message='this is a message for testing summary emails')) + groups = frappe.get_doc( + dict( + doctype="Daily Work Summary Group", + name="Daily Work Summary", + users=self.users, + send_emails_at=hour, + subject="this is a subject for testing summary emails", + message="this is a message for testing summary emails", + ) + ) groups.insert() self.groups = groups diff --git a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py index 306f43a418f..e8ec265464e 100644 --- a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py +++ b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py @@ -16,37 +16,41 @@ class DailyWorkSummaryGroup(Document): def validate(self): if self.users: if not frappe.flags.in_test and not is_incoming_account_enabled(): - frappe.throw(_('Please enable default incoming account before creating Daily Work Summary Group')) + frappe.throw( + _("Please enable default incoming account before creating Daily Work Summary Group") + ) def trigger_emails(): - '''Send emails to Employees at the given hour asking - them what did they work on today''' + """Send emails to Employees at the given hour asking + them what did they work on today""" groups = frappe.get_all("Daily Work Summary Group") for d in groups: group_doc = frappe.get_doc("Daily Work Summary Group", d) - if (is_current_hour(group_doc.send_emails_at) + if ( + is_current_hour(group_doc.send_emails_at) and not is_holiday(group_doc.holiday_list) - and group_doc.enabled): + and group_doc.enabled + ): emails = get_user_emails_from_group(group_doc) # find emails relating to a company if emails: daily_work_summary = frappe.get_doc( - dict(doctype='Daily Work Summary', daily_work_summary_group=group_doc.name) + dict(doctype="Daily Work Summary", daily_work_summary_group=group_doc.name) ).insert() daily_work_summary.send_mails(group_doc, emails) def is_current_hour(hour): - return frappe.utils.nowtime().split(':')[0] == hour.split(':')[0] + return frappe.utils.nowtime().split(":")[0] == hour.split(":")[0] def send_summary(): - '''Send summary to everyone''' - for d in frappe.get_all('Daily Work Summary', dict(status='Open')): - daily_work_summary = frappe.get_doc('Daily Work Summary', d.name) + """Send summary to everyone""" + for d in frappe.get_all("Daily Work Summary", dict(status="Open")): + daily_work_summary = frappe.get_doc("Daily Work Summary", d.name) daily_work_summary.send_summary() def is_incoming_account_enabled(): - return frappe.db.get_value('Email Account', dict(enable_incoming=1, default_incoming=1)) + return frappe.db.get_value("Email Account", dict(enable_incoming=1, default_incoming=1)) diff --git a/erpnext/hr/doctype/department/department.py b/erpnext/hr/doctype/department/department.py index ed0bfcf0d5a..a9806c529f6 100644 --- a/erpnext/hr/doctype/department/department.py +++ b/erpnext/hr/doctype/department/department.py @@ -9,7 +9,7 @@ from erpnext.utilities.transaction_base import delete_events class Department(NestedSet): - nsm_parent_field = 'parent_department' + nsm_parent_field = "parent_department" def autoname(self): root = get_root_of("Department") @@ -26,7 +26,7 @@ class Department(NestedSet): def before_rename(self, old, new, merge=False): # renaming consistency with abbreviation - if not frappe.get_cached_value('Company', self.company, 'abbr') in new: + if not frappe.get_cached_value("Company", self.company, "abbr") in new: new = get_abbreviated_name(new, self.company) return new @@ -39,17 +39,20 @@ class Department(NestedSet): super(Department, self).on_trash() delete_events(self.doctype, self.name) + def on_doctype_update(): frappe.db.add_index("Department", ["lft", "rgt"]) + def get_abbreviated_name(name, company): - abbr = frappe.get_cached_value('Company', company, 'abbr') - new_name = '{0} - {1}'.format(name, abbr) + abbr = frappe.get_cached_value("Company", company, "abbr") + new_name = "{0} - {1}".format(name, abbr) return new_name + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): - condition = '' + condition = "" var_dict = { "name": get_root_of("Department"), "parent": parent, @@ -62,18 +65,26 @@ def get_children(doctype, parent=None, company=None, is_root=False): else: condition = "parent_department = %(parent)s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name as value, is_group as expandable from `tab{doctype}` where {condition} - order by name""".format(doctype=doctype, condition=condition), var_dict, as_dict=1) + order by name""".format( + doctype=doctype, condition=condition + ), + var_dict, + as_dict=1, + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict args = make_tree_args(**args) diff --git a/erpnext/hr/doctype/department/test_department.py b/erpnext/hr/doctype/department/test_department.py index 95bf6635011..b8c043f0b2d 100644 --- a/erpnext/hr/doctype/department/test_department.py +++ b/erpnext/hr/doctype/department/test_department.py @@ -6,20 +6,26 @@ import unittest import frappe test_ignore = ["Leave Block List"] + + class TestDepartment(unittest.TestCase): - def test_remove_department_data(self): - doc = create_department("Test Department") - frappe.delete_doc('Department', doc.name) + def test_remove_department_data(self): + doc = create_department("Test Department") + frappe.delete_doc("Department", doc.name) + def create_department(department_name, parent_department=None): - doc = frappe.get_doc({ - 'doctype': 'Department', - 'is_group': 0, - 'parent_department': parent_department, - 'department_name': department_name, - 'company': frappe.defaults.get_defaults().company - }).insert() + doc = frappe.get_doc( + { + "doctype": "Department", + "is_group": 0, + "parent_department": parent_department, + "department_name": department_name, + "company": frappe.defaults.get_defaults().company, + } + ).insert() - return doc + return doc -test_records = frappe.get_test_records('Department') + +test_records = frappe.get_test_records("Department") diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 375ae7c70ae..d849900ef2d 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -10,6 +10,7 @@ from frappe.model.document import Document class DepartmentApprover(Document): pass + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_approvers(doctype, txt, searchfield, start, page_len, filters): @@ -20,25 +21,44 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True) + employee = frappe.get_value( + "Employee", + filters.get("employee"), + ["employee_name", "department", "leave_approver", "expense_approver", "shift_request_approver"], + as_dict=True, + ) employee_department = filters.get("department") or employee.department if employee_department: - department_details = frappe.db.get_value("Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True) + department_details = frappe.db.get_value( + "Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True + ) if department_details: - department_list = frappe.db.sql("""select name from `tabDepartment` where lft <= %s + department_list = frappe.db.sql( + """select name from `tabDepartment` where lft <= %s and rgt >= %s and disabled=0 - order by lft desc""", (department_details.lft, department_details.rgt), as_list=True) + order by lft desc""", + (department_details.lft, department_details.rgt), + as_list=True, + ) if filters.get("doctype") == "Leave Application" and employee.leave_approver: - approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name'])) + approvers.append( + frappe.db.get_value("User", employee.leave_approver, ["name", "first_name", "last_name"]) + ) if filters.get("doctype") == "Expense Claim" and employee.expense_approver: - approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name'])) + approvers.append( + frappe.db.get_value("User", employee.expense_approver, ["name", "first_name", "last_name"]) + ) if filters.get("doctype") == "Shift Request" and employee.shift_request_approver: - approvers.append(frappe.db.get_value("User", employee.shift_request_approver, ['name', 'first_name', 'last_name'])) + approvers.append( + frappe.db.get_value( + "User", employee.shift_request_approver, ["name", "first_name", "last_name"] + ) + ) if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" @@ -51,15 +71,21 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): field_name = "Shift Request Approver" if department_list: for d in department_list: - approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from + approvers += frappe.db.sql( + """select user.name, user.first_name, user.last_name from tabUser user, `tabDepartment Approver` approver where approver.parent = %s and user.name like %s and approver.parentfield = %s - and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) + and approver.approver=user.name""", + (d, "%" + txt + "%", parentfield), + as_list=True, + ) if len(approvers) == 0: - error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name)) + error_msg = _("Please set {0} for the Employee: {1}").format( + field_name, frappe.bold(employee.employee_name) + ) if department_list: error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department)) frappe.throw(error_msg, title=_(field_name + " Missing")) diff --git a/erpnext/hr/doctype/designation/test_designation.py b/erpnext/hr/doctype/designation/test_designation.py index f2d6d36ff88..0840d13d1f3 100644 --- a/erpnext/hr/doctype/designation/test_designation.py +++ b/erpnext/hr/doctype/designation/test_designation.py @@ -5,15 +5,18 @@ import frappe # test_records = frappe.get_test_records('Designation') -def create_designation(**args): - args = frappe._dict(args) - if frappe.db.exists("Designation", args.designation_name or "_Test designation"): - return frappe.get_doc("Designation", args.designation_name or "_Test designation") - designation = frappe.get_doc({ - "doctype": "Designation", - "designation_name": args.designation_name or "_Test designation", - "description": args.description or "_Test description" - }) - designation.save() - return designation +def create_designation(**args): + args = frappe._dict(args) + if frappe.db.exists("Designation", args.designation_name or "_Test designation"): + return frappe.get_doc("Designation", args.designation_name or "_Test designation") + + designation = frappe.get_doc( + { + "doctype": "Designation", + "designation_name": args.designation_name or "_Test designation", + "description": args.description or "_Test description", + } + ) + designation.save() + return designation diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 8a2950696af..d24e7038422 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -18,22 +18,25 @@ from erpnext.utilities.transaction_base import delete_events class EmployeeUserDisabledError(frappe.ValidationError): pass + + class InactiveEmployeeStatusError(frappe.ValidationError): pass + class Employee(NestedSet): - nsm_parent_field = 'reports_to' + nsm_parent_field = "reports_to" def autoname(self): naming_method = frappe.db.get_value("HR Settings", None, "emp_created_by") if not naming_method: throw(_("Please setup Employee Naming System in Human Resource > HR Settings")) else: - if naming_method == 'Naming Series': + if naming_method == "Naming Series": set_name_by_naming_series(self) - elif naming_method == 'Employee Number': + elif naming_method == "Employee Number": self.name = self.employee_number - elif naming_method == 'Full Name': + elif naming_method == "Full Name": self.set_employee_name() self.name = self.employee_name @@ -41,6 +44,7 @@ class Employee(NestedSet): def validate(self): from erpnext.controllers.status_updater import validate_status + validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"]) self.employee = self.name @@ -58,19 +62,19 @@ class Employee(NestedSet): else: existing_user_id = frappe.db.get_value("Employee", self.name, "user_id") if existing_user_id: - remove_user_permission( - "Employee", self.name, existing_user_id) + remove_user_permission("Employee", self.name, existing_user_id) def after_rename(self, old, new, merge): self.db_set("employee", new) def set_employee_name(self): - self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name])) + self.employee_name = " ".join( + filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]) + ) def validate_user_details(self): if self.user_id: - data = frappe.db.get_value("User", - self.user_id, ["enabled", "user_image"], as_dict=1) + data = frappe.db.get_value("User", self.user_id, ["enabled", "user_image"], as_dict=1) if not data: self.user_id = None @@ -93,14 +97,14 @@ class Employee(NestedSet): self.update_approver_role() def update_user_permissions(self): - if not self.create_user_permission: return - if not has_permission('User Permission', ptype='write', raise_exception=False): return + if not self.create_user_permission: + return + if not has_permission("User Permission", ptype="write", raise_exception=False): + return - employee_user_permission_exists = frappe.db.exists('User Permission', { - 'allow': 'Employee', - 'for_value': self.name, - 'user': self.user_id - }) + 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 @@ -137,12 +141,14 @@ class Employee(NestedSet): if not user.user_image: user.user_image = self.image try: - frappe.get_doc({ - "doctype": "File", - "file_url": self.image, - "attached_to_doctype": "User", - "attached_to_name": self.user_id - }).insert() + frappe.get_doc( + { + "doctype": "File", + "file_url": self.image, + "attached_to_doctype": "User", + "attached_to_name": self.user_id, + } + ).insert() except frappe.DuplicateEntryError: # already exists pass @@ -164,16 +170,32 @@ class Employee(NestedSet): if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()): throw(_("Date of Birth cannot be greater than today.")) - if self.date_of_birth and self.date_of_joining and getdate(self.date_of_birth) >= getdate(self.date_of_joining): + if ( + self.date_of_birth + and self.date_of_joining + and getdate(self.date_of_birth) >= getdate(self.date_of_joining) + ): throw(_("Date of Joining must be greater than Date of Birth")) - elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)): + elif ( + self.date_of_retirement + and self.date_of_joining + and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)) + ): throw(_("Date Of Retirement must be greater than Date of Joining")) - elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)): + elif ( + self.relieving_date + and self.date_of_joining + and (getdate(self.relieving_date) < getdate(self.date_of_joining)) + ): throw(_("Relieving Date must be greater than or equal to Date of Joining")) - elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)): + elif ( + self.contract_end_date + and self.date_of_joining + and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)) + ): throw(_("Contract End Date must be greater than Date of Joining")) def validate_email(self): @@ -189,14 +211,20 @@ class Employee(NestedSet): self.prefered_email = preferred_email def validate_status(self): - if self.status == 'Left': - reports_to = frappe.db.get_all('Employee', - filters={'reports_to': self.name, 'status': "Active"}, - fields=['name','employee_name'] + if self.status == "Left": + reports_to = frappe.db.get_all( + "Employee", + filters={"reports_to": self.name, "status": "Active"}, + fields=["name", "employee_name"], ) if reports_to: - link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to] - message = _("The following employees are currently still reporting to {0}:").format(frappe.bold(self.employee_name)) + link_to_employees = [ + frappe.utils.get_link_to_form("Employee", employee.name, label=employee.employee_name) + for employee in reports_to + ] + message = _("The following employees are currently still reporting to {0}:").format( + frappe.bold(self.employee_name) + ) message += "

    • " + "
    • ".join(link_to_employees) message += "

    " message += _("Please make sure the employees above report to another Active employee.") @@ -205,7 +233,7 @@ class Employee(NestedSet): throw(_("Please enter relieving date.")) def validate_for_enabled_user_id(self, enabled): - if not self.status == 'Active': + if not self.status == "Active": return if enabled is None: @@ -214,11 +242,16 @@ class Employee(NestedSet): frappe.throw(_("User {0} is disabled").format(self.user_id), EmployeeUserDisabledError) def validate_duplicate_user_id(self): - employee = frappe.db.sql_list("""select name from `tabEmployee` where - user_id=%s and status='Active' and name!=%s""", (self.user_id, self.name)) + employee = frappe.db.sql_list( + """select name from `tabEmployee` where + user_id=%s and status='Active' and name!=%s""", + (self.user_id, self.name), + ) if employee: - throw(_("User {0} is already assigned to Employee {1}").format( - self.user_id, employee[0]), frappe.DuplicateEntryError) + throw( + _("User {0} is already assigned to Employee {1}").format(self.user_id, employee[0]), + frappe.DuplicateEntryError, + ) def validate_reports_to(self): if self.reports_to == self.name: @@ -227,17 +260,25 @@ class Employee(NestedSet): def on_trash(self): self.update_nsm_model() delete_events(self.doctype, self.name) - if frappe.db.exists("Employee Transfer", {'new_employee_id': self.name, 'docstatus': 1}): - emp_transfer = frappe.get_doc("Employee Transfer", {'new_employee_id': self.name, 'docstatus': 1}) - emp_transfer.db_set("new_employee_id", '') + if frappe.db.exists("Employee Transfer", {"new_employee_id": self.name, "docstatus": 1}): + emp_transfer = frappe.get_doc( + "Employee Transfer", {"new_employee_id": self.name, "docstatus": 1} + ) + emp_transfer.db_set("new_employee_id", "") def validate_preferred_email(self): if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)): frappe.msgprint(_("Please enter {0}").format(self.prefered_contact_email)) def validate_onboarding_process(self): - employee_onboarding = frappe.get_all("Employee Onboarding", - filters={"job_applicant": self.job_applicant, "docstatus": 1, "boarding_status": ("!=", "Completed")}) + employee_onboarding = frappe.get_all( + "Employee Onboarding", + filters={ + "job_applicant": self.job_applicant, + "docstatus": 1, + "boarding_status": ("!=", "Completed"), + }, + ) if employee_onboarding: doc = frappe.get_doc("Employee Onboarding", employee_onboarding[0].name) doc.validate_employee_creation() @@ -245,20 +286,26 @@ class Employee(NestedSet): def reset_employee_emails_cache(self): prev_doc = self.get_doc_before_save() or {} - cell_number = cstr(self.get('cell_number')) - prev_number = cstr(prev_doc.get('cell_number')) - if (cell_number != prev_number or - self.get('user_id') != prev_doc.get('user_id')): - frappe.cache().hdel('employees_with_number', cell_number) - frappe.cache().hdel('employees_with_number', prev_number) + cell_number = cstr(self.get("cell_number")) + prev_number = cstr(prev_doc.get("cell_number")) + if cell_number != prev_number or self.get("user_id") != prev_doc.get("user_id"): + frappe.cache().hdel("employees_with_number", cell_number) + frappe.cache().hdel("employees_with_number", prev_number) + def get_timeline_data(doctype, name): - '''Return timeline for attendance''' - return dict(frappe.db.sql('''select unix_timestamp(attendance_date), count(*) + """Return timeline for attendance""" + return dict( + frappe.db.sql( + """select unix_timestamp(attendance_date), count(*) from `tabAttendance` where employee=%s and attendance_date > date_sub(curdate(), interval 1 year) and status in ('Present', 'Half Day') - group by attendance_date''', name)) + group by attendance_date""", + name, + ) + ) + @frappe.whitelist() def get_retirement_date(date_of_birth=None): @@ -266,14 +313,15 @@ def get_retirement_date(date_of_birth=None): if date_of_birth: try: retirement_age = int(frappe.db.get_single_value("HR Settings", "retirement_age") or 60) - dt = add_years(getdate(date_of_birth),retirement_age) - ret = {'date_of_retirement': dt.strftime('%Y-%m-%d')} + dt = add_years(getdate(date_of_birth), retirement_age) + ret = {"date_of_retirement": dt.strftime("%Y-%m-%d")} except ValueError: # invalid date ret = {} return ret + def validate_employee_role(doc, method): # called via User hook if "Employee" in [d.role for d in doc.get("roles")]: @@ -281,39 +329,52 @@ def validate_employee_role(doc, method): frappe.msgprint(_("Please set User ID field in an Employee record to set Employee Role")) doc.get("roles").remove(doc.get("roles", {"role": "Employee"})[0]) + def update_user_permissions(doc, method): # called via User hook if "Employee" in [d.role for d in doc.get("roles")]: - if not has_permission('User Permission', ptype='write', raise_exception=False): return + if not has_permission("User Permission", ptype="write", raise_exception=False): + return employee = frappe.get_doc("Employee", {"user_id": doc.name}) employee.update_user_permissions() + def get_employee_email(employee_doc): - return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email") + return ( + employee_doc.get("user_id") + or employee_doc.get("personal_email") + or employee_doc.get("company_email") + ) + def get_holiday_list_for_employee(employee, raise_exception=True): if employee: holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"]) else: - holiday_list='' - company=frappe.db.get_value("Global Defaults", None, "default_company") + holiday_list = "" + company = frappe.db.get_value("Global Defaults", None, "default_company") if not holiday_list: - holiday_list = frappe.get_cached_value('Company', company, "default_holiday_list") + holiday_list = frappe.get_cached_value("Company", company, "default_holiday_list") if not holiday_list and raise_exception: - frappe.throw(_('Please set a default Holiday List for Employee {0} or Company {1}').format(employee, company)) + frappe.throw( + _("Please set a default Holiday List for Employee {0} or Company {1}").format(employee, company) + ) return holiday_list -def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False): - ''' + +def is_holiday( + employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False +): + """ Returns True if given Employee has an holiday on the given date - :param employee: Employee `name` - :param date: Date to check. Will check for today if None - :param raise_exception: Raise an exception if no holiday list found, default is True - :param only_non_weekly: Check only non-weekly holidays, default is False - ''' + :param employee: Employee `name` + :param date: Date to check. Will check for today if None + :param raise_exception: Raise an exception if no holiday list found, default is True + :param only_non_weekly: Check only non-weekly holidays, default is False + """ holiday_list = get_holiday_list_for_employee(employee, raise_exception) if not date: @@ -322,34 +383,28 @@ def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, if not holiday_list: return False - filters = { - 'parent': holiday_list, - 'holiday_date': date - } + filters = {"parent": holiday_list, "holiday_date": date} if only_non_weekly: - filters['weekly_off'] = False + filters["weekly_off"] = False - holidays = frappe.get_all( - 'Holiday', - fields=['description'], - filters=filters, - pluck='description' - ) + holidays = frappe.get_all("Holiday", fields=["description"], filters=filters, pluck="description") if with_description: return len(holidays) > 0, holidays return len(holidays) > 0 + @frappe.whitelist() -def deactivate_sales_person(status = None, employee = None): +def deactivate_sales_person(status=None, employee=None): if status == "Left": sales_person = frappe.db.get_value("Sales Person", {"Employee": employee}) if sales_person: frappe.db.set_value("Sales Person", sales_person, "enabled", 0) + @frappe.whitelist() -def create_user(employee, user = None, email=None): +def create_user(employee, user=None, email=None): emp = frappe.get_doc("Employee", employee) employee_name = emp.employee_name.split(" ") @@ -367,93 +422,98 @@ def create_user(employee, user = None, email=None): emp.prefered_email = email user = frappe.new_doc("User") - user.update({ - "name": emp.employee_name, - "email": emp.prefered_email, - "enabled": 1, - "first_name": first_name, - "middle_name": middle_name, - "last_name": last_name, - "gender": emp.gender, - "birth_date": emp.date_of_birth, - "phone": emp.cell_number, - "bio": emp.bio - }) + user.update( + { + "name": emp.employee_name, + "email": emp.prefered_email, + "enabled": 1, + "first_name": first_name, + "middle_name": middle_name, + "last_name": last_name, + "gender": emp.gender, + "birth_date": emp.date_of_birth, + "phone": emp.cell_number, + "bio": emp.bio, + } + ) user.insert() return user.name + def get_all_employee_emails(company): - '''Returns list of employee emails either based on user_id or company_email''' - employee_list = frappe.get_all('Employee', - fields=['name','employee_name'], - filters={ - 'status': 'Active', - 'company': company - } + """Returns list of employee emails either based on user_id or company_email""" + employee_list = frappe.get_all( + "Employee", fields=["name", "employee_name"], filters={"status": "Active", "company": company} ) employee_emails = [] for employee in employee_list: if not employee: continue - user, company_email, personal_email = frappe.db.get_value('Employee', - employee, ['user_id', 'company_email', 'personal_email']) + user, company_email, personal_email = frappe.db.get_value( + "Employee", employee, ["user_id", "company_email", "personal_email"] + ) email = user or company_email or personal_email if email: employee_emails.append(email) return employee_emails + def get_employee_emails(employee_list): - '''Returns list of employee emails either based on user_id or company_email''' + """Returns list of employee emails either based on user_id or company_email""" employee_emails = [] for employee in employee_list: if not employee: continue - user, company_email, personal_email = frappe.db.get_value('Employee', employee, - ['user_id', 'company_email', 'personal_email']) + user, company_email, personal_email = frappe.db.get_value( + "Employee", employee, ["user_id", "company_email", "personal_email"] + ) email = user or company_email or personal_email if email: employee_emails.append(email) return employee_emails + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): - filters = [['status', '=', 'Active']] - if company and company != 'All Companies': - filters.append(['company', '=', company]) + filters = [["status", "=", "Active"]] + if company and company != "All Companies": + filters.append(["company", "=", company]) - fields = ['name as value', 'employee_name as title'] + fields = ["name as value", "employee_name as title"] if is_root: - parent = '' - if parent and company and parent!=company: - filters.append(['reports_to', '=', parent]) + parent = "" + if parent and company and parent != company: + filters.append(["reports_to", "=", parent]) else: - filters.append(['reports_to', '=', '']) + filters.append(["reports_to", "=", ""]) - employees = frappe.get_list(doctype, fields=fields, - filters=filters, order_by='name') + employees = frappe.get_list(doctype, fields=fields, filters=filters, order_by="name") for employee in employees: - is_expandable = frappe.get_all(doctype, filters=[ - ['reports_to', '=', employee.get('value')] - ]) + is_expandable = frappe.get_all(doctype, filters=[["reports_to", "=", employee.get("value")]]) employee.expandable = 1 if is_expandable else 0 return employees + def on_doctype_update(): frappe.db.add_index("Employee", ["lft", "rgt"]) -def has_user_permission_for_employee(user_name, employee_name): - return frappe.db.exists({ - 'doctype': 'User Permission', - 'user': user_name, - 'allow': 'Employee', - 'for_value': employee_name - }) -def has_upload_permission(doc, ptype='read', user=None): +def has_user_permission_for_employee(user_name, employee_name): + return frappe.db.exists( + { + "doctype": "User Permission", + "user": user_name, + "allow": "Employee", + "for_value": employee_name, + } + ) + + +def has_upload_permission(doc, ptype="read", user=None): if not user: user = frappe.session.user if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype): diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 0aaff52ee28..d7235418a23 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -1,52 +1,46 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on the attendance of this Employee'), - 'fieldname': 'employee', - 'non_standard_fieldnames': { - 'Bank Account': 'party', - 'Employee Grievance': 'raised_by' - }, - 'transactions': [ + "heatmap": True, + "heatmap_message": _("This is based on the attendance of this Employee"), + "fieldname": "employee", + "non_standard_fieldnames": {"Bank Account": "party", "Employee Grievance": "raised_by"}, + "transactions": [ + {"label": _("Attendance"), "items": ["Attendance", "Attendance Request", "Employee Checkin"]}, { - 'label': _('Attendance'), - 'items': ['Attendance', 'Attendance Request', 'Employee Checkin'] + "label": _("Leave"), + "items": ["Leave Application", "Leave Allocation", "Leave Policy Assignment"], }, { - 'label': _('Leave'), - 'items': ['Leave Application', 'Leave Allocation', 'Leave Policy Assignment'] + "label": _("Lifecycle"), + "items": [ + "Employee Transfer", + "Employee Promotion", + "Employee Separation", + "Employee Grievance", + ], + }, + {"label": _("Shift"), "items": ["Shift Request", "Shift Assignment"]}, + {"label": _("Expense"), "items": ["Expense Claim", "Travel Request", "Employee Advance"]}, + {"label": _("Benefit"), "items": ["Employee Benefit Application", "Employee Benefit Claim"]}, + { + "label": _("Payroll"), + "items": [ + "Salary Structure Assignment", + "Salary Slip", + "Additional Salary", + "Timesheet", + "Employee Incentive", + "Retention Bonus", + "Bank Account", + ], }, { - 'label': _('Lifecycle'), - 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance'] + "label": _("Training"), + "items": ["Training Event", "Training Result", "Training Feedback", "Employee Skill Map"], }, - { - 'label': _('Shift'), - 'items': ['Shift Request', 'Shift Assignment'] - }, - { - 'label': _('Expense'), - 'items': ['Expense Claim', 'Travel Request', 'Employee Advance'] - }, - { - 'label': _('Benefit'), - 'items': ['Employee Benefit Application', 'Employee Benefit Claim'] - }, - { - 'label': _('Payroll'), - 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account'] - }, - { - 'label': _('Training'), - 'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map'] - }, - { - 'label': _('Evaluation'), - 'items': ['Appraisal'] - }, - ] + {"label": _("Evaluation"), "items": ["Appraisal"]}, + ], } diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py index 0bb66374d1e..1829bc4f2fc 100644 --- a/erpnext/hr/doctype/employee/employee_reminders.py +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -44,13 +44,10 @@ def send_advance_holiday_reminders(frequency): else: return - employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name') + employees = frappe.db.get_all("Employee", filters={"status": "Active"}, pluck="name") for employee in employees: holidays = get_holidays_for_employee( - employee, - start_date, end_date, - only_non_weekly=True, - raise_exception=False + employee, start_date, end_date, only_non_weekly=True, raise_exception=False ) send_holidays_reminder_in_advance(employee, holidays) @@ -60,7 +57,7 @@ def send_holidays_reminder_in_advance(employee, holidays): if not holidays: return - employee_doc = frappe.get_doc('Employee', employee) + employee_doc = frappe.get_doc("Employee", employee) employee_email = get_employee_email(employee_doc) frequency = frappe.db.get_single_value("HR Settings", "frequency") @@ -70,15 +67,18 @@ def send_holidays_reminder_in_advance(employee, holidays): subject=_("Upcoming Holidays Reminder"), template="holiday_reminder", args=dict( - reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')), + reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format( + employee_doc.get("first_name") + ), message=_("Below is the list of upcoming holidays for you:"), advance_holiday_reminder=True, holidays=holidays, - frequency=frequency[:-2] + frequency=frequency[:-2], ), - header=email_header + header=email_header, ) + # ------------------ # BIRTHDAY REMINDERS # ------------------ @@ -109,10 +109,10 @@ def send_birthday_reminders(): def get_birthday_reminder_text_and_message(birthday_persons): if len(birthday_persons) == 1: - birthday_person_text = birthday_persons[0]['name'] + birthday_person_text = birthday_persons[0]["name"] else: # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim - person_names = [d['name'] for d in birthday_persons] + person_names = [d["name"] for d in birthday_persons] birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False) reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text) @@ -133,7 +133,7 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message) birthday_persons=birthday_persons, message=message, ), - header=_("Birthday Reminder 🎂") + header=_("Birthday Reminder 🎂"), ) @@ -150,15 +150,16 @@ def get_employees_having_an_event_today(event_type): from collections import defaultdict # Set column based on event type - if event_type == 'birthday': - condition_column = 'date_of_birth' - elif event_type == 'work_anniversary': - condition_column = 'date_of_joining' + if event_type == "birthday": + condition_column = "date_of_birth" + elif event_type == "work_anniversary": + condition_column = "date_of_joining" else: return - employees_born_today = frappe.db.multisql({ - "mariadb": f""" + employees_born_today = frappe.db.multisql( + { + "mariadb": f""" SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining` FROM `tabEmployee` WHERE @@ -170,7 +171,7 @@ def get_employees_having_an_event_today(event_type): AND `status` = 'Active' """, - "postgres": f""" + "postgres": f""" SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image" FROM "tabEmployee" WHERE @@ -182,12 +183,15 @@ def get_employees_having_an_event_today(event_type): AND "status" = 'Active' """, - }, dict(today=today(), condition_column=condition_column), as_dict=1) + }, + dict(today=today(), condition_column=condition_column), + as_dict=1, + ) grouped_employees = defaultdict(lambda: []) for employee_doc in employees_born_today: - grouped_employees[employee_doc.get('company')].append(employee_doc) + grouped_employees[employee_doc.get("company")].append(employee_doc) return grouped_employees @@ -222,19 +226,19 @@ def send_work_anniversary_reminders(): def get_work_anniversary_reminder_text_and_message(anniversary_persons): if len(anniversary_persons) == 1: - anniversary_person = anniversary_persons[0]['name'] + anniversary_person = anniversary_persons[0]["name"] persons_name = anniversary_person # Number of years completed at the company - completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year + completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year anniversary_person += f" completed {completed_years} year(s)" else: person_names_with_years = [] names = [] for person in anniversary_persons: - person_text = person['name'] + person_text = person["name"] names.append(person_text) # Number of years completed at the company - completed_years = getdate().year - person['date_of_joining'].year + completed_years = getdate().year - person["date_of_joining"].year person_text += f" completed {completed_years} year(s)" person_names_with_years.append(person_text) @@ -260,5 +264,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person anniversary_persons=anniversary_persons, message=message, ), - header=_("Work Anniversary Reminder") + header=_("Work Anniversary Reminder"), ) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 67cbea67e1f..50894b61716 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -9,7 +9,8 @@ import frappe.utils import erpnext from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError -test_records = frappe.get_test_records('Employee') +test_records = frappe.get_test_records("Employee") + class TestEmployee(unittest.TestCase): def test_employee_status_left(self): @@ -21,7 +22,7 @@ class TestEmployee(unittest.TestCase): employee2_doc.reports_to = employee1_doc.name employee2_doc.save() employee1_doc.reload() - employee1_doc.status = 'Left' + employee1_doc.status = "Left" self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save) def test_employee_status_inactive(self): @@ -36,11 +37,19 @@ class TestEmployee(unittest.TestCase): employee_doc.reload() make_holiday_list() - frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List" + ) - frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""") - salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly", - employee=employee_doc.name, company=employee_doc.company) + frappe.db.sql( + """delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""" + ) + salary_structure = make_salary_structure( + "Test Inactive Employee Salary Slip", + "Monthly", + employee=employee_doc.name, + company=employee_doc.company, + ) salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name) self.assertRaises(InactiveEmployeeStatusError, salary_slip.save) @@ -48,38 +57,43 @@ class TestEmployee(unittest.TestCase): def tearDown(self): frappe.db.rollback() + def make_employee(user, company=None, **kwargs): if not frappe.db.get_value("User", user): - frappe.get_doc({ - "doctype": "User", - "email": user, - "first_name": user, - "new_password": "password", - "send_welcome_email": 0, - "roles": [{"doctype": "Has Role", "role": "Employee"}] - }).insert() + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": user, + "new_password": "password", + "send_welcome_email": 0, + "roles": [{"doctype": "Has Role", "role": "Employee"}], + } + ).insert() if not frappe.db.get_value("Employee", {"user_id": user}): - employee = frappe.get_doc({ - "doctype": "Employee", - "naming_series": "EMP-", - "first_name": user, - "company": company or erpnext.get_default_company(), - "user_id": user, - "date_of_birth": "1990-05-08", - "date_of_joining": "2013-01-01", - "department": frappe.get_all("Department", fields="name")[0].name, - "gender": "Female", - "company_email": user, - "prefered_contact_email": "Company Email", - "prefered_email": user, - "status": "Active", - "employment_type": "Intern" - }) + employee = frappe.get_doc( + { + "doctype": "Employee", + "naming_series": "EMP-", + "first_name": user, + "company": company or erpnext.get_default_company(), + "user_id": user, + "date_of_birth": "1990-05-08", + "date_of_joining": "2013-01-01", + "department": frappe.get_all("Department", fields="name")[0].name, + "gender": "Female", + "company_email": user, + "prefered_contact_email": "Company Email", + "prefered_email": user, + "status": "Active", + "employment_type": "Intern", + } + ) if kwargs: employee.update(kwargs) employee.insert() return employee.name else: - frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active") - return frappe.get_value("Employee", {"employee_name":user}, "name") + frappe.db.set_value("Employee", {"employee_name": user}, "status", "Active") + return frappe.get_value("Employee", {"employee_name": user}, "name") diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py index a4097ab9d19..9bde77c9569 100644 --- a/erpnext/hr/doctype/employee/test_employee_reminders.py +++ b/erpnext/hr/doctype/employee/test_employee_reminders.py @@ -21,23 +21,22 @@ class TestEmployeeReminders(unittest.TestCase): # Create a test holiday list test_holiday_dates = cls.get_test_holiday_dates() test_holiday_list = make_holiday_list( - 'TestHolidayRemindersList', + "TestHolidayRemindersList", holiday_dates=[ - {'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'}, - {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'}, - {'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1}, - {'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'}, - {'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'}, - {'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'}, + {"holiday_date": test_holiday_dates[0], "description": "test holiday1"}, + {"holiday_date": test_holiday_dates[1], "description": "test holiday2"}, + {"holiday_date": test_holiday_dates[2], "description": "test holiday3", "weekly_off": 1}, + {"holiday_date": test_holiday_dates[3], "description": "test holiday4"}, + {"holiday_date": test_holiday_dates[4], "description": "test holiday5"}, + {"holiday_date": test_holiday_dates[5], "description": "test holiday6"}, ], - from_date=getdate()-timedelta(days=10), - to_date=getdate()+timedelta(weeks=5) + from_date=getdate() - timedelta(days=10), + to_date=getdate() + timedelta(weeks=5), ) # Create a test employee test_employee = frappe.get_doc( - 'Employee', - make_employee('test@gopher.io', company="_Test Company") + "Employee", make_employee("test@gopher.io", company="_Test Company") ) # Attach the holiday list to employee @@ -49,16 +48,16 @@ class TestEmployeeReminders(unittest.TestCase): cls.test_holiday_dates = test_holiday_dates # Employee without holidays in this month/week - test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company") - test_employee_2 = frappe.get_doc('Employee', test_employee_2) + test_employee_2 = make_employee("test@empwithoutholiday.io", company="_Test Company") + test_employee_2 = frappe.get_doc("Employee", test_employee_2) test_holiday_list = make_holiday_list( - 'TestHolidayRemindersList2', + "TestHolidayRemindersList2", holiday_dates=[ - {'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'}, + {"holiday_date": add_months(getdate(), 1), "description": "test holiday1"}, ], from_date=add_months(getdate(), -2), - to_date=add_months(getdate(), 2) + to_date=add_months(getdate(), 2), ) test_employee_2.holiday_list = test_holiday_list.name test_employee_2.save() @@ -71,11 +70,11 @@ class TestEmployeeReminders(unittest.TestCase): today_date = getdate() return [ today_date, - today_date-timedelta(days=4), - today_date-timedelta(days=3), - today_date+timedelta(days=1), - today_date+timedelta(days=3), - today_date+timedelta(weeks=3) + today_date - timedelta(days=4), + today_date - timedelta(days=3), + today_date + timedelta(days=1), + today_date + timedelta(days=3), + today_date + timedelta(weeks=3), ] def setUp(self): @@ -88,19 +87,23 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue(is_holiday(self.test_employee.name)) self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1])) - self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1))) + self.assertFalse(is_holiday(self.test_employee.name, date=getdate() - timedelta(days=1))) # Test weekly_off holidays self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2])) - self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True)) + self.assertFalse( + is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True) + ) # Test with descriptions has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True) self.assertTrue(has_holiday) - self.assertTrue('test holiday1' in descriptions) + self.assertTrue("test holiday1" in descriptions) def test_birthday_reminders(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + employee = frappe.get_doc( + "Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0] + ) employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] employee.company_email = "test@example.com" employee.company = "_Test Company" @@ -124,7 +127,8 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) def test_work_anniversary_reminders(self): - make_employee("test_work_anniversary@gmail.com", + make_employee( + "test_work_anniversary@gmail.com", date_of_joining="1998" + frappe.utils.nowdate()[4:], company="_Test Company", ) @@ -134,7 +138,7 @@ class TestEmployeeReminders(unittest.TestCase): send_work_anniversary_reminders, ) - employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary") employees = employees_having_work_anniversary.get("_Test Company") or [] user_ids = [] for entry in employees: @@ -152,14 +156,15 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) def test_work_anniversary_reminder_not_sent_for_0_years(self): - make_employee("test_work_anniversary_2@gmail.com", + make_employee( + "test_work_anniversary_2@gmail.com", date_of_joining=getdate(), company="_Test Company", ) from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today - employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + employees_having_work_anniversary = get_employees_having_an_event_today("work_anniversary") employees = employees_having_work_anniversary.get("_Test Company") or [] user_ids = [] for entry in employees: @@ -168,20 +173,18 @@ class TestEmployeeReminders(unittest.TestCase): self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids) def test_send_holidays_reminder_in_advance(self): - setup_hr_settings('Weekly') + setup_hr_settings("Weekly") holidays = get_holidays_for_employee( - self.test_employee.get('name'), - getdate(), getdate() + timedelta(days=3), - only_non_weekly=True, - raise_exception=False - ) - - send_holidays_reminder_in_advance( - self.test_employee.get('name'), - holidays + self.test_employee.get("name"), + getdate(), + getdate() + timedelta(days=3), + only_non_weekly=True, + raise_exception=False, ) + send_holidays_reminder_in_advance(self.test_employee.get("name"), holidays) + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertEqual(len(email_queue), 1) self.assertTrue("Holidays this Week." in email_queue[0].message) @@ -189,67 +192,69 @@ class TestEmployeeReminders(unittest.TestCase): def test_advance_holiday_reminders_monthly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly - setup_hr_settings('Monthly') + setup_hr_settings("Monthly") # disable emp 2, set same holiday list - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Left', - 'holiday_list': self.test_employee.holiday_list - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Left", "holiday_list": self.test_employee.holiday_list}, + ) send_reminders_in_advance_monthly() email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) # even though emp 2 has holiday, non-active employees should not be recipients - recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient") self.assertTrue(self.test_employee_2.user_id not in recipients) # teardown: enable emp 2 - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Active', - 'holiday_list': self.holiday_list_2.name - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Active", "holiday_list": self.holiday_list_2.name}, + ) def test_advance_holiday_reminders_weekly(self): from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly - setup_hr_settings('Weekly') + setup_hr_settings("Weekly") # disable emp 2, set same holiday list - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Left', - 'holiday_list': self.test_employee.holiday_list - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Left", "holiday_list": self.test_employee.holiday_list}, + ) send_reminders_in_advance_weekly() email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertTrue(len(email_queue) > 0) # even though emp 2 has holiday, non-active employees should not be recipients - recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient') + recipients = frappe.db.get_all("Email Queue Recipient", pluck="recipient") self.assertTrue(self.test_employee_2.user_id not in recipients) # teardown: enable emp 2 - frappe.db.set_value('Employee', self.test_employee_2.name, { - 'status': 'Active', - 'holiday_list': self.holiday_list_2.name - }) + frappe.db.set_value( + "Employee", + self.test_employee_2.name, + {"status": "Active", "holiday_list": self.holiday_list_2.name}, + ) def test_reminder_not_sent_if_no_holdays(self): - setup_hr_settings('Monthly') + setup_hr_settings("Monthly") # reminder not sent if there are no holidays holidays = get_holidays_for_employee( - self.test_employee_2.get('name'), - getdate(), getdate() + timedelta(days=3), + self.test_employee_2.get("name"), + getdate(), + getdate() + timedelta(days=3), only_non_weekly=True, - raise_exception=False - ) - send_holidays_reminder_in_advance( - self.test_employee_2.get('name'), - holidays + raise_exception=False, ) + send_holidays_reminder_in_advance(self.test_employee_2.get("name"), holidays) email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) self.assertEqual(len(email_queue), 0) @@ -259,5 +264,5 @@ def setup_hr_settings(frequency=None): hr_settings = frappe.get_doc("HR Settings", "HR Settings") hr_settings.send_holiday_reminders = 1 set_proceed_with_frequency_change() - hr_settings.frequency = frequency or 'Weekly' - hr_settings.save() \ No newline at end of file + hr_settings.frequency = frequency or "Weekly" + hr_settings.save() diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 7aac2b63ed3..3d4023d3195 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -16,17 +16,19 @@ from erpnext.hr.utils import validate_active_employee class EmployeeAdvanceOverPayment(frappe.ValidationError): pass + class EmployeeAdvance(Document): def onload(self): - self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings', - 'make_payment_via_journal_entry') + self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value( + "Accounts Settings", "make_payment_via_journal_entry" + ) def validate(self): validate_active_employee(self.employee) self.set_status() def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry') + self.ignore_linked_doctypes = "GL Entry" def set_status(self): if self.docstatus == 0: @@ -46,30 +48,30 @@ class EmployeeAdvance(Document): paid_amount = ( frappe.qb.from_(gle) - .select(Sum(gle.debit).as_("paid_amount")) - .where( - (gle.against_voucher_type == 'Employee Advance') - & (gle.against_voucher == self.name) - & (gle.party_type == 'Employee') - & (gle.party == self.employee) - & (gle.docstatus == 1) - & (gle.is_cancelled == 0) - ) - ).run(as_dict=True)[0].paid_amount or 0 + .select(Sum(gle.debit).as_("paid_amount")) + .where( + (gle.against_voucher_type == "Employee Advance") + & (gle.against_voucher == self.name) + & (gle.party_type == "Employee") + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].paid_amount or 0 return_amount = ( frappe.qb.from_(gle) - .select(Sum(gle.credit).as_("return_amount")) - .where( - (gle.against_voucher_type == 'Employee Advance') - & (gle.voucher_type != 'Expense Claim') - & (gle.against_voucher == self.name) - & (gle.party_type == 'Employee') - & (gle.party == self.employee) - & (gle.docstatus == 1) - & (gle.is_cancelled == 0) - ) - ).run(as_dict=True)[0].return_amount or 0 + .select(Sum(gle.credit).as_("return_amount")) + .where( + (gle.against_voucher_type == "Employee Advance") + & (gle.voucher_type != "Expense Claim") + & (gle.against_voucher == self.name) + & (gle.party_type == "Employee") + & (gle.party == self.employee) + & (gle.docstatus == 1) + & (gle.is_cancelled == 0) + ) + ).run(as_dict=True)[0].return_amount or 0 if paid_amount != 0: paid_amount = flt(paid_amount) / flt(self.exchange_rate) @@ -77,8 +79,10 @@ class EmployeeAdvance(Document): return_amount = flt(return_amount) / flt(self.exchange_rate) if flt(paid_amount) > self.advance_amount: - frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"), - EmployeeAdvanceOverPayment) + frappe.throw( + _("Row {0}# Paid Amount cannot be greater than requested advance amount"), + EmployeeAdvanceOverPayment, + ) if flt(return_amount) > self.paid_amount - self.claimed_amount: frappe.throw(_("Return amount cannot be greater unclaimed amount")) @@ -86,11 +90,12 @@ class EmployeeAdvance(Document): self.db_set("paid_amount", paid_amount) self.db_set("return_amount", return_amount) self.set_status() - frappe.db.set_value("Employee Advance", self.name , "status", self.status) - + frappe.db.set_value("Employee Advance", self.name, "status", self.status) def update_claimed_amount(self): - claimed_amount = frappe.db.sql(""" + claimed_amount = ( + frappe.db.sql( + """ SELECT sum(ifnull(allocated_amount, 0)) FROM `tabExpense Claim Advance` eca, `tabExpense Claim` ec WHERE @@ -99,65 +104,83 @@ class EmployeeAdvance(Document): AND ec.name = eca.parent AND ec.docstatus=1 AND eca.allocated_amount > 0 - """, self.name)[0][0] or 0 + """, + self.name, + )[0][0] + or 0 + ) frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount)) self.reload() self.set_status() frappe.db.set_value("Employee Advance", self.name, "status", self.status) + @frappe.whitelist() def get_pending_amount(employee, posting_date): - employee_due_amount = frappe.get_all("Employee Advance", \ - filters = {"employee":employee, "docstatus":1, "posting_date":("<=", posting_date)}, \ - fields = ["advance_amount", "paid_amount"]) + employee_due_amount = frappe.get_all( + "Employee Advance", + filters={"employee": employee, "docstatus": 1, "posting_date": ("<=", posting_date)}, + fields=["advance_amount", "paid_amount"], + ) return sum([(emp.advance_amount - emp.paid_amount) for emp in employee_due_amount]) + @frappe.whitelist() def make_bank_entry(dt, dn): doc = frappe.get_doc(dt, dn) - payment_account = get_default_bank_cash_account(doc.company, account_type="Cash", - mode_of_payment=doc.mode_of_payment) + payment_account = get_default_bank_cash_account( + doc.company, account_type="Cash", mode_of_payment=doc.mode_of_payment + ) if not payment_account: frappe.throw(_("Please set a Default Cash Account in Company defaults")) - advance_account_currency = frappe.db.get_value('Account', doc.advance_account, 'account_currency') + advance_account_currency = frappe.db.get_value("Account", doc.advance_account, "account_currency") - advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate(advance_account_currency,doc ) + advance_amount, advance_exchange_rate = get_advance_amount_advance_exchange_rate( + advance_account_currency, doc + ) paying_amount, paying_exchange_rate = get_paying_amount_paying_exchange_rate(payment_account, doc) je = frappe.new_doc("Journal Entry") je.posting_date = nowdate() - je.voucher_type = 'Bank Entry' + je.voucher_type = "Bank Entry" je.company = doc.company - je.remark = 'Payment against Employee Advance: ' + dn + '\n' + doc.purpose + je.remark = "Payment against Employee Advance: " + dn + "\n" + doc.purpose je.multi_currency = 1 if advance_account_currency != payment_account.account_currency else 0 - je.append("accounts", { - "account": doc.advance_account, - "account_currency": advance_account_currency, - "exchange_rate": flt(advance_exchange_rate), - "debit_in_account_currency": flt(advance_amount), - "reference_type": "Employee Advance", - "reference_name": doc.name, - "party_type": "Employee", - "cost_center": erpnext.get_default_cost_center(doc.company), - "party": doc.employee, - "is_advance": "Yes" - }) + je.append( + "accounts", + { + "account": doc.advance_account, + "account_currency": advance_account_currency, + "exchange_rate": flt(advance_exchange_rate), + "debit_in_account_currency": flt(advance_amount), + "reference_type": "Employee Advance", + "reference_name": doc.name, + "party_type": "Employee", + "cost_center": erpnext.get_default_cost_center(doc.company), + "party": doc.employee, + "is_advance": "Yes", + }, + ) - je.append("accounts", { - "account": payment_account.account, - "cost_center": erpnext.get_default_cost_center(doc.company), - "credit_in_account_currency": flt(paying_amount), - "account_currency": payment_account.account_currency, - "account_type": payment_account.account_type, - "exchange_rate": flt(paying_exchange_rate) - }) + je.append( + "accounts", + { + "account": payment_account.account, + "cost_center": erpnext.get_default_cost_center(doc.company), + "credit_in_account_currency": flt(paying_amount), + "account_currency": payment_account.account_currency, + "account_type": payment_account.account_type, + "exchange_rate": flt(paying_exchange_rate), + }, + ) return je.as_dict() + def get_advance_amount_advance_exchange_rate(advance_account_currency, doc): if advance_account_currency != doc.currency: advance_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) @@ -168,6 +191,7 @@ def get_advance_amount_advance_exchange_rate(advance_account_currency, doc): return advance_amount, advance_exchange_rate + def get_paying_amount_paying_exchange_rate(payment_account, doc): if payment_account.account_currency != doc.currency: paying_amount = flt(doc.advance_amount) * flt(doc.exchange_rate) @@ -178,6 +202,7 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc): return paying_amount, paying_exchange_rate + @frappe.whitelist() def create_return_through_additional_salary(doc): import json @@ -185,7 +210,7 @@ def create_return_through_additional_salary(doc): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = doc.employee additional_salary.currency = doc.currency additional_salary.amount = doc.paid_amount - doc.claimed_amount @@ -195,54 +220,79 @@ def create_return_through_additional_salary(doc): return additional_salary + @frappe.whitelist() -def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, currency, exchange_rate, mode_of_payment=None): - bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) +def make_return_entry( + employee, + company, + employee_advance_name, + return_amount, + advance_account, + currency, + exchange_rate, + mode_of_payment=None, +): + bank_cash_account = get_default_bank_cash_account( + company, account_type="Cash", mode_of_payment=mode_of_payment + ) if not bank_cash_account: frappe.throw(_("Please set a Default Cash Account in Company defaults")) - advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') + advance_account_currency = frappe.db.get_value("Account", advance_account, "account_currency") - je = frappe.new_doc('Journal Entry') + je = frappe.new_doc("Journal Entry") je.posting_date = nowdate() je.voucher_type = get_voucher_type(mode_of_payment) je.company = company - je.remark = 'Return against Employee Advance: ' + employee_advance_name + je.remark = "Return against Employee Advance: " + employee_advance_name je.multi_currency = 1 if advance_account_currency != bank_cash_account.account_currency else 0 - advance_account_amount = flt(return_amount) if advance_account_currency==currency \ + advance_account_amount = ( + flt(return_amount) + if advance_account_currency == currency else flt(return_amount) * flt(exchange_rate) + ) - je.append('accounts', { - 'account': advance_account, - 'credit_in_account_currency': advance_account_amount, - 'account_currency': advance_account_currency, - 'exchange_rate': flt(exchange_rate) if advance_account_currency == currency else 1, - 'reference_type': 'Employee Advance', - 'reference_name': employee_advance_name, - 'party_type': 'Employee', - 'party': employee, - 'is_advance': 'Yes' - }) + je.append( + "accounts", + { + "account": advance_account, + "credit_in_account_currency": advance_account_amount, + "account_currency": advance_account_currency, + "exchange_rate": flt(exchange_rate) if advance_account_currency == currency else 1, + "reference_type": "Employee Advance", + "reference_name": employee_advance_name, + "party_type": "Employee", + "party": employee, + "is_advance": "Yes", + }, + ) - bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \ + bank_amount = ( + flt(return_amount) + if bank_cash_account.account_currency == currency else flt(return_amount) * flt(exchange_rate) + ) - je.append("accounts", { - "account": bank_cash_account.account, - "debit_in_account_currency": bank_amount, - "account_currency": bank_cash_account.account_currency, - "account_type": bank_cash_account.account_type, - "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1 - }) + je.append( + "accounts", + { + "account": bank_cash_account.account, + "debit_in_account_currency": bank_amount, + "account_currency": bank_cash_account.account_currency, + "account_type": bank_cash_account.account_type, + "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1, + }, + ) return je.as_dict() + def get_voucher_type(mode_of_payment=None): voucher_type = "Cash Entry" if mode_of_payment: - mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') + mode_of_payment_type = frappe.get_cached_value("Mode of Payment", mode_of_payment, "type") if mode_of_payment_type == "Bank": voucher_type = "Bank Entry" diff --git a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py index 089bd2c1b5c..73fac5131ea 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py @@ -1,18 +1,9 @@ - - def get_data(): return { - 'fieldname': 'employee_advance', - 'non_standard_fieldnames': { - 'Payment Entry': 'reference_name', - 'Journal Entry': 'reference_name' + "fieldname": "employee_advance", + "non_standard_fieldnames": { + "Payment Entry": "reference_name", + "Journal Entry": "reference_name", }, - 'transactions': [ - { - 'items': ['Expense Claim'] - }, - { - 'items': ['Payment Entry', 'Journal Entry'] - } - ] + "transactions": [{"items": ["Expense Claim"]}, {"items": ["Payment Entry", "Journal Entry"]}], } diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 2744de96ec3..9b006ffcffe 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -59,7 +59,9 @@ class TestEmployeeAdvance(unittest.TestCase): args = {"type": "Deduction"} create_salary_component("Advance Salary - Deduction", **args) - make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name) + make_salary_structure( + "Test Additional Salary for Advance Return", "Monthly", employee=employee_name + ) # additional salary for 700 first advance.reload() @@ -101,10 +103,11 @@ def make_payment_entry(advance): return journal_entry + def make_employee_advance(employee_name, args=None): doc = frappe.new_doc("Employee Advance") doc.employee = employee_name - doc.company = "_Test company" + doc.company = "_Test company" doc.purpose = "For site visit" doc.currency = erpnext.get_company_currency("_Test company") doc.exchange_rate = 1 @@ -130,13 +133,16 @@ def get_advances_for_claim(claim, advance_name, amount=None): else: allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount) - claim.append("advances", { - "employee_advance": entry.name, - "posting_date": entry.posting_date, - "advance_account": entry.advance_account, - "advance_paid": entry.paid_amount, - "unclaimed_amount": allocated_amount, - "allocated_amount": allocated_amount - }) + claim.append( + "advances", + { + "employee_advance": entry.name, + "posting_date": entry.posting_date, + "advance_account": entry.advance_account, + "advance_paid": entry.paid_amount, + "unclaimed_amount": allocated_amount, + "allocated_amount": allocated_amount, + }, + ) - return claim \ No newline at end of file + return claim diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py index af2ca50b78a..43665cc8b22 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py @@ -14,32 +14,31 @@ class EmployeeAttendanceTool(Document): @frappe.whitelist() -def get_employees(date, department = None, branch = None, company = None): +def get_employees(date, department=None, branch=None, company=None): attendance_not_marked = [] attendance_marked = [] filters = {"status": "Active", "date_of_joining": ["<=", date]} - for field, value in {'department': department, - 'branch': branch, 'company': company}.items(): + for field, value in {"department": department, "branch": branch, "company": company}.items(): if value: filters[field] = value - employee_list = frappe.get_list("Employee", fields=["employee", "employee_name"], filters=filters, order_by="employee_name") + employee_list = frappe.get_list( + "Employee", fields=["employee", "employee_name"], filters=filters, order_by="employee_name" + ) marked_employee = {} - for emp in frappe.get_list("Attendance", fields=["employee", "status"], - filters={"attendance_date": date}): - marked_employee[emp['employee']] = emp['status'] + for emp in frappe.get_list( + "Attendance", fields=["employee", "status"], filters={"attendance_date": date} + ): + marked_employee[emp["employee"]] = emp["status"] for employee in employee_list: - employee['status'] = marked_employee.get(employee['employee']) - if employee['employee'] not in marked_employee: + employee["status"] = marked_employee.get(employee["employee"]) + if employee["employee"] not in marked_employee: attendance_not_marked.append(employee) else: attendance_marked.append(employee) - return { - "marked": attendance_marked, - "unmarked": attendance_not_marked - } + return {"marked": attendance_marked, "unmarked": attendance_not_marked} @frappe.whitelist() @@ -53,16 +52,18 @@ def mark_employee_attendance(employee_list, status, date, leave_type=None, compa else: leave_type = None - company = frappe.db.get_value("Employee", employee['employee'], "Company", cache=True) + company = frappe.db.get_value("Employee", employee["employee"], "Company", cache=True) - attendance=frappe.get_doc(dict( - doctype='Attendance', - employee=employee.get('employee'), - employee_name=employee.get('employee_name'), - attendance_date=getdate(date), - status=status, - leave_type=leave_type, - company=company - )) + attendance = frappe.get_doc( + dict( + doctype="Attendance", + employee=employee.get("employee"), + employee_name=employee.get("employee_name"), + attendance_date=getdate(date), + status=status, + leave_type=leave_type, + company=company, + ) + ) attendance.insert() - attendance.submit() \ No newline at end of file + attendance.submit() diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index c1d4ac7fded..87f48b7e257 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -20,20 +20,31 @@ class EmployeeCheckin(Document): self.fetch_shift() def validate_duplicate_log(self): - doc = frappe.db.exists('Employee Checkin', { - 'employee': self.employee, - 'time': self.time, - 'name': ['!=', self.name]}) + doc = frappe.db.exists( + "Employee Checkin", {"employee": self.employee, "time": self.time, "name": ["!=", self.name]} + ) if doc: - doc_link = frappe.get_desk_link('Employee Checkin', doc) - frappe.throw(_('This employee already has a log with the same timestamp.{0}') - .format("
    " + doc_link)) + doc_link = frappe.get_desk_link("Employee Checkin", doc) + frappe.throw( + _("This employee already has a log with the same timestamp.{0}").format("
    " + doc_link) + ) def fetch_shift(self): - shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True) + shift_actual_timings = get_actual_start_end_datetime_of_shift( + self.employee, get_datetime(self.time), True + ) if shift_actual_timings[0] and shift_actual_timings[1]: - if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance: - frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name)) + if ( + shift_actual_timings[2].shift_type.determine_check_in_and_check_out + == "Strictly based on Log Type in Employee Checkin" + and not self.log_type + and not self.skip_auto_attendance + ): + frappe.throw( + _("Log Type is required for check-ins falling in the shift: {0}.").format( + shift_actual_timings[2].shift_type.name + ) + ) if not self.attendance: self.shift = shift_actual_timings[2].shift_type.name self.shift_actual_start = shift_actual_timings[0] @@ -43,8 +54,16 @@ class EmployeeCheckin(Document): else: self.shift = None + @frappe.whitelist() -def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'): +def add_log_based_on_employee_field( + employee_field_value, + timestamp, + device_id=None, + log_type=None, + skip_auto_attendance=0, + employee_fieldname="attendance_device_id", +): """Finds the relevant Employee using the employee field value and creates a Employee Checkin. :param employee_field_value: The value to look for in employee field. @@ -58,11 +77,20 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N if not employee_field_value or not timestamp: frappe.throw(_("'employee_field_value' and 'timestamp' are required.")) - employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value}, ["name", "employee_name", employee_fieldname], as_dict=True) + employee = frappe.db.get_values( + "Employee", + {employee_fieldname: employee_field_value}, + ["name", "employee_name", employee_fieldname], + as_dict=True, + ) if employee: employee = employee[0] else: - frappe.throw(_("No Employee found for the given employee field value. '{}': {}").format(employee_fieldname,employee_field_value)) + frappe.throw( + _("No Employee found for the given employee field value. '{}': {}").format( + employee_fieldname, employee_field_value + ) + ) doc = frappe.new_doc("Employee Checkin") doc.employee = employee.name @@ -70,13 +98,24 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N doc.time = timestamp doc.device_id = device_id doc.log_type = log_type - if cint(skip_auto_attendance) == 1: doc.skip_auto_attendance = '1' + if cint(skip_auto_attendance) == 1: + doc.skip_auto_attendance = "1" doc.insert() return doc -def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None): +def mark_attendance_and_link_log( + logs, + attendance_status, + attendance_date, + working_hours=None, + late_entry=False, + early_exit=False, + in_time=None, + out_time=None, + shift=None, +): """Creates an attendance and links the attendance to the Employee Checkin. Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown. @@ -87,40 +126,52 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki """ log_names = [x.name for x in logs] employee = logs[0].employee - if attendance_status == 'Skip': - frappe.db.sql("""update `tabEmployee Checkin` + if attendance_status == "Skip": + frappe.db.sql( + """update `tabEmployee Checkin` set skip_auto_attendance = %s - where name in %s""", ('1', log_names)) + where name in %s""", + ("1", log_names), + ) return None - elif attendance_status in ('Present', 'Absent', 'Half Day'): - employee_doc = frappe.get_doc('Employee', employee) - if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): + elif attendance_status in ("Present", "Absent", "Half Day"): + employee_doc = frappe.get_doc("Employee", employee) + if not frappe.db.exists( + "Attendance", + {"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")}, + ): doc_dict = { - 'doctype': 'Attendance', - 'employee': employee, - 'attendance_date': attendance_date, - 'status': attendance_status, - 'working_hours': working_hours, - 'company': employee_doc.company, - 'shift': shift, - 'late_entry': late_entry, - 'early_exit': early_exit, - 'in_time': in_time, - 'out_time': out_time + "doctype": "Attendance", + "employee": employee, + "attendance_date": attendance_date, + "status": attendance_status, + "working_hours": working_hours, + "company": employee_doc.company, + "shift": shift, + "late_entry": late_entry, + "early_exit": early_exit, + "in_time": in_time, + "out_time": out_time, } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() - frappe.db.sql("""update `tabEmployee Checkin` + frappe.db.sql( + """update `tabEmployee Checkin` set attendance = %s - where name in %s""", (attendance.name, log_names)) + where name in %s""", + (attendance.name, log_names), + ) return attendance else: - frappe.db.sql("""update `tabEmployee Checkin` + frappe.db.sql( + """update `tabEmployee Checkin` set skip_auto_attendance = %s - where name in %s""", ('1', log_names)) + where name in %s""", + ("1", log_names), + ) return None else: - frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status)) + frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status)) def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): @@ -133,29 +184,35 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): """ total_hours = 0 in_time = out_time = None - if check_in_out_type == 'Alternating entries as IN and OUT during the same shift': + if check_in_out_type == "Alternating entries as IN and OUT during the same shift": in_time = logs[0].time if len(logs) >= 2: out_time = logs[-1].time - if working_hours_calc_type == 'First Check-in and Last Check-out': + if working_hours_calc_type == "First Check-in and Last Check-out": # assumption in this case: First log always taken as IN, Last log always taken as OUT total_hours = time_diff_in_hours(in_time, logs[-1].time) - elif working_hours_calc_type == 'Every Valid Check-in and Check-out': + elif working_hours_calc_type == "Every Valid Check-in and Check-out": logs = logs[:] while len(logs) >= 2: total_hours += time_diff_in_hours(logs[0].time, logs[1].time) del logs[:2] - elif check_in_out_type == 'Strictly based on Log Type in Employee Checkin': - if working_hours_calc_type == 'First Check-in and Last Check-out': - first_in_log_index = find_index_in_dict(logs, 'log_type', 'IN') - first_in_log = logs[first_in_log_index] if first_in_log_index or first_in_log_index == 0 else None - last_out_log_index = find_index_in_dict(reversed(logs), 'log_type', 'OUT') - last_out_log = logs[len(logs)-1-last_out_log_index] if last_out_log_index or last_out_log_index == 0 else None + elif check_in_out_type == "Strictly based on Log Type in Employee Checkin": + if working_hours_calc_type == "First Check-in and Last Check-out": + first_in_log_index = find_index_in_dict(logs, "log_type", "IN") + first_in_log = ( + logs[first_in_log_index] if first_in_log_index or first_in_log_index == 0 else None + ) + last_out_log_index = find_index_in_dict(reversed(logs), "log_type", "OUT") + last_out_log = ( + logs[len(logs) - 1 - last_out_log_index] + if last_out_log_index or last_out_log_index == 0 + else None + ) if first_in_log and last_out_log: in_time, out_time = first_in_log.time, last_out_log.time total_hours = time_diff_in_hours(in_time, out_time) - elif working_hours_calc_type == 'Every Valid Check-in and Check-out': + elif working_hours_calc_type == "Every Valid Check-in and Check-out": in_log = out_log = None for log in logs: if in_log and out_log: @@ -165,16 +222,18 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): total_hours += time_diff_in_hours(in_log.time, out_log.time) in_log = out_log = None if not in_log: - in_log = log if log.log_type == 'IN' else None + in_log = log if log.log_type == "IN" else None elif not out_log: - out_log = log if log.log_type == 'OUT' else None + out_log = log if log.log_type == "OUT" else None if in_log and out_log: out_time = out_log.time total_hours += time_diff_in_hours(in_log.time, out_log.time) return total_hours, in_time, out_time + def time_diff_in_hours(start, end): - return round((end-start).total_seconds() / 3600, 1) + return round((end - start).total_seconds() / 3600, 1) + def find_index_in_dict(dict_list, key, value): return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py index 254bf9e2569..97f76b03502 100644 --- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py @@ -19,85 +19,108 @@ class TestEmployeeCheckin(unittest.TestCase): def test_add_log_based_on_employee_field(self): employee = make_employee("test_add_log_based_on_employee_field@example.com") employee = frappe.get_doc("Employee", employee) - employee.attendance_device_id = '3344' + employee.attendance_device_id = "3344" employee.save() time_now = now_datetime().__str__()[:-7] - employee_checkin = add_log_based_on_employee_field('3344', time_now, 'mumbai_first_floor', 'IN') + employee_checkin = add_log_based_on_employee_field("3344", time_now, "mumbai_first_floor", "IN") self.assertEqual(employee_checkin.employee, employee.name) self.assertEqual(employee_checkin.time, time_now) - self.assertEqual(employee_checkin.device_id, 'mumbai_first_floor') - self.assertEqual(employee_checkin.log_type, 'IN') + self.assertEqual(employee_checkin.device_id, "mumbai_first_floor") + self.assertEqual(employee_checkin.log_type, "IN") def test_mark_attendance_and_link_log(self): employee = make_employee("test_mark_attendance_and_link_log@example.com") logs = make_n_checkins(employee, 3) - mark_attendance_and_link_log(logs, 'Skip', nowdate()) + mark_attendance_and_link_log(logs, "Skip", nowdate()) log_names = [log.name for log in logs] - logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'skip_auto_attendance':1}) + logs_count = frappe.db.count( + "Employee Checkin", {"name": ["in", log_names], "skip_auto_attendance": 1} + ) self.assertEqual(logs_count, 3) logs = make_n_checkins(employee, 4, 2) now_date = nowdate() - frappe.db.delete('Attendance', {'employee':employee}) - attendance = mark_attendance_and_link_log(logs, 'Present', now_date, 8.2) + frappe.db.delete("Attendance", {"employee": employee}) + attendance = mark_attendance_and_link_log(logs, "Present", now_date, 8.2) log_names = [log.name for log in logs] - logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'attendance':attendance.name}) + logs_count = frappe.db.count( + "Employee Checkin", {"name": ["in", log_names], "attendance": attendance.name} + ) self.assertEqual(logs_count, 4) - attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_hours':8.2, - 'employee':employee, 'attendance_date':now_date}) + attendance_count = frappe.db.count( + "Attendance", + {"status": "Present", "working_hours": 8.2, "employee": employee, "attendance_date": now_date}, + ) self.assertEqual(attendance_count, 1) def test_calculate_working_hours(self): - check_in_out_type = ['Alternating entries as IN and OUT during the same shift', - 'Strictly based on Log Type in Employee Checkin'] - working_hours_calc_type = ['First Check-in and Last Check-out', - 'Every Valid Check-in and Check-out'] + check_in_out_type = [ + "Alternating entries as IN and OUT during the same shift", + "Strictly based on Log Type in Employee Checkin", + ] + working_hours_calc_type = [ + "First Check-in and Last Check-out", + "Every Valid Check-in and Check-out", + ] logs_type_1 = [ - {'time':now_datetime()-timedelta(minutes=390)}, - {'time':now_datetime()-timedelta(minutes=300)}, - {'time':now_datetime()-timedelta(minutes=270)}, - {'time':now_datetime()-timedelta(minutes=90)}, - {'time':now_datetime()-timedelta(minutes=0)} - ] + {"time": now_datetime() - timedelta(minutes=390)}, + {"time": now_datetime() - timedelta(minutes=300)}, + {"time": now_datetime() - timedelta(minutes=270)}, + {"time": now_datetime() - timedelta(minutes=90)}, + {"time": now_datetime() - timedelta(minutes=0)}, + ] logs_type_2 = [ - {'time':now_datetime()-timedelta(minutes=390),'log_type':'OUT'}, - {'time':now_datetime()-timedelta(minutes=360),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=300),'log_type':'OUT'}, - {'time':now_datetime()-timedelta(minutes=290),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=260),'log_type':'OUT'}, - {'time':now_datetime()-timedelta(minutes=240),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=150),'log_type':'IN'}, - {'time':now_datetime()-timedelta(minutes=60),'log_type':'OUT'} - ] + {"time": now_datetime() - timedelta(minutes=390), "log_type": "OUT"}, + {"time": now_datetime() - timedelta(minutes=360), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=300), "log_type": "OUT"}, + {"time": now_datetime() - timedelta(minutes=290), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=260), "log_type": "OUT"}, + {"time": now_datetime() - timedelta(minutes=240), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=150), "log_type": "IN"}, + {"time": now_datetime() - timedelta(minutes=60), "log_type": "OUT"}, + ] logs_type_1 = [frappe._dict(x) for x in logs_type_1] logs_type_2 = [frappe._dict(x) for x in logs_type_2] - working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0]) + working_hours = calculate_working_hours( + logs_type_1, check_in_out_type[0], working_hours_calc_type[0] + ) self.assertEqual(working_hours, (6.5, logs_type_1[0].time, logs_type_1[-1].time)) - working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1]) + working_hours = calculate_working_hours( + logs_type_1, check_in_out_type[0], working_hours_calc_type[1] + ) self.assertEqual(working_hours, (4.5, logs_type_1[0].time, logs_type_1[-1].time)) - working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0]) + working_hours = calculate_working_hours( + logs_type_2, check_in_out_type[1], working_hours_calc_type[0] + ) self.assertEqual(working_hours, (5, logs_type_2[1].time, logs_type_2[-1].time)) - working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1]) + working_hours = calculate_working_hours( + logs_type_2, check_in_out_type[1], working_hours_calc_type[1] + ) self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time)) + def make_n_checkins(employee, n, hours_to_reverse=1): - logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))] - for i in range(n-1): - logs.append(make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n-i))) + logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n + 1))] + for i in range(n - 1): + logs.append( + make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n - i)) + ) return logs def make_checkin(employee, time=now_datetime()): - log = frappe.get_doc({ - "doctype": "Employee Checkin", - "employee" : employee, - "time" : time, - "device_id" : "device1", - "log_type" : "IN" - }).insert() + log = frappe.get_doc( + { + "doctype": "Employee Checkin", + "employee": employee, + "time": time, + "device_id": "device1", + "log_type": "IN", + } + ).insert() return log diff --git a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py index 1dd6ad3b15a..efc68ce87a3 100644 --- a/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py +++ b/erpnext/hr/doctype/employee_grade/employee_grade_dashboard.py @@ -1,13 +1,9 @@ - - def get_data(): return { - 'transactions': [ + "transactions": [ { - 'items': ['Employee', 'Leave Period'], + "items": ["Employee", "Leave Period"], }, - { - 'items': ['Employee Onboarding Template', 'Employee Separation Template'] - } + {"items": ["Employee Onboarding Template", "Employee Separation Template"]}, ] } diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py index fd9a33b3771..45de79f4f5e 100644 --- a/erpnext/hr/doctype/employee_grievance/employee_grievance.py +++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py @@ -9,7 +9,8 @@ from frappe.model.document import Document class EmployeeGrievance(Document): def on_submit(self): if self.status not in ["Invalid", "Resolved"]: - frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format( - bold("Invalid"), - bold("Resolved")) + frappe.throw( + _("Only Employee Grievance with status {0} or {1} can be submitted").format( + bold("Invalid"), bold("Resolved") + ) ) diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py index e2d0002aa62..910d8828603 100644 --- a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py +++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py @@ -13,6 +13,7 @@ class TestEmployeeGrievance(unittest.TestCase): def test_create_employee_grievance(self): create_employee_grievance() + def create_employee_grievance(): grievance_type = create_grievance_type() emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company") @@ -27,10 +28,10 @@ def create_employee_grievance(): grievance.grievance_against = emp_2 grievance.description = "test descrip" - #set cause + # set cause grievance.cause_of_grievance = "test cause" - #resolution details + # resolution details grievance.resolution_date = today() grievance.resolution_detail = "test resolution detail" grievance.resolved_by = "test_emp_grievance_@example.com" diff --git a/erpnext/hr/doctype/employee_group/test_employee_group.py b/erpnext/hr/doctype/employee_group/test_employee_group.py index a87f4007bd8..3922f54f331 100644 --- a/erpnext/hr/doctype/employee_group/test_employee_group.py +++ b/erpnext/hr/doctype/employee_group/test_employee_group.py @@ -11,17 +11,16 @@ from erpnext.hr.doctype.employee.test_employee import make_employee class TestEmployeeGroup(unittest.TestCase): pass + def make_employee_group(): employee = make_employee("testemployee@example.com") - employee_group = frappe.get_doc({ - "doctype": "Employee Group", - "employee_group_name": "_Test Employee Group", - "employee_list": [ - { - "employee": employee - } - ] - }) + employee_group = frappe.get_doc( + { + "doctype": "Employee Group", + "employee_group_name": "_Test Employee Group", + "employee_list": [{"employee": employee}], + } + ) employee_group_exist = frappe.db.exists("Employee Group", "_Test Employee Group") if not employee_group_exist: employee_group.insert() @@ -29,6 +28,7 @@ def make_employee_group(): else: return employee_group_exist + def get_employee_group(): employee_group = frappe.db.exists("Employee Group", "_Test Employee Group") return employee_group diff --git a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py index 01a9fe29a98..14b75bef569 100644 --- a/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/employee_onboarding.py @@ -9,7 +9,9 @@ from frappe.model.mapper import get_mapped_doc from erpnext.hr.utils import EmployeeBoardingController -class IncompleteTaskError(frappe.ValidationError): pass +class IncompleteTaskError(frappe.ValidationError): + pass + class EmployeeOnboarding(EmployeeBoardingController): def validate(self): @@ -17,9 +19,13 @@ class EmployeeOnboarding(EmployeeBoardingController): self.validate_duplicate_employee_onboarding() def validate_duplicate_employee_onboarding(self): - emp_onboarding = frappe.db.exists("Employee Onboarding",{"job_applicant": self.job_applicant}) + emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant}) if emp_onboarding and emp_onboarding != self.name: - frappe.throw(_("Employee Onboarding: {0} is already for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant))) + frappe.throw( + _("Employee Onboarding: {0} is already for Job Applicant: {1}").format( + frappe.bold(emp_onboarding), frappe.bold(self.job_applicant) + ) + ) def validate_employee_creation(self): if self.docstatus != 1: @@ -31,7 +37,9 @@ class EmployeeOnboarding(EmployeeBoardingController): else: task_status = frappe.db.get_value("Task", activity.task, "status") if task_status not in ["Completed", "Cancelled"]: - frappe.throw(_("All the mandatory Task for employee creation hasn't been done yet."), IncompleteTaskError) + frappe.throw( + _("All the mandatory Task for employee creation hasn't been done yet."), IncompleteTaskError + ) def on_submit(self): super(EmployeeOnboarding, self).on_submit() @@ -42,19 +50,29 @@ class EmployeeOnboarding(EmployeeBoardingController): def on_cancel(self): super(EmployeeOnboarding, self).on_cancel() + @frappe.whitelist() def make_employee(source_name, target_doc=None): doc = frappe.get_doc("Employee Onboarding", source_name) doc.validate_employee_creation() + def set_missing_values(source, target): target.personal_email = frappe.db.get_value("Job Applicant", source.job_applicant, "email_id") target.status = "Active" - doc = get_mapped_doc("Employee Onboarding", source_name, { + + doc = get_mapped_doc( + "Employee Onboarding", + source_name, + { "Employee Onboarding": { "doctype": "Employee", "field_map": { "first_name": "employee_name", "employee_grade": "grade", - }} - }, target_doc, set_missing_values) + }, + } + }, + target_doc, + set_missing_values, + ) return doc diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index daa068e6e03..21d00ce2de7 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -15,8 +15,8 @@ from erpnext.hr.doctype.job_offer.test_job_offer import create_job_offer class TestEmployeeOnboarding(unittest.TestCase): def test_employee_onboarding_incomplete_task(self): - if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}): - frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'}) + if frappe.db.exists("Employee Onboarding", {"employee_name": "Test Researcher"}): + frappe.delete_doc("Employee Onboarding", {"employee_name": "Test Researcher"}) frappe.db.sql("delete from `tabEmployee Onboarding`") project = "Employee Onboarding : test@researcher.com" frappe.db.sql("delete from tabProject where name=%s", project) @@ -26,35 +26,31 @@ class TestEmployeeOnboarding(unittest.TestCase): job_offer = create_job_offer(job_applicant=applicant.name) job_offer.submit() - onboarding = frappe.new_doc('Employee Onboarding') + onboarding = frappe.new_doc("Employee Onboarding") onboarding.job_applicant = applicant.name onboarding.job_offer = job_offer.name - onboarding.company = '_Test Company' - onboarding.designation = 'Researcher' - onboarding.append('activities', { - 'activity_name': 'Assign ID Card', - 'role': 'HR User', - 'required_for_employee_creation': 1 - }) - onboarding.append('activities', { - 'activity_name': 'Assign a laptop', - 'role': 'HR User' - }) - onboarding.status = 'Pending' + onboarding.company = "_Test Company" + onboarding.designation = "Researcher" + onboarding.append( + "activities", + {"activity_name": "Assign ID Card", "role": "HR User", "required_for_employee_creation": 1}, + ) + onboarding.append("activities", {"activity_name": "Assign a laptop", "role": "HR User"}) + onboarding.status = "Pending" onboarding.insert() onboarding.submit() project_name = frappe.db.get_value("Project", onboarding.project, "project_name") - self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com') + self.assertEqual(project_name, "Employee Onboarding : test@researcher.com") # don't allow making employee if onboarding is not complete self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) # complete the task - project = frappe.get_doc('Project', onboarding.project) - for task in frappe.get_all('Task', dict(project=project.name)): - task = frappe.get_doc('Task', task.name) - task.status = 'Completed' + project = frappe.get_doc("Project", onboarding.project) + for task in frappe.get_all("Task", dict(project=project.name)): + task = frappe.get_doc("Task", task.name) + task.status = "Completed" task.save() # make employee @@ -62,23 +58,25 @@ class TestEmployeeOnboarding(unittest.TestCase): employee = make_employee(onboarding.name) employee.first_name = employee.employee_name employee.date_of_joining = nowdate() - employee.date_of_birth = '1990-05-08' - employee.gender = 'Female' + employee.date_of_birth = "1990-05-08" + employee.gender = "Female" employee.insert() - self.assertEqual(employee.employee_name, 'Test Researcher') + self.assertEqual(employee.employee_name, "Test Researcher") + def get_job_applicant(): - if frappe.db.exists('Job Applicant', 'test@researcher.com'): - return frappe.get_doc('Job Applicant', 'test@researcher.com') - applicant = frappe.new_doc('Job Applicant') - applicant.applicant_name = 'Test Researcher' - applicant.email_id = 'test@researcher.com' - applicant.designation = 'Researcher' - applicant.status = 'Open' - applicant.cover_letter = 'I am a great Researcher.' + if frappe.db.exists("Job Applicant", "test@researcher.com"): + return frappe.get_doc("Job Applicant", "test@researcher.com") + applicant = frappe.new_doc("Job Applicant") + applicant.applicant_name = "Test Researcher" + applicant.email_id = "test@researcher.com" + applicant.designation = "Researcher" + applicant.status = "Open" + applicant.cover_letter = "I am a great Researcher." applicant.insert() return applicant + def _set_up(): for doctype in ["Employee Onboarding"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) diff --git a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py index 48f2c1d2709..93237ee3797 100644 --- a/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py +++ b/erpnext/hr/doctype/employee_onboarding_template/employee_onboarding_template_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'employee_onboarding_template', - 'transactions': [ - { - 'items': ['Employee Onboarding'] - }, - ], - } + return { + "fieldname": "employee_onboarding_template", + "transactions": [ + {"items": ["Employee Onboarding"]}, + ], + } diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py index cf6156e3264..d77c1dddfd0 100644 --- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py @@ -16,12 +16,16 @@ class EmployeePromotion(Document): def before_submit(self): if getdate(self.promotion_date) > getdate(): - frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date"), - frappe.DocstatusTransitionError) + frappe.throw( + _("Employee Promotion cannot be submitted before Promotion Date"), + frappe.DocstatusTransitionError, + ) def on_submit(self): employee = frappe.get_doc("Employee", self.employee) - employee = update_employee_work_history(employee, self.promotion_details, date=self.promotion_date) + employee = update_employee_work_history( + employee, self.promotion_details, date=self.promotion_date + ) employee.save() def on_cancel(self): diff --git a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py index fc9d195a3f3..06825ece910 100644 --- a/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py +++ b/erpnext/hr/doctype/employee_promotion/test_employee_promotion.py @@ -15,18 +15,20 @@ class TestEmployeePromotion(unittest.TestCase): frappe.db.sql("""delete from `tabEmployee Promotion`""") def test_submit_before_promotion_date(self): - promotion_obj = frappe.get_doc({ - "doctype": "Employee Promotion", - "employee": self.employee, - "promotion_details" :[ - { - "property": "Designation", - "current": "Software Developer", - "new": "Project Manager", - "fieldname": "designation" - } - ] - }) + promotion_obj = frappe.get_doc( + { + "doctype": "Employee Promotion", + "employee": self.employee, + "promotion_details": [ + { + "property": "Designation", + "current": "Software Developer", + "new": "Project Manager", + "fieldname": "designation", + } + ], + } + ) promotion_obj.promotion_date = add_days(getdate(), 1) promotion_obj.save() self.assertRaises(frappe.DocstatusTransitionError, promotion_obj.submit) diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py index 4e1780b9978..e349c674d8b 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral.py @@ -30,7 +30,7 @@ class EmployeeReferral(Document): @frappe.whitelist() def create_job_applicant(source_name, target_doc=None): emp_ref = frappe.get_doc("Employee Referral", source_name) - #just for Api call if some set status apart from default Status + # just for Api call if some set status apart from default Status status = emp_ref.status if emp_ref.status in ["Pending", "In process"]: status = "Open" @@ -47,9 +47,13 @@ def create_job_applicant(source_name, target_doc=None): job_applicant.resume_link = emp_ref.resume_link job_applicant.save() - frappe.msgprint(_("Job Applicant {0} created successfully.").format( - get_link_to_form("Job Applicant", job_applicant.name)), - title=_("Success"), indicator="green") + frappe.msgprint( + _("Job Applicant {0} created successfully.").format( + get_link_to_form("Job Applicant", job_applicant.name) + ), + title=_("Success"), + indicator="green", + ) emp_ref.db_set("status", "In Process") diff --git a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py index 1733ac9726e..4d683fbfcf3 100644 --- a/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py +++ b/erpnext/hr/doctype/employee_referral/employee_referral_dashboard.py @@ -1,15 +1,8 @@ - - def get_data(): return { - 'fieldname': 'employee_referral', - 'non_standard_fieldnames': { - 'Additional Salary': 'ref_docname' - }, - 'transactions': [ - { - 'items': ['Job Applicant', 'Additional Salary'] - }, - - ] + "fieldname": "employee_referral", + "non_standard_fieldnames": {"Additional Salary": "ref_docname"}, + "transactions": [ + {"items": ["Job Applicant", "Additional Salary"]}, + ], } diff --git a/erpnext/hr/doctype/employee_referral/test_employee_referral.py b/erpnext/hr/doctype/employee_referral/test_employee_referral.py index 529e3551454..475a935e864 100644 --- a/erpnext/hr/doctype/employee_referral/test_employee_referral.py +++ b/erpnext/hr/doctype/employee_referral/test_employee_referral.py @@ -15,7 +15,6 @@ from erpnext.hr.doctype.employee_referral.employee_referral import ( class TestEmployeeReferral(unittest.TestCase): - def setUp(self): frappe.db.sql("DELETE FROM `tabJob Applicant`") frappe.db.sql("DELETE FROM `tabEmployee Referral`") @@ -23,13 +22,12 @@ class TestEmployeeReferral(unittest.TestCase): def test_workflow_and_status_sync(self): emp_ref = create_employee_referral() - #Check Initial status + # Check Initial status self.assertTrue(emp_ref.status, "Pending") job_applicant = create_job_applicant(emp_ref.name) - - #Check status sync + # Check status sync emp_ref.reload() self.assertTrue(emp_ref.status, "In Process") @@ -47,7 +45,6 @@ class TestEmployeeReferral(unittest.TestCase): emp_ref.reload() self.assertTrue(emp_ref.status, "Accepted") - # Check for Referral reference in additional salary add_sal = create_additional_salary(emp_ref) diff --git a/erpnext/hr/doctype/employee_separation/test_employee_separation.py b/erpnext/hr/doctype/employee_separation/test_employee_separation.py index 0007b9e1f38..5ba57bad899 100644 --- a/erpnext/hr/doctype/employee_separation/test_employee_separation.py +++ b/erpnext/hr/doctype/employee_separation/test_employee_separation.py @@ -7,17 +7,15 @@ import frappe test_dependencies = ["Employee Onboarding"] + class TestEmployeeSeparation(unittest.TestCase): def test_employee_separation(self): employee = frappe.db.get_value("Employee", {"status": "Active"}) - separation = frappe.new_doc('Employee Separation') + separation = frappe.new_doc("Employee Separation") separation.employee = employee - separation.company = '_Test Company' - separation.append('activities', { - 'activity_name': 'Deactivate Employee', - 'role': 'HR User' - }) - separation.boarding_status = 'Pending' + separation.company = "_Test Company" + separation.append("activities", {"activity_name": "Deactivate Employee", "role": "HR User"}) + separation.boarding_status = "Pending" separation.insert() separation.submit() self.assertEqual(separation.docstatus, 1) diff --git a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py index f165d0a0eb4..3ffd8dd6e2c 100644 --- a/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py +++ b/erpnext/hr/doctype/employee_separation_template/employee_separation_template_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'employee_separation_template', - 'transactions': [ - { - 'items': ['Employee Separation'] - }, - ], - } + return { + "fieldname": "employee_separation_template", + "transactions": [ + {"items": ["Employee Separation"]}, + ], + } diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py index f927d413ae3..6dbefe59da5 100644 --- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py @@ -13,8 +13,10 @@ from erpnext.hr.utils import update_employee_work_history class EmployeeTransfer(Document): def before_submit(self): if getdate(self.transfer_date) > getdate(): - frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), - frappe.DocstatusTransitionError) + frappe.throw( + _("Employee Transfer cannot be submitted before Transfer Date"), + frappe.DocstatusTransitionError, + ) def on_submit(self): employee = frappe.get_doc("Employee", self.employee) @@ -22,22 +24,26 @@ class EmployeeTransfer(Document): new_employee = frappe.copy_doc(employee) new_employee.name = None new_employee.employee_number = None - new_employee = update_employee_work_history(new_employee, self.transfer_details, date=self.transfer_date) + new_employee = update_employee_work_history( + new_employee, self.transfer_details, date=self.transfer_date + ) if self.new_company and self.company != self.new_company: new_employee.internal_work_history = [] new_employee.date_of_joining = self.transfer_date new_employee.company = self.new_company - #move user_id to new employee before insert + # move user_id to new employee before insert if employee.user_id and not self.validate_user_in_details(): new_employee.user_id = employee.user_id employee.db_set("user_id", "") new_employee.insert() self.db_set("new_employee_id", new_employee.name) - #relieve the old employee + # relieve the old employee employee.db_set("relieving_date", self.transfer_date) employee.db_set("status", "Left") else: - employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date) + employee = update_employee_work_history( + employee, self.transfer_details, date=self.transfer_date + ) if self.new_company and self.company != self.new_company: employee.company = self.new_company employee.date_of_joining = self.transfer_date @@ -47,14 +53,18 @@ class EmployeeTransfer(Document): employee = frappe.get_doc("Employee", self.employee) if self.create_new_employee_id: if self.new_employee_id: - frappe.throw(_("Please delete the Employee {0} to cancel this document").format( - "{0}".format(self.new_employee_id) - )) - #mark the employee as active + frappe.throw( + _("Please delete the Employee {0} to cancel this document").format( + "{0}".format(self.new_employee_id) + ) + ) + # mark the employee as active employee.status = "Active" - employee.relieving_date = '' + employee.relieving_date = "" else: - employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True) + employee = update_employee_work_history( + employee, self.transfer_details, date=self.transfer_date, cancel=True + ) if self.new_company != self.company: employee.company = self.company employee.save() diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py index 64eee402fec..37a190a1627 100644 --- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py +++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py @@ -19,18 +19,20 @@ class TestEmployeeTransfer(unittest.TestCase): def test_submit_before_transfer_date(self): make_employee("employee2@transfers.com") - transfer_obj = frappe.get_doc({ - "doctype": "Employee Transfer", - "employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"), - "transfer_details" :[ - { - "property": "Designation", - "current": "Software Developer", - "new": "Project Manager", - "fieldname": "designation" - } - ] - }) + transfer_obj = frappe.get_doc( + { + "doctype": "Employee Transfer", + "employee": frappe.get_value("Employee", {"user_id": "employee2@transfers.com"}, "name"), + "transfer_details": [ + { + "property": "Designation", + "current": "Software Developer", + "new": "Project Manager", + "fieldname": "designation", + } + ], + } + ) transfer_obj.transfer_date = add_days(getdate(), 1) transfer_obj.save() self.assertRaises(frappe.DocstatusTransitionError, transfer_obj.submit) @@ -42,32 +44,35 @@ class TestEmployeeTransfer(unittest.TestCase): def test_new_employee_creation(self): make_employee("employee3@transfers.com") - transfer = frappe.get_doc({ - "doctype": "Employee Transfer", - "employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"), - "create_new_employee_id": 1, - "transfer_date": getdate(), - "transfer_details" :[ - { - "property": "Designation", - "current": "Software Developer", - "new": "Project Manager", - "fieldname": "designation" - } - ] - }).insert() + transfer = frappe.get_doc( + { + "doctype": "Employee Transfer", + "employee": frappe.get_value("Employee", {"user_id": "employee3@transfers.com"}, "name"), + "create_new_employee_id": 1, + "transfer_date": getdate(), + "transfer_details": [ + { + "property": "Designation", + "current": "Software Developer", + "new": "Project Manager", + "fieldname": "designation", + } + ], + } + ).insert() transfer.submit() self.assertTrue(transfer.new_employee_id) self.assertEqual(frappe.get_value("Employee", transfer.new_employee_id, "status"), "Active") self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left") def test_employee_history(self): - employee = make_employee("employee4@transfers.com", + employee = make_employee( + "employee4@transfers.com", company="Test Company", date_of_birth=getdate("30-09-1980"), date_of_joining=getdate("01-10-2021"), department="Accounts - TC", - designation="Accountant" + designation="Accountant", ) transfer = create_employee_transfer(employee) @@ -94,36 +99,40 @@ class TestEmployeeTransfer(unittest.TestCase): def create_company(): if not frappe.db.exists("Company", "Test Company"): - frappe.get_doc({ - "doctype": "Company", - "company_name": "Test Company", - "default_currency": "INR", - "country": "India" - }).insert() + frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test Company", + "default_currency": "INR", + "country": "India", + } + ).insert() def create_employee_transfer(employee): - doc = frappe.get_doc({ - "doctype": "Employee Transfer", - "employee": employee, - "transfer_date": getdate(), - "transfer_details": [ - { - "property": "Designation", - "current": "Accountant", - "new": "Manager", - "fieldname": "designation" - }, - { - "property": "Department", - "current": "Accounts - TC", - "new": "Management - TC", - "fieldname": "department" - } - ] - }) + doc = frappe.get_doc( + { + "doctype": "Employee Transfer", + "employee": employee, + "transfer_date": getdate(), + "transfer_details": [ + { + "property": "Designation", + "current": "Accountant", + "new": "Manager", + "fieldname": "designation", + }, + { + "property": "Department", + "current": "Accounts - TC", + "new": "Management - TC", + "fieldname": "department", + }, + ], + } + ) doc.save() doc.submit() - return doc \ No newline at end of file + return doc diff --git a/erpnext/hr/doctype/employment_type/test_employment_type.py b/erpnext/hr/doctype/employment_type/test_employment_type.py index c43f9636c70..fdf6965e48a 100644 --- a/erpnext/hr/doctype/employment_type/test_employment_type.py +++ b/erpnext/hr/doctype/employment_type/test_employment_type.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Employment Type') +test_records = frappe.get_test_records("Employment Type") diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index c1bf9c2d984..311a1eb81c8 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -13,13 +13,19 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee -class InvalidExpenseApproverError(frappe.ValidationError): pass -class ExpenseApproverIdentityError(frappe.ValidationError): pass +class InvalidExpenseApproverError(frappe.ValidationError): + pass + + +class ExpenseApproverIdentityError(frappe.ValidationError): + pass + class ExpenseClaim(AccountsController): def onload(self): - self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value('Accounts Settings', - 'make_payment_via_journal_entry') + self.get("__onload").make_payment_via_journal_entry = frappe.db.get_single_value( + "Accounts Settings", "make_payment_via_journal_entry" + ) def validate(self): validate_active_employee(self.employee) @@ -36,29 +42,35 @@ class ExpenseClaim(AccountsController): self.project = frappe.db.get_value("Task", self.task, "project") def set_status(self, update=False): - status = { - "0": "Draft", - "1": "Submitted", - "2": "Cancelled" - }[cstr(self.docstatus or 0)] + status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[cstr(self.docstatus or 0)] precision = self.precision("grand_total") if ( # set as paid self.is_paid - or (flt(self.total_sanctioned_amount > 0) and ( - # grand total is reimbursed - (self.docstatus == 1 and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision)) - # grand total (to be paid) is 0 since linked advances already cover the claimed amount - or (flt(self.grand_total, precision) == 0) - )) + or ( + flt(self.total_sanctioned_amount > 0) + and ( + # grand total is reimbursed + ( + self.docstatus == 1 + and flt(self.grand_total, precision) == flt(self.total_amount_reimbursed, precision) + ) + # grand total (to be paid) is 0 since linked advances already cover the claimed amount + or (flt(self.grand_total, precision) == 0) + ) + ) ) and self.approval_status == "Approved": status = "Paid" - elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': + elif ( + flt(self.total_sanctioned_amount) > 0 + and self.docstatus == 1 + and self.approval_status == "Approved" + ): status = "Unpaid" - elif self.docstatus == 1 and self.approval_status == 'Rejected': - status = 'Rejected' + elif self.docstatus == 1 and self.approval_status == "Rejected": + status = "Rejected" if update: self.db_set("status", status) @@ -70,14 +82,16 @@ class ExpenseClaim(AccountsController): def set_payable_account(self): if not self.payable_account and not self.is_paid: - self.payable_account = frappe.get_cached_value('Company', self.company, 'default_expense_claim_payable_account') + self.payable_account = frappe.get_cached_value( + "Company", self.company, "default_expense_claim_payable_account" + ) def set_cost_center(self): if not self.cost_center: - self.cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') + self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") def on_submit(self): - if self.approval_status=="Draft": + if self.approval_status == "Draft": frappe.throw(_("""Approval Status must be 'Approved' or 'Rejected'""")) self.update_task_and_project() @@ -91,7 +105,7 @@ class ExpenseClaim(AccountsController): def on_cancel(self): self.update_task_and_project() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") if self.payable_account: self.make_gl_entries(cancel=True) @@ -122,43 +136,51 @@ class ExpenseClaim(AccountsController): # payable entry if self.grand_total: gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "credit": self.grand_total, - "credit_in_account_currency": self.grand_total, - "against": ",".join([d.default_account for d in self.expenses]), - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.payable_account, + "credit": self.grand_total, + "credit_in_account_currency": self.grand_total, + "against": ",".join([d.default_account for d in self.expenses]), + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "cost_center": self.cost_center, + }, + item=self, + ) ) # expense entries for data in self.expenses: gl_entry.append( - self.get_gl_dict({ - "account": data.default_account, - "debit": data.sanctioned_amount, - "debit_in_account_currency": data.sanctioned_amount, - "against": self.employee, - "cost_center": data.cost_center or self.cost_center - }, item=data) + self.get_gl_dict( + { + "account": data.default_account, + "debit": data.sanctioned_amount, + "debit_in_account_currency": data.sanctioned_amount, + "against": self.employee, + "cost_center": data.cost_center or self.cost_center, + }, + item=data, + ) ) for data in self.advances: gl_entry.append( - self.get_gl_dict({ - "account": data.advance_account, - "credit": data.allocated_amount, - "credit_in_account_currency": data.allocated_amount, - "against": ",".join([d.default_account for d in self.expenses]), - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": "Employee Advance", - "against_voucher": data.employee_advance - }) + self.get_gl_dict( + { + "account": data.advance_account, + "credit": data.allocated_amount, + "credit_in_account_currency": data.allocated_amount, + "against": ",".join([d.default_account for d in self.expenses]), + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": "Employee Advance", + "against_voucher": data.employee_advance, + } + ) ) self.add_tax_gl_entries(gl_entry) @@ -167,25 +189,31 @@ class ExpenseClaim(AccountsController): # payment entry payment_account = get_bank_cash_account(self.mode_of_payment, self.company).get("account") gl_entry.append( - self.get_gl_dict({ - "account": payment_account, - "credit": self.grand_total, - "credit_in_account_currency": self.grand_total, - "against": self.employee - }, item=self) + self.get_gl_dict( + { + "account": payment_account, + "credit": self.grand_total, + "credit_in_account_currency": self.grand_total, + "against": self.employee, + }, + item=self, + ) ) gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "party_type": "Employee", - "party": self.employee, - "against": payment_account, - "debit": self.grand_total, - "debit_in_account_currency": self.grand_total, - "against_voucher": self.name, - "against_voucher_type": self.doctype, - }, item=self) + self.get_gl_dict( + { + "account": self.payable_account, + "party_type": "Employee", + "party": self.employee, + "against": payment_account, + "debit": self.grand_total, + "debit_in_account_currency": self.grand_total, + "against_voucher": self.name, + "against_voucher_type": self.doctype, + }, + item=self, + ) ) return gl_entry @@ -194,22 +222,28 @@ class ExpenseClaim(AccountsController): # tax table gl entries for tax in self.get("taxes"): gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "debit": tax.tax_amount, - "debit_in_account_currency": tax.tax_amount, - "against": self.employee, - "cost_center": self.cost_center, - "against_voucher_type": self.doctype, - "against_voucher": self.name - }, item=tax) + self.get_gl_dict( + { + "account": tax.account_head, + "debit": tax.tax_amount, + "debit_in_account_currency": tax.tax_amount, + "against": self.employee, + "cost_center": self.cost_center, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + }, + item=tax, + ) ) def validate_account_details(self): for data in self.expenses: if not data.cost_center: - frappe.throw(_("Row {0}: {1} is required in the expenses table to book an expense claim.") - .format(data.idx, frappe.bold("Cost Center"))) + frappe.throw( + _("Row {0}: {1} is required in the expenses table to book an expense claim.").format( + data.idx, frappe.bold("Cost Center") + ) + ) if self.is_paid: if not self.mode_of_payment: @@ -218,8 +252,8 @@ class ExpenseClaim(AccountsController): def calculate_total_amount(self): self.total_claimed_amount = 0 self.total_sanctioned_amount = 0 - for d in self.get('expenses'): - if self.approval_status == 'Rejected': + for d in self.get("expenses"): + if self.approval_status == "Rejected": d.sanctioned_amount = 0.0 self.total_claimed_amount += flt(d.amount) @@ -230,12 +264,16 @@ class ExpenseClaim(AccountsController): self.total_taxes_and_charges = 0 for tax in self.taxes: if tax.rate: - tax.tax_amount = flt(self.total_sanctioned_amount) * flt(tax.rate/100) + tax.tax_amount = flt(self.total_sanctioned_amount) * flt(tax.rate / 100) tax.total = flt(tax.tax_amount) + flt(self.total_sanctioned_amount) self.total_taxes_and_charges += flt(tax.tax_amount) - self.grand_total = flt(self.total_sanctioned_amount) + flt(self.total_taxes_and_charges) - flt(self.total_advance_amount) + self.grand_total = ( + flt(self.total_sanctioned_amount) + + flt(self.total_taxes_and_charges) + - flt(self.total_advance_amount) + ) def update_task(self): task = frappe.get_doc("Task", self.task) @@ -245,16 +283,23 @@ class ExpenseClaim(AccountsController): def validate_advances(self): self.total_advance_amount = 0 for d in self.get("advances"): - ref_doc = frappe.db.get_value("Employee Advance", d.employee_advance, - ["posting_date", "paid_amount", "claimed_amount", "advance_account"], as_dict=1) + ref_doc = frappe.db.get_value( + "Employee Advance", + d.employee_advance, + ["posting_date", "paid_amount", "claimed_amount", "advance_account"], + as_dict=1, + ) d.posting_date = ref_doc.posting_date d.advance_account = ref_doc.advance_account d.advance_paid = ref_doc.paid_amount d.unclaimed_amount = flt(ref_doc.paid_amount) - flt(ref_doc.claimed_amount) if d.allocated_amount and flt(d.allocated_amount) > flt(d.unclaimed_amount): - frappe.throw(_("Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2}") - .format(d.idx, d.allocated_amount, d.unclaimed_amount)) + frappe.throw( + _("Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2}").format( + d.idx, d.allocated_amount, d.unclaimed_amount + ) + ) self.total_advance_amount += flt(d.allocated_amount) @@ -263,27 +308,36 @@ class ExpenseClaim(AccountsController): if flt(self.total_advance_amount, precision) > flt(self.total_claimed_amount, precision): frappe.throw(_("Total advance amount cannot be greater than total claimed amount")) - if self.total_sanctioned_amount \ - and flt(self.total_advance_amount, precision) > flt(self.total_sanctioned_amount, precision): + if self.total_sanctioned_amount and flt(self.total_advance_amount, precision) > flt( + self.total_sanctioned_amount, precision + ): frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount")) def validate_sanctioned_amount(self): - for d in self.get('expenses'): + for d in self.get("expenses"): if flt(d.sanctioned_amount) > flt(d.amount): - frappe.throw(_("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx)) + frappe.throw( + _("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx) + ) def set_expense_account(self, validate=False): for expense in self.expenses: if not expense.default_account or not validate: - expense.default_account = get_expense_claim_account(expense.expense_type, self.company)["account"] + expense.default_account = get_expense_claim_account(expense.expense_type, self.company)[ + "account" + ] + def update_reimbursed_amount(doc, amount): doc.total_amount_reimbursed += amount - frappe.db.set_value("Expense Claim", doc.name , "total_amount_reimbursed", doc.total_amount_reimbursed) + frappe.db.set_value( + "Expense Claim", doc.name, "total_amount_reimbursed", doc.total_amount_reimbursed + ) doc.set_status() - frappe.db.set_value("Expense Claim", doc.name , "status", doc.status) + frappe.db.set_value("Expense Claim", doc.name, "status", doc.status) + @frappe.whitelist() def make_bank_entry(dt, dn): @@ -294,96 +348,115 @@ def make_bank_entry(dt, dn): if not default_bank_cash_account: default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash") - payable_amount = flt(expense_claim.total_sanctioned_amount) \ - - flt(expense_claim.total_amount_reimbursed) - flt(expense_claim.total_advance_amount) + payable_amount = ( + flt(expense_claim.total_sanctioned_amount) + - flt(expense_claim.total_amount_reimbursed) + - flt(expense_claim.total_advance_amount) + ) je = frappe.new_doc("Journal Entry") - je.voucher_type = 'Bank Entry' + je.voucher_type = "Bank Entry" je.company = expense_claim.company - je.remark = 'Payment against Expense Claim: ' + dn + je.remark = "Payment against Expense Claim: " + dn - je.append("accounts", { - "account": expense_claim.payable_account, - "debit_in_account_currency": payable_amount, - "reference_type": "Expense Claim", - "party_type": "Employee", - "party": expense_claim.employee, - "cost_center": erpnext.get_default_cost_center(expense_claim.company), - "reference_name": expense_claim.name - }) + je.append( + "accounts", + { + "account": expense_claim.payable_account, + "debit_in_account_currency": payable_amount, + "reference_type": "Expense Claim", + "party_type": "Employee", + "party": expense_claim.employee, + "cost_center": erpnext.get_default_cost_center(expense_claim.company), + "reference_name": expense_claim.name, + }, + ) - je.append("accounts", { - "account": default_bank_cash_account.account, - "credit_in_account_currency": payable_amount, - "reference_type": "Expense Claim", - "reference_name": expense_claim.name, - "balance": default_bank_cash_account.balance, - "account_currency": default_bank_cash_account.account_currency, - "cost_center": erpnext.get_default_cost_center(expense_claim.company), - "account_type": default_bank_cash_account.account_type - }) + je.append( + "accounts", + { + "account": default_bank_cash_account.account, + "credit_in_account_currency": payable_amount, + "reference_type": "Expense Claim", + "reference_name": expense_claim.name, + "balance": default_bank_cash_account.balance, + "account_currency": default_bank_cash_account.account_currency, + "cost_center": erpnext.get_default_cost_center(expense_claim.company), + "account_type": default_bank_cash_account.account_type, + }, + ) return je.as_dict() + @frappe.whitelist() def get_expense_claim_account_and_cost_center(expense_claim_type, company): data = get_expense_claim_account(expense_claim_type, company) cost_center = erpnext.get_default_cost_center(company) - return { - "account": data.get("account"), - "cost_center": cost_center - } + return {"account": data.get("account"), "cost_center": cost_center} + @frappe.whitelist() def get_expense_claim_account(expense_claim_type, company): - account = frappe.db.get_value("Expense Claim Account", - {"parent": expense_claim_type, "company": company}, "default_account") + account = frappe.db.get_value( + "Expense Claim Account", {"parent": expense_claim_type, "company": company}, "default_account" + ) if not account: - frappe.throw(_("Set the default account for the {0} {1}") - .format(frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type))) + frappe.throw( + _("Set the default account for the {0} {1}").format( + frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type) + ) + ) + + return {"account": account} - return { - "account": account - } @frappe.whitelist() def get_advances(employee, advance_id=None): if not advance_id: - condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee)) + condition = "docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount".format( + frappe.db.escape(employee) + ) else: - condition = 'name={0}'.format(frappe.db.escape(advance_id)) + condition = "name={0}".format(frappe.db.escape(advance_id)) - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, posting_date, paid_amount, claimed_amount, advance_account from `tabEmployee Advance` where {0} - """.format(condition), as_dict=1) + """.format( + condition + ), + as_dict=1, + ) @frappe.whitelist() def get_expense_claim( - employee_name, company, employee_advance_name, posting_date, paid_amount, claimed_amount): - default_payable_account = frappe.get_cached_value('Company', company, "default_payable_account") - default_cost_center = frappe.get_cached_value('Company', company, 'cost_center') + employee_name, company, employee_advance_name, posting_date, paid_amount, claimed_amount +): + default_payable_account = frappe.get_cached_value("Company", company, "default_payable_account") + default_cost_center = frappe.get_cached_value("Company", company, "cost_center") - expense_claim = frappe.new_doc('Expense Claim') + expense_claim = frappe.new_doc("Expense Claim") expense_claim.company = company expense_claim.employee = employee_name expense_claim.payable_account = default_payable_account expense_claim.cost_center = default_cost_center expense_claim.is_paid = 1 if flt(paid_amount) else 0 expense_claim.append( - 'advances', + "advances", { - 'employee_advance': employee_advance_name, - 'posting_date': posting_date, - 'advance_paid': flt(paid_amount), - 'unclaimed_amount': flt(paid_amount) - flt(claimed_amount), - 'allocated_amount': flt(paid_amount) - flt(claimed_amount) - } + "employee_advance": employee_advance_name, + "posting_date": posting_date, + "advance_paid": flt(paid_amount), + "unclaimed_amount": flt(paid_amount) - flt(claimed_amount), + "allocated_amount": flt(paid_amount) - flt(claimed_amount), + }, ) return expense_claim diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py index 44052cc8e6b..8b1acc619ee 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'reference_name', - 'internal_links': { - 'Employee Advance': ['advances', 'employee_advance'] - }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Journal Entry'] - }, - { - 'label': _('Reference'), - 'items': ['Employee Advance'] - }, - ] + "fieldname": "reference_name", + "internal_links": {"Employee Advance": ["advances", "employee_advance"]}, + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}, + {"label": _("Reference"), "items": ["Employee Advance"]}, + ], } diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 01b74fb24b4..9b3d53a2105 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -10,8 +10,8 @@ from erpnext.accounts.doctype.account.test_account import create_account from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry -test_dependencies = ['Employee'] -company_name = '_Test Company 3' +test_dependencies = ["Employee"] +company_name = "_Test Company 3" class TestExpenseClaim(unittest.TestCase): @@ -23,28 +23,26 @@ class TestExpenseClaim(unittest.TestCase): frappe.db.sql("""delete from `tabProject`""") frappe.db.sql("update `tabExpense Claim` set project = '', task = ''") - project = frappe.get_doc({ - "project_name": "_Test Project 1", - "doctype": "Project" - }) + project = frappe.get_doc({"project_name": "_Test Project 1", "doctype": "Project"}) project.save() - task = frappe.get_doc(dict( - doctype = 'Task', - subject = '_Test Project Task 1', - status = 'Open', - project = project.name - )).insert() + task = frappe.get_doc( + dict(doctype="Task", subject="_Test Project Task 1", status="Open", project=project.name) + ).insert() task_name = task.name payable_account = get_payable_account(company_name) - make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name) + make_expense_claim( + payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name + ) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200) self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200) - expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name) + expense_claim2 = make_expense_claim( + payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name + ) self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700) self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700) @@ -56,7 +54,9 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_status(self): payable_account = get_payable_account(company_name) - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3") + expense_claim = make_expense_claim( + payable_account, 300, 200, company_name, "Travel Expenses - _TC3" + ) je_dict = make_bank_entry("Expense Claim", expense_claim.name) je = frappe.get_doc(je_dict) @@ -78,7 +78,9 @@ class TestExpenseClaim(unittest.TestCase): self.assertEqual(claim.status, "Submitted") # no gl entries created - gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': claim.name}) + gl_entry = frappe.get_all( + "GL Entry", {"voucher_type": "Expense Claim", "voucher_no": claim.name} + ) self.assertEqual(len(gl_entry), 0) def test_expense_claim_against_fully_paid_advances(self): @@ -91,7 +93,9 @@ class TestExpenseClaim(unittest.TestCase): frappe.db.delete("Employee Advance") payable_account = get_payable_account("_Test Company") - claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) advance = make_employee_advance(claim.employee) pe = make_payment_entry(advance) @@ -117,10 +121,12 @@ class TestExpenseClaim(unittest.TestCase): frappe.db.delete("Employee Advance") payable_account = get_payable_account("_Test Company") - claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) # link advance for partial amount - advance = make_employee_advance(claim.employee, {'advance_amount': 500}) + advance = make_employee_advance(claim.employee, {"advance_amount": 500}) pe = make_advance_payment(advance) pe.submit() @@ -141,21 +147,35 @@ class TestExpenseClaim(unittest.TestCase): def test_expense_claim_gl_entry(self): payable_account = get_payable_account(company_name) taxes = generate_taxes() - expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", - do_not_submit=True, taxes=taxes) + expense_claim = make_expense_claim( + payable_account, + 300, + 200, + company_name, + "Travel Expenses - _TC3", + do_not_submit=True, + taxes=taxes, + ) expense_claim.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Expense Claim' and voucher_no=%s - order by account asc""", expense_claim.name, as_dict=1) + order by account asc""", + expense_claim.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ['Output Tax CGST - _TC3',18.0, 0.0], - [payable_account, 0.0, 218.0], - ["Travel Expenses - _TC3", 200.0, 0.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + ["Output Tax CGST - _TC3", 18.0, 0.0], + [payable_account, 0.0, 218.0], + ["Travel Expenses - _TC3", 200.0, 0.0], + ] + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) @@ -164,20 +184,30 @@ class TestExpenseClaim(unittest.TestCase): def test_rejected_expense_claim(self): payable_account = get_payable_account(company_name) - expense_claim = frappe.get_doc({ - "doctype": "Expense Claim", - "employee": "_T-Employee-00001", - "payable_account": payable_account, - "approval_status": "Rejected", - "expenses": - [{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}] - }) + expense_claim = frappe.get_doc( + { + "doctype": "Expense Claim", + "employee": "_T-Employee-00001", + "payable_account": payable_account, + "approval_status": "Rejected", + "expenses": [ + { + "expense_type": "Travel", + "default_account": "Travel Expenses - _TC3", + "amount": 300, + "sanctioned_amount": 200, + } + ], + } + ) expense_claim.submit() - self.assertEqual(expense_claim.status, 'Rejected') + self.assertEqual(expense_claim.status, "Rejected") self.assertEqual(expense_claim.total_sanctioned_amount, 0.0) - gl_entry = frappe.get_all('GL Entry', {'voucher_type': 'Expense Claim', 'voucher_no': expense_claim.name}) + gl_entry = frappe.get_all( + "GL Entry", {"voucher_type": "Expense Claim", "voucher_no": expense_claim.name} + ) self.assertEqual(len(gl_entry), 0) def test_expense_approver_perms(self): @@ -186,7 +216,9 @@ class TestExpenseClaim(unittest.TestCase): # check doc shared payable_account = get_payable_account("_Test Company") - expense_claim = make_expense_claim(payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + expense_claim = make_expense_claim( + payable_account, 300, 200, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) expense_claim.expense_approver = user expense_claim.save() self.assertTrue(expense_claim.name in frappe.share.get_shared("Expense Claim", user)) @@ -210,51 +242,76 @@ class TestExpenseClaim(unittest.TestCase): def test_multiple_payment_entries_against_expense(self): # Creating expense claim payable_account = get_payable_account("_Test Company") - expense_claim = make_expense_claim(payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC") + expense_claim = make_expense_claim( + payable_account, 5500, 5500, "_Test Company", "Travel Expenses - _TC" + ) expense_claim.save() expense_claim.submit() # Payment entry 1: paying 500 - make_payment_entry(expense_claim, payable_account,500) - outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + make_payment_entry(expense_claim, payable_account, 500) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts( + expense_claim + ) self.assertEqual(outstanding_amount, 5000) self.assertEqual(total_amount_reimbursed, 500) # Payment entry 1: paying 2000 - make_payment_entry(expense_claim, payable_account,2000) - outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + make_payment_entry(expense_claim, payable_account, 2000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts( + expense_claim + ) self.assertEqual(outstanding_amount, 3000) self.assertEqual(total_amount_reimbursed, 2500) # Payment entry 1: paying 3000 - make_payment_entry(expense_claim, payable_account,3000) - outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts(expense_claim) + make_payment_entry(expense_claim, payable_account, 3000) + outstanding_amount, total_amount_reimbursed = get_outstanding_and_total_reimbursed_amounts( + expense_claim + ) self.assertEqual(outstanding_amount, 0) self.assertEqual(total_amount_reimbursed, 5500) def get_payable_account(company): - return frappe.get_cached_value('Company', company, 'default_payable_account') + return frappe.get_cached_value("Company", company, "default_payable_account") + def generate_taxes(): - parent_account = frappe.db.get_value('Account', - {'company': company_name, 'is_group':1, 'account_type': 'Tax'}, - 'name') - account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account) - return {'taxes':[{ - "account_head": account, - "rate": 9, - "description": "CGST", - "tax_amount": 10, - "total": 210 - }]} + parent_account = frappe.db.get_value( + "Account", {"company": company_name, "is_group": 1, "account_type": "Tax"}, "name" + ) + account = create_account( + company=company_name, + account_name="Output Tax CGST", + account_type="Tax", + parent_account=parent_account, + ) + return { + "taxes": [ + {"account_head": account, "rate": 9, "description": "CGST", "tax_amount": 10, "total": 210} + ] + } -def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): + +def make_expense_claim( + payable_account, + amount, + sanctioned_amount, + company, + account, + project=None, + task_name=None, + do_not_submit=False, + taxes=None, +): employee = frappe.db.get_value("Employee", {"status": "Active"}) if not employee: employee = make_employee("test_employee@expense_claim.com", company=company) - currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) + currency, cost_center = frappe.db.get_value( + "Company", company, ["default_currency", "cost_center"] + ) expense_claim = { "doctype": "Expense Claim", "employee": employee, @@ -262,14 +319,16 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco "approval_status": "Approved", "company": company, "currency": currency, - "expenses": [{ - "expense_type": "Travel", - "default_account": account, - "currency": currency, - "amount": amount, - "sanctioned_amount": sanctioned_amount, - "cost_center": cost_center - }] + "expenses": [ + { + "expense_type": "Travel", + "default_account": account, + "currency": currency, + "amount": amount, + "sanctioned_amount": sanctioned_amount, + "cost_center": cost_center, + } + ], } if taxes: expense_claim.update(taxes) @@ -286,17 +345,24 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco expense_claim.submit() return expense_claim -def get_outstanding_and_total_reimbursed_amounts(expense_claim): - outstanding_amount = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount")) - \ - flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) - total_amount_reimbursed = flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) - return outstanding_amount,total_amount_reimbursed +def get_outstanding_and_total_reimbursed_amounts(expense_claim): + outstanding_amount = flt( + frappe.db.get_value("Expense Claim", expense_claim.name, "total_sanctioned_amount") + ) - flt(frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed")) + total_amount_reimbursed = flt( + frappe.db.get_value("Expense Claim", expense_claim.name, "total_amount_reimbursed") + ) + + return outstanding_amount, total_amount_reimbursed + def make_payment_entry(expense_claim, payable_account, amt): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - pe = get_payment_entry("Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt) + pe = get_payment_entry( + "Expense Claim", expense_claim.name, bank_account="_Test Bank USD - _TC", bank_amount=amt + ) pe.reference_no = "1" pe.reference_date = nowdate() pe.source_exchange_rate = 1 @@ -304,4 +370,3 @@ def make_payment_entry(expense_claim, payable_account, amt): pe.references[0].allocated_amount = amt pe.insert() pe.submit() - diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py index 570b2c115fa..6d29f7d8e71 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.py @@ -18,12 +18,13 @@ class ExpenseClaimType(Document): for entry in self.accounts: accounts_list.append(entry.company) - if len(accounts_list)!= len(set(accounts_list)): + if len(accounts_list) != len(set(accounts_list)): frappe.throw(_("Same Company is entered more than once")) def validate_accounts(self): for entry in self.accounts: """Error when Company of Ledger account doesn't match with Company Selected""" if frappe.db.get_value("Account", entry.default_account, "company") != entry.company: - frappe.throw(_("Account {0} does not match with Company {1}" - ).format(entry.default_account, entry.company)) + frappe.throw( + _("Account {0} does not match with Company {1}").format(entry.default_account, entry.company) + ) diff --git a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py index a2403b6eb8f..62348e28255 100644 --- a/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py +++ b/erpnext/hr/doctype/expense_claim_type/test_expense_claim_type.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Expense Claim Type') + class TestExpenseClaimType(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 35775ab816c..ea32ba744d8 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -10,7 +10,9 @@ from frappe.model.document import Document from frappe.utils import cint, formatdate, getdate, today -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + class HolidayList(Document): def validate(self): @@ -21,9 +23,14 @@ class HolidayList(Document): def get_weekly_off_dates(self): self.validate_values() date_list = self.get_weekly_off_date_list(self.from_date, self.to_date) - last_idx = max([cint(d.idx) for d in self.get("holidays")] or [0,]) + last_idx = max( + [cint(d.idx) for d in self.get("holidays")] + or [ + 0, + ] + ) for i, d in enumerate(date_list): - ch = self.append('holidays', {}) + ch = self.append("holidays", {}) ch.description = self.weekly_off ch.holiday_date = d ch.weekly_off = 1 @@ -33,14 +40,17 @@ class HolidayList(Document): if not self.weekly_off: throw(_("Please select weekly off day")) - def validate_days(self): if getdate(self.from_date) > getdate(self.to_date): throw(_("To Date cannot be before From Date")) for day in self.get("holidays"): if not (getdate(self.from_date) <= getdate(day.holiday_date) <= getdate(self.to_date)): - frappe.throw(_("The holiday on {0} is not between From Date and To Date").format(formatdate(day.holiday_date))) + frappe.throw( + _("The holiday on {0} is not between From Date and To Date").format( + formatdate(day.holiday_date) + ) + ) def get_weekly_off_date_list(self, start_date, end_date): start_date, end_date = getdate(start_date), getdate(end_date) @@ -66,7 +76,8 @@ class HolidayList(Document): @frappe.whitelist() def clear_table(self): - self.set('holidays', []) + self.set("holidays", []) + @frappe.whitelist() def get_events(start, end, filters=None): @@ -82,23 +93,28 @@ def get_events(start, end, filters=None): filters = [] if start: - filters.append(['Holiday', 'holiday_date', '>', getdate(start)]) + filters.append(["Holiday", "holiday_date", ">", getdate(start)]) if end: - filters.append(['Holiday', 'holiday_date', '<', getdate(end)]) + filters.append(["Holiday", "holiday_date", "<", getdate(end)]) - return frappe.get_list('Holiday List', - fields=['name', '`tabHoliday`.holiday_date', '`tabHoliday`.description', '`tabHoliday List`.color'], - filters = filters, - update={"allDay": 1}) + return frappe.get_list( + "Holiday List", + fields=[ + "name", + "`tabHoliday`.holiday_date", + "`tabHoliday`.description", + "`tabHoliday List`.color", + ], + filters=filters, + update={"allDay": 1}, + ) def is_holiday(holiday_list, date=None): - """Returns true if the given date is a holiday in the given holiday list - """ + """Returns true if the given date is a holiday in the given holiday list""" if date is None: date = today() if holiday_list: - return bool(frappe.get_all('Holiday List', - dict(name=holiday_list, holiday_date=date))) + return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date))) else: return False diff --git a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py index e074e266b87..0cbf09461b5 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list_dashboard.py @@ -1,21 +1,15 @@ - - def get_data(): return { - 'fieldname': 'holiday_list', - 'non_standard_fieldnames': { - 'Company': 'default_holiday_list', - 'Leave Period': 'optional_holiday_list' + "fieldname": "holiday_list", + "non_standard_fieldnames": { + "Company": "default_holiday_list", + "Leave Period": "optional_holiday_list", }, - 'transactions': [ + "transactions": [ { - 'items': ['Company', 'Employee', 'Workstation'], + "items": ["Company", "Employee", "Workstation"], }, - { - 'items': ['Leave Period', 'Shift Type'] - }, - { - 'items': ['Service Level', 'Service Level Agreement'] - } - ] + {"items": ["Leave Period", "Shift Type"]}, + {"items": ["Service Level", "Service Level Agreement"]}, + ], } diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py index aed901aa6da..d32cfe82650 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py @@ -12,24 +12,31 @@ from frappe.utils import getdate class TestHolidayList(unittest.TestCase): def test_holiday_list(self): today_date = getdate() - test_holiday_dates = [today_date-timedelta(days=5), today_date-timedelta(days=4)] - holiday_list = make_holiday_list("test_holiday_list", + test_holiday_dates = [today_date - timedelta(days=5), today_date - timedelta(days=4)] + holiday_list = make_holiday_list( + "test_holiday_list", holiday_dates=[ - {'holiday_date': test_holiday_dates[0], 'description': 'test holiday'}, - {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'} - ]) - fetched_holiday_list = frappe.get_value('Holiday List', holiday_list.name) + {"holiday_date": test_holiday_dates[0], "description": "test holiday"}, + {"holiday_date": test_holiday_dates[1], "description": "test holiday2"}, + ], + ) + fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name) self.assertEqual(holiday_list.name, fetched_holiday_list) -def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getdate(), holiday_dates=None): + +def make_holiday_list( + name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None +): frappe.delete_doc_if_exists("Holiday List", name, force=1) - doc = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": name, - "from_date" : from_date, - "to_date" : to_date, - "holidays" : holiday_dates - }).insert() + doc = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": name, + "from_date": from_date, + "to_date": to_date, + "holidays": holiday_dates, + } + ).insert() return doc @@ -39,7 +46,7 @@ def set_holiday_list(holiday_list, company_name): Context manager for setting holiday list in tests """ try: - company = frappe.get_doc('Company', company_name) + company = frappe.get_doc("Company", company_name) previous_holiday_list = company.default_holiday_list company.default_holiday_list = holiday_list @@ -49,6 +56,6 @@ def set_holiday_list(holiday_list, company_name): finally: # restore holiday list setup - company = frappe.get_doc('Company', company_name) + company = frappe.get_doc("Company", company_name) company.default_holiday_list = previous_holiday_list company.save() diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index c295bcbc0d9..72a49e285a0 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -10,6 +10,7 @@ from frappe.utils import format_date # Wether to proceed with frequency change PROCEED_WITH_FREQUENCY_CHANGE = False + class HRSettings(Document): def validate(self): self.set_naming_series() @@ -22,21 +23,24 @@ class HRSettings(Document): def set_naming_series(self): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Employee", "employee_number", - self.get("emp_created_by")=="Naming Series", hide_name_field=True) + + set_by_naming_series( + "Employee", + "employee_number", + self.get("emp_created_by") == "Naming Series", + hide_name_field=True, + ) def validate_frequency_change(self): weekly_job, monthly_job = None, None try: weekly_job = frappe.get_doc( - 'Scheduled Job Type', - 'employee_reminders.send_reminders_in_advance_weekly' + "Scheduled Job Type", "employee_reminders.send_reminders_in_advance_weekly" ) monthly_job = frappe.get_doc( - 'Scheduled Job Type', - 'employee_reminders.send_reminders_in_advance_monthly' + "Scheduled Job Type", "employee_reminders.send_reminders_in_advance_monthly" ) except frappe.DoesNotExistError: return @@ -62,17 +66,20 @@ class HRSettings(Document): from_date = frappe.bold(format_date(from_date)) to_date = frappe.bold(format_date(to_date)) frappe.msgprint( - msg=frappe._('Employees will miss holiday reminders from {} until {}.
    Do you want to proceed with this change?').format(from_date, to_date), - title='Confirm change in Frequency', + msg=frappe._( + "Employees will miss holiday reminders from {} until {}.
    Do you want to proceed with this change?" + ).format(from_date, to_date), + title="Confirm change in Frequency", primary_action={ - 'label': frappe._('Yes, Proceed'), - 'client_action': 'erpnext.proceed_save_with_reminders_frequency_change' + "label": frappe._("Yes, Proceed"), + "client_action": "erpnext.proceed_save_with_reminders_frequency_change", }, - raise_exception=frappe.ValidationError + raise_exception=frappe.ValidationError, ) + @frappe.whitelist() def set_proceed_with_frequency_change(): - '''Enables proceed with frequency change''' + """Enables proceed with frequency change""" global PROCEED_WITH_FREQUENCY_CHANGE PROCEED_WITH_FREQUENCY_CHANGE = True diff --git a/erpnext/hr/doctype/interest/test_interest.py b/erpnext/hr/doctype/interest/test_interest.py index d4ecd9b841e..eacb57f7587 100644 --- a/erpnext/hr/doctype/interest/test_interest.py +++ b/erpnext/hr/doctype/interest/test_interest.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Interest') + class TestInterest(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py index f5312476a28..8f0ff021335 100644 --- a/erpnext/hr/doctype/interview/interview.py +++ b/erpnext/hr/doctype/interview/interview.py @@ -13,6 +13,7 @@ from frappe.utils import cstr, get_datetime, get_link_to_form class DuplicateInterviewRoundError(frappe.ValidationError): pass + class Interview(Document): def validate(self): self.validate_duplicate_interview() @@ -20,37 +21,47 @@ class Interview(Document): self.validate_overlap() def on_submit(self): - if self.status not in ['Cleared', 'Rejected']: - frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed')) + if self.status not in ["Cleared", "Rejected"]: + frappe.throw( + _("Only Interviews with Cleared or Rejected status can be submitted."), title=_("Not Allowed") + ) def validate_duplicate_interview(self): - duplicate_interview = frappe.db.exists('Interview', { - 'job_applicant': self.job_applicant, - 'interview_round': self.interview_round, - 'docstatus': 1 - } + duplicate_interview = frappe.db.exists( + "Interview", + {"job_applicant": self.job_applicant, "interview_round": self.interview_round, "docstatus": 1}, ) if duplicate_interview: - frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format( - frappe.bold(get_link_to_form('Interview', duplicate_interview)), - frappe.bold(self.job_applicant) - )) + frappe.throw( + _( + "Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}" + ).format( + frappe.bold(get_link_to_form("Interview", duplicate_interview)), + frappe.bold(self.job_applicant), + ) + ) def validate_designation(self): - applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation') - if self.designation : + applicant_designation = frappe.db.get_value("Job Applicant", self.job_applicant, "designation") + if self.designation: if self.designation != applicant_designation: - frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format( - self.interview_round, frappe.bold(self.designation), applicant_designation), - exc=DuplicateInterviewRoundError) + frappe.throw( + _( + "Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}" + ).format( + self.interview_round, frappe.bold(self.designation), applicant_designation + ), + exc=DuplicateInterviewRoundError, + ) else: self.designation = applicant_designation def validate_overlap(self): - interviewers = [entry.interviewer for entry in self.interview_details] or [''] + interviewers = [entry.interviewer for entry in self.interview_details] or [""] - overlaps = frappe.db.sql(""" + overlaps = frappe.db.sql( + """ SELECT interview.name FROM `tabInterview` as interview INNER JOIN `tabInterview Detail` as detail @@ -60,13 +71,25 @@ class Interview(Document): ((from_time < %s and to_time > %s) or (from_time > %s and to_time < %s) or (from_time = %s)) - """, (self.scheduled_on, self.name, self.job_applicant, interviewers, - self.from_time, self.to_time, self.from_time, self.to_time, self.from_time)) + """, + ( + self.scheduled_on, + self.name, + self.job_applicant, + interviewers, + self.from_time, + self.to_time, + self.from_time, + self.to_time, + self.from_time, + ), + ) if overlaps: - overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0])) - frappe.throw(overlapping_details, title=_('Overlap')) - + overlapping_details = _("Interview overlaps with {0}").format( + get_link_to_form("Interview", overlaps[0][0]) + ) + frappe.throw(overlapping_details, title=_("Overlap")) @frappe.whitelist() def reschedule_interview(self, scheduled_on, from_time, to_time): @@ -74,116 +97,135 @@ class Interview(Document): from_time = self.from_time to_time = self.to_time - self.db_set({ - 'scheduled_on': scheduled_on, - 'from_time': from_time, - 'to_time': to_time - }) + self.db_set({"scheduled_on": scheduled_on, "from_time": from_time, "to_time": to_time}) self.notify_update() recipients = get_recipients(self.name) try: frappe.sendmail( - recipients= recipients, - subject=_('Interview: {0} Rescheduled').format(self.name), - message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format( - original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time), + recipients=recipients, + subject=_("Interview: {0} Rescheduled").format(self.name), + message=_("Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}").format( + original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time + ), reference_doctype=self.doctype, - reference_name=self.name + reference_name=self.name, ) except Exception: - frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.')) + frappe.msgprint( + _("Failed to send the Interview Reschedule notification. Please configure your email account.") + ) - frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green') + frappe.msgprint(_("Interview Rescheduled successfully"), indicator="green") def get_recipients(name, for_feedback=0): - interview = frappe.get_doc('Interview', name) + interview = frappe.get_doc("Interview", name) if for_feedback: recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback] else: recipients = [d.interviewer for d in interview.interview_details] - recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id')) + recipients.append(frappe.db.get_value("Job Applicant", interview.job_applicant, "email_id")) return recipients @frappe.whitelist() def get_interviewers(interview_round): - return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer']) + return frappe.get_all( + "Interviewer", filters={"parent": interview_round}, fields=["user as interviewer"] + ) def send_interview_reminder(): - reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings', - ['send_interview_reminder', 'interview_reminder_template'], as_dict=True) + reminder_settings = frappe.db.get_value( + "HR Settings", + "HR Settings", + ["send_interview_reminder", "interview_reminder_template"], + as_dict=True, + ) if not reminder_settings.send_interview_reminder: return - remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00' - remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S') + remind_before = cstr(frappe.db.get_single_value("HR Settings", "remind_before")) or "01:00:00" + remind_before = datetime.datetime.strptime(remind_before, "%H:%M:%S") reminder_date_time = datetime.datetime.now() + datetime.timedelta( - hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second) + hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second + ) - interviews = frappe.get_all('Interview', filters={ - 'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)], - 'status': 'Pending', - 'reminded': 0, - 'docstatus': ['!=', 2] - }) + interviews = frappe.get_all( + "Interview", + filters={ + "scheduled_on": ["between", (datetime.datetime.now(), reminder_date_time)], + "status": "Pending", + "reminded": 0, + "docstatus": ["!=", 2], + }, + ) - interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template) + interview_template = frappe.get_doc( + "Email Template", reminder_settings.interview_reminder_template + ) for d in interviews: - doc = frappe.get_doc('Interview', d.name) + doc = frappe.get_doc("Interview", d.name) context = doc.as_dict() message = frappe.render_template(interview_template.response, context) recipients = get_recipients(doc.name) frappe.sendmail( - recipients= recipients, + recipients=recipients, subject=interview_template.subject, message=message, reference_doctype=doc.doctype, - reference_name=doc.name + reference_name=doc.name, ) - doc.db_set('reminded', 1) + doc.db_set("reminded", 1) def send_daily_feedback_reminder(): - reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings', - ['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True) + reminder_settings = frappe.db.get_value( + "HR Settings", + "HR Settings", + ["send_interview_feedback_reminder", "feedback_reminder_notification_template"], + as_dict=True, + ) if not reminder_settings.send_interview_feedback_reminder: return - interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template) - interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]}) + interview_feedback_template = frappe.get_doc( + "Email Template", reminder_settings.feedback_reminder_notification_template + ) + interviews = frappe.get_all( + "Interview", filters={"status": ["in", ["Under Review", "Pending"]], "docstatus": ["!=", 2]} + ) for entry in interviews: recipients = get_recipients(entry.name, for_feedback=1) - doc = frappe.get_doc('Interview', entry.name) + doc = frappe.get_doc("Interview", entry.name) context = doc.as_dict() message = frappe.render_template(interview_feedback_template.response, context) if len(recipients): frappe.sendmail( - recipients= recipients, + recipients=recipients, subject=interview_feedback_template.subject, message=message, - reference_doctype='Interview', - reference_name=entry.name + reference_doctype="Interview", + reference_name=entry.name, ) @frappe.whitelist() def get_expected_skill_set(interview_round): - return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill']) + return frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields=["skill"]) @frappe.whitelist() @@ -196,16 +238,16 @@ def create_interview_feedback(data, interview_name, interviewer, job_applicant): data = frappe._dict(json.loads(data)) if frappe.session.user != interviewer: - frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback')) + frappe.throw(_("Only Interviewer Are allowed to submit Interview Feedback")) - interview_feedback = frappe.new_doc('Interview Feedback') + interview_feedback = frappe.new_doc("Interview Feedback") interview_feedback.interview = interview_name interview_feedback.interviewer = interviewer interview_feedback.job_applicant = job_applicant for d in data.skill_set: d = frappe._dict(d) - interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating}) + interview_feedback.append("skill_assessment", {"skill": d.skill, "rating": d.rating}) interview_feedback.feedback = data.feedback interview_feedback.result = data.result @@ -213,24 +255,33 @@ def create_interview_feedback(data, interview_name, interviewer, job_applicant): interview_feedback.save() interview_feedback.submit() - frappe.msgprint(_('Interview Feedback {0} submitted successfully').format( - get_link_to_form('Interview Feedback', interview_feedback.name))) + frappe.msgprint( + _("Interview Feedback {0} submitted successfully").format( + get_link_to_form("Interview Feedback", interview_feedback.name) + ) + ) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters): filters = [ - ['Has Role', 'parent', 'like', '%{}%'.format(txt)], - ['Has Role', 'role', '=', 'interviewer'], - ['Has Role', 'parenttype', '=', 'User'] + ["Has Role", "parent", "like", "%{}%".format(txt)], + ["Has Role", "role", "=", "interviewer"], + ["Has Role", "parenttype", "=", "User"], ] if filters and isinstance(filters, list): filters.extend(filters) - return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len, - filters=filters, fields = ['parent'], as_list=1) + return frappe.get_all( + "Has Role", + limit_start=start, + limit_page_length=page_len, + filters=filters, + fields=["parent"], + as_list=1, + ) @frappe.whitelist() @@ -249,12 +300,13 @@ def get_events(start, end, filters=None): "Pending": "#fff4f0", "Under Review": "#d3e8fc", "Cleared": "#eaf5ed", - "Rejected": "#fce7e7" + "Rejected": "#fce7e7", } - conditions = get_event_conditions('Interview', filters) + conditions = get_event_conditions("Interview", filters) - interviews = frappe.db.sql(""" + interviews = frappe.db.sql( + """ SELECT DISTINCT `tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round, `tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time, @@ -265,10 +317,13 @@ def get_events(start, end, filters=None): (`tabInterview`.scheduled_on between %(start)s and %(end)s) and docstatus != 2 {conditions} - """.format(conditions=conditions), { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + """.format( + conditions=conditions + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) for d in interviews: subject_data = [] @@ -279,13 +334,13 @@ def get_events(start, end, filters=None): color = event_color.get(d.status) interview_data = { - 'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')), - 'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')), - 'name': d.name, - 'subject': '\n'.join(subject_data), - 'color': color if color else "#89bcde" + "from": get_datetime("%s %s" % (d.scheduled_on, d.from_time or "00:00:00")), + "to": get_datetime("%s %s" % (d.scheduled_on, d.to_time or "00:00:00")), + "name": d.name, + "subject": "\n".join(subject_data), + "color": color if color else "#89bcde", } events.append(interview_data) - return events \ No newline at end of file + return events diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py index 1a2257a6d90..840a5ad919d 100644 --- a/erpnext/hr/doctype/interview/test_interview.py +++ b/erpnext/hr/doctype/interview/test_interview.py @@ -18,23 +18,30 @@ from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_appli class TestInterview(unittest.TestCase): def test_validations_for_designation(self): job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0) + interview = create_interview_and_dependencies( + job_applicant.name, designation="_Test_Sales_manager", save=0 + ) self.assertRaises(DuplicateInterviewRoundError, interview.save) def test_notification_on_rescheduling(self): job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4)) + interview = create_interview_and_dependencies( + job_applicant.name, scheduled_on=add_days(getdate(), -4) + ) previous_scheduled_date = interview.scheduled_on frappe.db.sql("DELETE FROM `tabEmail Queue`") - interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2), - from_time=nowtime(), to_time=nowtime()) + interview.reschedule_interview( + add_days(getdate(previous_scheduled_date), 2), from_time=nowtime(), to_time=nowtime() + ) interview.reload() self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2)) - notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")}) + notification = frappe.get_all( + "Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")} + ) self.assertIsNotNone(notification) def test_notification_for_scheduling(self): @@ -74,16 +81,17 @@ class TestInterview(unittest.TestCase): frappe.db.rollback() -def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1): +def create_interview_and_dependencies( + job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1 +): if designation: - designation=create_designation(designation_name = "_Test_Sales_manager").name + designation = create_designation(designation_name="_Test_Sales_manager").name interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer") interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer") interview_round = create_interview_round( - "Technical Round", ["Python", "JS"], - designation=designation, save=True + "Technical Round", ["Python", "JS"], designation=designation, save=True ) interview = frappe.new_doc("Interview") @@ -101,6 +109,7 @@ def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_tim return interview + def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True): create_skill_set(skill_set) interview_round = frappe.new_doc("Interview Round") @@ -114,15 +123,14 @@ def create_interview_round(name, skill_set, interviewers=[], designation=None, s interview_round.append("expected_skill_set", {"skill": skill}) for interviewer in interviewers: - interview_round.append("interviewer", { - "user": interviewer - }) + interview_round.append("interviewer", {"user": interviewer}) if save: interview_round.save() return interview_round + def create_skill_set(skill_set): for skill in skill_set: if not frappe.db.exists("Skill", skill): @@ -130,6 +138,7 @@ def create_skill_set(skill_set): doc.skill_name = skill doc.save() + def create_interview_type(name="test_interview_type"): if frappe.db.exists("Interview Type", name): return frappe.get_doc("Interview Type", name).name @@ -141,32 +150,41 @@ def create_interview_type(name="test_interview_type"): return doc.name + def setup_reminder_settings(): - if not frappe.db.exists('Email Template', _('Interview Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html')) + if not frappe.db.exists("Email Template", _("Interview Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_reminder_notification_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Reminder'), - 'response': response, - 'subject': _('Interview Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Reminder"), + "response": response, + "subject": _("Interview Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html')) + if not frappe.db.exists("Email Template", _("Interview Feedback Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_feedback_reminder_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Feedback Reminder'), - 'response': response, - 'subject': _('Interview Feedback Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Feedback Reminder"), + "response": response, + "subject": _("Interview Feedback Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - hr_settings = frappe.get_doc('HR Settings') - hr_settings.interview_reminder_template = _('Interview Reminder') - hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder') + hr_settings = frappe.get_doc("HR Settings") + hr_settings.interview_reminder_template = _("Interview Reminder") + hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") hr_settings.save() diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py index d046458f196..b8f8aee524d 100644 --- a/erpnext/hr/doctype/interview_feedback/interview_feedback.py +++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py @@ -24,28 +24,36 @@ class InterviewFeedback(Document): def validate_interviewer(self): applicable_interviewers = get_applicable_interviewers(self.interview) if self.interviewer not in applicable_interviewers: - frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format( - frappe.bold(self.interviewer), frappe.bold(self.interview))) + frappe.throw( + _("{0} is not allowed to submit Interview Feedback for the Interview: {1}").format( + frappe.bold(self.interviewer), frappe.bold(self.interview) + ) + ) def validate_interview_date(self): - scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on') + scheduled_date = frappe.db.get_value("Interview", self.interview, "scheduled_on") if getdate() < getdate(scheduled_date) and self.docstatus == 1: - frappe.throw(_('{0} submission before {1} is not allowed').format( - frappe.bold('Interview Feedback'), - frappe.bold('Interview Scheduled Date') - )) + frappe.throw( + _("{0} submission before {1} is not allowed").format( + frappe.bold("Interview Feedback"), frappe.bold("Interview Scheduled Date") + ) + ) def validate_duplicate(self): - duplicate_feedback = frappe.db.exists('Interview Feedback', { - 'interviewer': self.interviewer, - 'interview': self.interview, - 'docstatus': 1 - }) + duplicate_feedback = frappe.db.exists( + "Interview Feedback", + {"interviewer": self.interviewer, "interview": self.interview, "docstatus": 1}, + ) if duplicate_feedback: - frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format( - self.interview, get_link_to_form('Interview Feedback', duplicate_feedback))) + frappe.throw( + _( + "Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue." + ).format( + self.interview, get_link_to_form("Interview Feedback", duplicate_feedback) + ) + ) def calculate_average_rating(self): total_rating = 0 @@ -53,10 +61,12 @@ class InterviewFeedback(Document): if d.rating: total_rating += d.rating - self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0) + self.average_rating = flt( + total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0 + ) def update_interview_details(self): - doc = frappe.get_doc('Interview', self.interview) + doc = frappe.get_doc("Interview", self.interview) total_rating = 0 if self.docstatus == 2: @@ -75,12 +85,14 @@ class InterviewFeedback(Document): if entry.average_rating: total_rating += entry.average_rating - doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0) + doc.average_rating = flt( + total_rating / len(doc.interview_details) if len(doc.interview_details) else 0 + ) doc.save() doc.notify_update() @frappe.whitelist() def get_applicable_interviewers(interview): - data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer']) + data = frappe.get_all("Interview Detail", filters={"parent": interview}, fields=["interviewer"]) return [d.interviewer for d in data] diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py index 4185f2827a5..0c408b4d35d 100644 --- a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py +++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py @@ -17,14 +17,16 @@ class TestInterviewFeedback(unittest.TestCase): def test_validation_for_skill_set(self): frappe.set_user("Administrator") job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1)) + interview = create_interview_and_dependencies( + job_applicant.name, scheduled_on=add_days(getdate(), -1) + ) skill_ratings = get_skills_rating(interview.interview_round) interviewer = interview.interview_details[0].interviewer - create_skill_set(['Leadership']) + create_skill_set(["Leadership"]) interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings) - interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4}) + interview_feedback.append("skill_assessment", {"skill": "Leadership", "rating": 4}) frappe.set_user(interviewer) self.assertRaises(frappe.ValidationError, interview_feedback.save) @@ -33,7 +35,9 @@ class TestInterviewFeedback(unittest.TestCase): def test_average_ratings_on_feedback_submission_and_cancellation(self): job_applicant = create_job_applicant() - interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1)) + interview = create_interview_and_dependencies( + job_applicant.name, scheduled_on=add_days(getdate(), -1) + ) skill_ratings = get_skills_rating(interview.interview_round) # For First Interviewer Feedback @@ -48,20 +52,26 @@ class TestInterviewFeedback(unittest.TestCase): if d.rating: total_rating += d.rating - avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0) + avg_rating = flt( + total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0 + ) self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating) - avg_on_interview_detail = frappe.db.get_value('Interview Detail', { - 'parent': feedback_1.interview, - 'interviewer': feedback_1.interviewer, - 'interview_feedback': feedback_1.name - }, 'average_rating') + avg_on_interview_detail = frappe.db.get_value( + "Interview Detail", + { + "parent": feedback_1.interview, + "interviewer": feedback_1.interviewer, + "interview_feedback": feedback_1.name, + }, + "average_rating", + ) # 1. average should be reflected in Interview Detail. self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating)) - '''For Second Interviewer Feedback''' + """For Second Interviewer Feedback""" interviewer = interview.interview_details[1].interviewer frappe.set_user(interviewer) @@ -95,7 +105,9 @@ def create_interview_feedback(interview, interviewer, skills_ratings): def get_skills_rating(interview_round): import random - skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"]) + skills = frappe.get_all( + "Expected Skill Set", filters={"parent": interview_round}, fields=["skill"] + ) for d in skills: d["rating"] = random.randint(1, 5) return skills diff --git a/erpnext/hr/doctype/interview_round/interview_round.py b/erpnext/hr/doctype/interview_round/interview_round.py index 0f442c320ad..83dbf0ea98e 100644 --- a/erpnext/hr/doctype/interview_round/interview_round.py +++ b/erpnext/hr/doctype/interview_round/interview_round.py @@ -11,6 +11,7 @@ from frappe.model.document import Document class InterviewRound(Document): pass + @frappe.whitelist() def create_interview(doc): if isinstance(doc, str): @@ -24,10 +25,5 @@ def create_interview(doc): if doc.interviewers: interview.interview_details = [] for data in doc.interviewers: - interview.append("interview_details", { - "interviewer": data.user - }) + interview.append("interview_details", {"interviewer": data.user}) return interview - - - diff --git a/erpnext/hr/doctype/interview_round/test_interview_round.py b/erpnext/hr/doctype/interview_round/test_interview_round.py index dcec9419c07..95681653743 100644 --- a/erpnext/hr/doctype/interview_round/test_interview_round.py +++ b/erpnext/hr/doctype/interview_round/test_interview_round.py @@ -8,4 +8,3 @@ import unittest class TestInterviewRound(unittest.TestCase): pass - diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py index 54ccfca38f7..14ebca432e5 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant.py @@ -13,7 +13,9 @@ from frappe.utils import validate_email_address from erpnext.hr.doctype.interview.interview import get_interviewers -class DuplicationError(frappe.ValidationError): pass +class DuplicationError(frappe.ValidationError): + pass + class JobApplicant(Document): def onload(self): @@ -36,8 +38,8 @@ class JobApplicant(Document): self.set_status_for_employee_referral() if not self.applicant_name and self.email_id: - guess = self.email_id.split('@')[0] - self.applicant_name = ' '.join([p.capitalize() for p in guess.split('.')]) + guess = self.email_id.split("@")[0] + self.applicant_name = " ".join([p.capitalize() for p in guess.split(".")]) def set_status_for_employee_referral(self): emp_ref = frappe.get_doc("Employee Referral", self.employee_referral) @@ -46,6 +48,7 @@ class JobApplicant(Document): elif self.status in ["Accepted", "Rejected"]: emp_ref.db_set("status", self.status) + @frappe.whitelist() def create_interview(doc, interview_round): import json @@ -59,7 +62,11 @@ def create_interview(doc, interview_round): round_designation = frappe.db.get_value("Interview Round", interview_round, "designation") if round_designation and doc.designation and round_designation != doc.designation: - frappe.throw(_("Interview Round {0} is only applicable for the Designation {1}").format(interview_round, round_designation)) + frappe.throw( + _("Interview Round {0} is only applicable for the Designation {1}").format( + interview_round, round_designation + ) + ) interview = frappe.new_doc("Interview") interview.interview_round = interview_round @@ -70,16 +77,16 @@ def create_interview(doc, interview_round): interviewer_detail = get_interviewers(interview_round) for d in interviewer_detail: - interview.append("interview_details", { - "interviewer": d.interviewer - }) + interview.append("interview_details", {"interviewer": d.interviewer}) return interview + @frappe.whitelist() def get_interview_details(job_applicant): - interview_details = frappe.db.get_all("Interview", - filters={"job_applicant":job_applicant, "docstatus": ["!=", 2]}, - fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"] + interview_details = frappe.db.get_all( + "Interview", + filters={"job_applicant": job_applicant, "docstatus": ["!=", 2]}, + fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"], ) interview_detail_map = {} diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py index 9406fc54855..14b944ac614 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py @@ -1,17 +1,9 @@ - - def get_data(): return { - 'fieldname': 'job_applicant', - 'transactions': [ - { - 'items': ['Employee', 'Employee Onboarding'] - }, - { - 'items': ['Job Offer', 'Appointment Letter'] - }, - { - 'items': ['Interview'] - } + "fieldname": "job_applicant", + "transactions": [ + {"items": ["Employee", "Employee Onboarding"]}, + {"items": ["Job Offer", "Appointment Letter"]}, + {"items": ["Interview"]}, ], } diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py index bf1622028d8..99d11619781 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py @@ -10,21 +10,25 @@ from erpnext.hr.doctype.designation.test_designation import create_designation class TestJobApplicant(unittest.TestCase): def test_job_applicant_naming(self): - applicant = frappe.get_doc({ - "doctype": "Job Applicant", - "status": "Open", - "applicant_name": "_Test Applicant", - "email_id": "job_applicant_naming@example.com" - }).insert() - self.assertEqual(applicant.name, 'job_applicant_naming@example.com') + applicant = frappe.get_doc( + { + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com", + } + ).insert() + self.assertEqual(applicant.name, "job_applicant_naming@example.com") - applicant = frappe.get_doc({ - "doctype": "Job Applicant", - "status": "Open", - "applicant_name": "_Test Applicant", - "email_id": "job_applicant_naming@example.com" - }).insert() - self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1') + applicant = frappe.get_doc( + { + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com", + } + ).insert() + self.assertEqual(applicant.name, "job_applicant_naming@example.com-1") def tearDown(self): frappe.db.rollback() @@ -41,11 +45,13 @@ def create_job_applicant(**args): if frappe.db.exists("Job Applicant", filters): return frappe.get_doc("Job Applicant", filters) - job_applicant = frappe.get_doc({ - "doctype": "Job Applicant", - "status": args.status or "Open", - "designation": create_designation().name - }) + job_applicant = frappe.get_doc( + { + "doctype": "Job Applicant", + "status": args.status or "Open", + "designation": create_designation().name, + } + ) job_applicant.update(filters) job_applicant.save() diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index 39f471929b4..dc76a2d0038 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -17,9 +17,15 @@ class JobOffer(Document): def validate(self): self.validate_vacancies() - job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant, "docstatus": ["!=", 2]}) + job_offer = frappe.db.exists( + "Job Offer", {"job_applicant": self.job_applicant, "docstatus": ["!=", 2]} + ) if job_offer and job_offer != self.name: - frappe.throw(_("Job Offer: {0} is already for Job Applicant: {1}").format(frappe.bold(job_offer), frappe.bold(self.job_applicant))) + frappe.throw( + _("Job Offer: {0} is already for Job Applicant: {1}").format( + frappe.bold(job_offer), frappe.bold(self.job_applicant) + ) + ) def validate_vacancies(self): staffing_plan = get_staffing_plan_detail(self.designation, self.company, self.offer_date) @@ -27,7 +33,7 @@ class JobOffer(Document): if staffing_plan and check_vacancies: job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0: - error_variable = 'for ' + frappe.bold(self.designation) + error_variable = "for " + frappe.bold(self.designation) if staffing_plan.get("parent"): error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) @@ -37,20 +43,27 @@ class JobOffer(Document): update_job_applicant(self.status, self.job_applicant) def get_job_offer(self, from_date, to_date): - ''' Returns job offer created during a time period ''' - return frappe.get_all("Job Offer", filters={ - "offer_date": ['between', (from_date, to_date)], + """Returns job offer created during a time period""" + return frappe.get_all( + "Job Offer", + filters={ + "offer_date": ["between", (from_date, to_date)], "designation": self.designation, "company": self.company, - "docstatus": 1 - }, fields=['name']) + "docstatus": 1, + }, + fields=["name"], + ) + def update_job_applicant(status, job_applicant): if status in ("Accepted", "Rejected"): frappe.set_value("Job Applicant", job_applicant, "status", status) + def get_staffing_plan_detail(designation, company, offer_date): - detail = frappe.db.sql(""" + detail = frappe.db.sql( + """ SELECT DISTINCT spd.parent, sp.from_date as from_date, sp.to_date as to_date, @@ -64,20 +77,33 @@ def get_staffing_plan_detail(designation, company, offer_date): AND sp.company=%s AND spd.parent = sp.name AND %s between sp.from_date and sp.to_date - """, (designation, company, offer_date), as_dict=1) + """, + (designation, company, offer_date), + as_dict=1, + ) return frappe._dict(detail[0]) if (detail and detail[0].parent) else None + @frappe.whitelist() def make_employee(source_name, target_doc=None): def set_missing_values(source, target): - target.personal_email, target.first_name = frappe.db.get_value("Job Applicant", \ - source.job_applicant, ["email_id", "applicant_name"]) - doc = get_mapped_doc("Job Offer", source_name, { + target.personal_email, target.first_name = frappe.db.get_value( + "Job Applicant", source.job_applicant, ["email_id", "applicant_name"] + ) + + doc = get_mapped_doc( + "Job Offer", + source_name, + { "Job Offer": { "doctype": "Employee", "field_map": { "applicant_name": "employee_name", - }} - }, target_doc, set_missing_values) + }, + } + }, + target_doc, + set_missing_values, + ) return doc diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py index d94e03ca63f..7d8ef115d16 100644 --- a/erpnext/hr/doctype/job_offer/test_job_offer.py +++ b/erpnext/hr/doctype/job_offer/test_job_offer.py @@ -12,17 +12,19 @@ from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company # test_records = frappe.get_test_records('Job Offer') + class TestJobOffer(unittest.TestCase): def test_job_offer_creation_against_vacancies(self): frappe.db.set_value("HR Settings", None, "check_vacancies", 1) job_applicant = create_job_applicant(email_id="test_job_offer@example.com") job_offer = create_job_offer(job_applicant=job_applicant.name, designation="UX Designer") - create_staffing_plan(name='Test No Vacancies', staffing_details=[{ - "designation": "UX Designer", - "vacancies": 0, - "estimated_cost_per_position": 5000 - }]) + create_staffing_plan( + name="Test No Vacancies", + staffing_details=[ + {"designation": "UX Designer", "vacancies": 0, "estimated_cost_per_position": 5000} + ], + ) self.assertRaises(frappe.ValidationError, job_offer.submit) # test creation of job offer when vacancies are not present @@ -49,6 +51,7 @@ class TestJobOffer(unittest.TestCase): def tearDown(self): frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1") + def create_job_offer(**args): args = frappe._dict(args) if not args.job_applicant: @@ -57,32 +60,34 @@ def create_job_offer(**args): if not frappe.db.exists("Designation", args.designation): designation = create_designation(designation_name=args.designation) - job_offer = frappe.get_doc({ - "doctype": "Job Offer", - "job_applicant": args.job_applicant or job_applicant.name, - "offer_date": args.offer_date or nowdate(), - "designation": args.designation or "Researcher", - "status": args.status or "Accepted" - }) + job_offer = frappe.get_doc( + { + "doctype": "Job Offer", + "job_applicant": args.job_applicant or job_applicant.name, + "offer_date": args.offer_date or nowdate(), + "designation": args.designation or "Researcher", + "status": args.status or "Accepted", + } + ) return job_offer + def create_staffing_plan(**args): args = frappe._dict(args) make_company() frappe.db.set_value("Company", "_Test Company", "is_group", 1) if frappe.db.exists("Staffing Plan", args.name or "Test"): return - staffing_plan = frappe.get_doc({ - "doctype": "Staffing Plan", - "name": args.name or "Test", - "from_date": args.from_date or nowdate(), - "to_date": args.to_date or add_days(nowdate(), 10), - "staffing_details": args.staffing_details or [{ - "designation": "Researcher", - "vacancies": 1, - "estimated_cost_per_position": 50000 - }] - }) + staffing_plan = frappe.get_doc( + { + "doctype": "Staffing Plan", + "name": args.name or "Test", + "from_date": args.from_date or nowdate(), + "to_date": args.to_date or add_days(nowdate(), 10), + "staffing_details": args.staffing_details + or [{"designation": "Researcher", "vacancies": 1, "estimated_cost_per_position": 50000}], + } + ) staffing_plan.insert() staffing_plan.submit() return staffing_plan diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index d53daf17d87..c71407d71d4 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -16,67 +16,78 @@ from erpnext.hr.doctype.staffing_plan.staffing_plan import ( class JobOpening(WebsiteGenerator): website = frappe._dict( - template = "templates/generators/job_opening.html", - condition_field = "publish", - page_title_field = "job_title", + template="templates/generators/job_opening.html", + condition_field="publish", + page_title_field="job_title", ) def validate(self): if not self.route: - self.route = frappe.scrub(self.job_title).replace('_', '-') + self.route = frappe.scrub(self.job_title).replace("_", "-") self.validate_current_vacancies() def validate_current_vacancies(self): if not self.staffing_plan: - staffing_plan = get_active_staffing_plan_details(self.company, - self.designation) + staffing_plan = get_active_staffing_plan_details(self.company, self.designation) if staffing_plan: self.staffing_plan = staffing_plan[0].name self.planned_vacancies = staffing_plan[0].vacancies elif not self.planned_vacancies: - planned_vacancies = frappe.db.sql(""" + planned_vacancies = frappe.db.sql( + """ select vacancies from `tabStaffing Plan Detail` - where parent=%s and designation=%s""", (self.staffing_plan, self.designation)) + where parent=%s and designation=%s""", + (self.staffing_plan, self.designation), + ) self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None if self.staffing_plan and self.planned_vacancies: staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company") - lft, rgt = frappe.get_cached_value('Company', staffing_plan_company, ["lft", "rgt"]) + lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"]) designation_counts = get_designation_counts(self.designation, self.company) - current_count = designation_counts['employee_count'] + designation_counts['job_openings'] + current_count = designation_counts["employee_count"] + designation_counts["job_openings"] if self.planned_vacancies <= current_count: - frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format( - self.designation, self.staffing_plan)) + frappe.throw( + _( + "Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}" + ).format(self.designation, self.staffing_plan) + ) def get_context(self, context): - context.parents = [{'route': 'jobs', 'title': _('All Jobs') }] + context.parents = [{"route": "jobs", "title": _("All Jobs")}] + def get_list_context(context): context.title = _("Jobs") - context.introduction = _('Current Job Openings') + context.introduction = _("Current Job Openings") context.get_list = get_job_openings -def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None): - fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range', - 'lower_range', 'upper_range', 'currency', 'job_application_route'] + +def get_job_openings( + doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None +): + fields = [ + "name", + "status", + "job_title", + "description", + "publish_salary_range", + "lower_range", + "upper_range", + "currency", + "job_application_route", + ] filters = filters or {} - filters.update({ - 'status': 'Open' - }) + filters.update({"status": "Open"}) if txt: - filters.update({ - 'job_title': ['like', '%{0}%'.format(txt)], - 'description': ['like', '%{0}%'.format(txt)] - }) + filters.update( + {"job_title": ["like", "%{0}%".format(txt)], "description": ["like", "%{0}%".format(txt)]} + ) - return frappe.get_all(doctype, - filters, - fields, - start=limit_start, - page_length=limit_page_length, - order_by=order_by + return frappe.get_all( + doctype, filters, fields, start=limit_start, page_length=limit_page_length, order_by=order_by ) diff --git a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py index 817969004f9..a30932870d0 100644 --- a/erpnext/hr/doctype/job_opening/job_opening_dashboard.py +++ b/erpnext/hr/doctype/job_opening/job_opening_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): - return { - 'fieldname': 'job_title', - 'transactions': [ - { - 'items': ['Job Applicant'] - } - ], - } + return { + "fieldname": "job_title", + "transactions": [{"items": ["Job Applicant"]}], + } diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.py b/erpnext/hr/doctype/job_opening/test_job_opening.py index a1c3a1d49e3..a72a6eb3384 100644 --- a/erpnext/hr/doctype/job_opening/test_job_opening.py +++ b/erpnext/hr/doctype/job_opening/test_job_opening.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Job Opening') + class TestJobOpening(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 232118fd67c..98408afab64 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -15,11 +15,25 @@ from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import ( from erpnext.hr.utils import get_leave_period, set_employee_name -class OverlapError(frappe.ValidationError): pass -class BackDatedAllocationError(frappe.ValidationError): pass -class OverAllocationError(frappe.ValidationError): pass -class LessAllocationError(frappe.ValidationError): pass -class ValueMultiplierError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + + +class BackDatedAllocationError(frappe.ValidationError): + pass + + +class OverAllocationError(frappe.ValidationError): + pass + + +class LessAllocationError(frappe.ValidationError): + pass + + +class ValueMultiplierError(frappe.ValidationError): + pass + class LeaveAllocation(Document): def validate(self): @@ -35,16 +49,22 @@ class LeaveAllocation(Document): def validate_leave_allocation_days(self): company = frappe.db.get_value("Employee", self.employee, "company") leave_period = get_leave_period(self.from_date, self.to_date, company) - max_leaves_allowed = flt(frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")) + max_leaves_allowed = flt( + frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") + ) if max_leaves_allowed > 0: leave_allocated = 0 if leave_period: - leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type, - leave_period[0].from_date, leave_period[0].to_date) + leave_allocated = get_leave_allocation_for_period( + self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date + ) leave_allocated += flt(self.new_leaves_allocated) if leave_allocated > max_leaves_allowed: - frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period") - .format(self.leave_type, self.employee)) + frappe.throw( + _( + "Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period" + ).format(self.leave_type, self.employee) + ) def on_submit(self): self.create_leave_ledger_entry() @@ -69,20 +89,22 @@ class LeaveAllocation(Document): "leaves": leaves_to_be_added, "from_date": self.from_date, "to_date": self.to_date, - "is_carry_forward": 0 + "is_carry_forward": 0, } create_leave_ledger_entry(self, args, True) def get_existing_leave_count(self): - ledger_entries = frappe.get_all("Leave Ledger Entry", - filters={ - "transaction_type": "Leave Allocation", - "transaction_name": self.name, - "employee": self.employee, - "company": self.company, - "leave_type": self.leave_type - }, - pluck="leaves") + ledger_entries = frappe.get_all( + "Leave Ledger Entry", + filters={ + "transaction_type": "Leave Allocation", + "transaction_name": self.name, + "employee": self.employee, + "company": self.company, + "leave_type": self.leave_type, + }, + pluck="leaves", + ) total_existing_leaves = 0 for entry in ledger_entries: total_existing_leaves += entry @@ -90,21 +112,33 @@ class LeaveAllocation(Document): return total_existing_leaves def validate_against_leave_applications(self): - leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, - self.from_date, self.to_date) + leaves_taken = get_approved_leaves_for_period( + self.employee, self.leave_type, self.from_date, self.to_date + ) if flt(leaves_taken) > flt(self.total_leaves_allocated): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) + frappe.msgprint( + _( + "Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period" + ).format(self.total_leaves_allocated, leaves_taken) + ) else: - frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) + frappe.throw( + _( + "Total allocated leaves {0} cannot be less than already approved leaves {1} for the period" + ).format(self.total_leaves_allocated, leaves_taken), + LessAllocationError, + ) def update_leave_policy_assignments_when_no_allocations_left(self): - allocations = frappe.db.get_list("Leave Allocation", filters = { - "docstatus": 1, - "leave_policy_assignment": self.leave_policy_assignment - }) + allocations = frappe.db.get_list( + "Leave Allocation", + filters={"docstatus": 1, "leave_policy_assignment": self.leave_policy_assignment}, + ) if len(allocations) == 0: - frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0) + frappe.db.set_value( + "Leave Policy Assignment", self.leave_policy_assignment, "leaves_allocated", 0 + ) def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: @@ -112,10 +146,13 @@ class LeaveAllocation(Document): def validate_lwp(self): if frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"): - frappe.throw(_("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type)) + frappe.throw( + _("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type) + ) def validate_allocation_overlap(self): - leave_allocation = frappe.db.sql(""" + leave_allocation = frappe.db.sql( + """ SELECT name FROM `tabLeave Allocation` @@ -123,29 +160,44 @@ class LeaveAllocation(Document): employee=%s AND leave_type=%s AND name <> %s AND docstatus=1 AND to_date >= %s AND from_date <= %s""", - (self.employee, self.leave_type, self.name, self.from_date, self.to_date)) + (self.employee, self.leave_type, self.name, self.from_date, self.to_date), + ) if leave_allocation: - frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}") - .format(self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date))) + frappe.msgprint( + _("{0} already allocated for Employee {1} for period {2} to {3}").format( + self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date) + ) + ) - frappe.throw(_('Reference') + ': {0}' - .format(leave_allocation[0][0]), OverlapError) + frappe.throw( + _("Reference") + + ': {0}'.format(leave_allocation[0][0]), + OverlapError, + ) def validate_back_dated_allocation(self): - future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` + future_allocation = frappe.db.sql( + """select name, from_date from `tabLeave Allocation` where employee=%s and leave_type=%s and docstatus=1 and from_date > %s - and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) + and carry_forward=1""", + (self.employee, self.leave_type, self.to_date), + as_dict=1, + ) if future_allocation: - frappe.throw(_("Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") - .format(formatdate(future_allocation[0].from_date), future_allocation[0].name), - BackDatedAllocationError) + frappe.throw( + _( + "Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}" + ).format(formatdate(future_allocation[0].from_date), future_allocation[0].name), + BackDatedAllocationError, + ) @frappe.whitelist() def set_total_leaves_allocated(self): - self.unused_leaves = get_carry_forwarded_leaves(self.employee, - self.leave_type, self.from_date, self.carry_forward) + self.unused_leaves = get_carry_forwarded_leaves( + self.employee, self.leave_type, self.from_date, self.carry_forward + ) self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated) @@ -154,11 +206,14 @@ class LeaveAllocation(Document): if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation() - if not self.total_leaves_allocated \ - and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") \ - and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory"): - frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}") - .format(self.leave_type)) + if ( + not self.total_leaves_allocated + and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") + and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory") + ): + frappe.throw( + _("Total leaves allocated is mandatory for Leave Type {0}").format(self.leave_type) + ) def limit_carry_forward_based_on_max_allowed_leaves(self): max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") @@ -167,13 +222,17 @@ class LeaveAllocation(Document): self.unused_leaves = max_leaves_allowed - flt(self.new_leaves_allocated) def set_carry_forwarded_leaves_in_previous_allocation(self, on_cancel=False): - ''' Set carry forwarded leaves in previous allocation ''' + """Set carry forwarded leaves in previous allocation""" previous_allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee) if on_cancel: self.unused_leaves = 0.0 if previous_allocation: - frappe.db.set_value("Leave Allocation", previous_allocation.name, - 'carry_forwarded_leaves_count', self.unused_leaves) + frappe.db.set_value( + "Leave Allocation", + previous_allocation.name, + "carry_forwarded_leaves_count", + self.unused_leaves, + ) def validate_total_leaves_allocated(self): # Adding a day to include To Date in the difference @@ -183,13 +242,15 @@ class LeaveAllocation(Document): def create_leave_ledger_entry(self, submit=True): if self.unused_leaves: - expiry_days = frappe.db.get_value("Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days") + expiry_days = frappe.db.get_value( + "Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days" + ) end_date = add_days(self.from_date, expiry_days - 1) if expiry_days else self.to_date args = dict( leaves=self.unused_leaves, from_date=self.from_date, - to_date= min(getdate(end_date), getdate(self.to_date)), - is_carry_forward=1 + to_date=min(getdate(end_date), getdate(self.to_date)), + is_carry_forward=1, ) create_leave_ledger_entry(self, args, submit) @@ -197,25 +258,31 @@ class LeaveAllocation(Document): leaves=self.new_leaves_allocated, from_date=self.from_date, to_date=self.to_date, - is_carry_forward=0 + is_carry_forward=0, ) create_leave_ledger_entry(self, args, submit) + def get_previous_allocation(from_date, leave_type, employee): - ''' Returns document properties of previous allocation ''' - return frappe.db.get_value("Leave Allocation", + """Returns document properties of previous allocation""" + return frappe.db.get_value( + "Leave Allocation", filters={ - 'to_date': ("<", from_date), - 'leave_type': leave_type, - 'employee': employee, - 'docstatus': 1 + "to_date": ("<", from_date), + "leave_type": leave_type, + "employee": employee, + "docstatus": 1, }, - order_by='to_date DESC', - fieldname=['name', 'from_date', 'to_date', 'employee', 'leave_type'], as_dict=1) + order_by="to_date DESC", + fieldname=["name", "from_date", "to_date", "employee", "leave_type"], + as_dict=1, + ) + def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): leave_allocated = 0 - leave_allocations = frappe.db.sql(""" + leave_allocations = frappe.db.sql( + """ select employee, leave_type, from_date, to_date, total_leaves_allocated from `tabLeave Allocation` where employee=%(employee)s and leave_type=%(leave_type)s @@ -223,12 +290,10 @@ def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + """, + {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, + as_dict=1, + ) if leave_allocations: for leave_alloc in leave_allocations: @@ -236,35 +301,42 @@ def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): return leave_allocated + @frappe.whitelist() def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): - ''' Returns carry forwarded leaves for the given employee ''' + """Returns carry forwarded leaves for the given employee""" unused_leaves = 0.0 previous_allocation = get_previous_allocation(date, leave_type, employee) if carry_forward and previous_allocation: validate_carry_forward(leave_type) - unused_leaves = get_unused_leaves(employee, leave_type, - previous_allocation.from_date, previous_allocation.to_date) + unused_leaves = get_unused_leaves( + employee, leave_type, previous_allocation.from_date, previous_allocation.to_date + ) if unused_leaves: - max_carry_forwarded_leaves = frappe.db.get_value("Leave Type", - leave_type, "maximum_carry_forwarded_leaves") + max_carry_forwarded_leaves = frappe.db.get_value( + "Leave Type", leave_type, "maximum_carry_forwarded_leaves" + ) if max_carry_forwarded_leaves and unused_leaves > flt(max_carry_forwarded_leaves): unused_leaves = flt(max_carry_forwarded_leaves) return unused_leaves + def get_unused_leaves(employee, leave_type, from_date, to_date): - ''' Returns unused leaves between the given period while skipping leave allocation expiry ''' - leaves = frappe.get_all("Leave Ledger Entry", filters={ - 'employee': employee, - 'leave_type': leave_type, - 'from_date': ('>=', from_date), - 'to_date': ('<=', to_date) - }, or_filters={ - 'is_expired': 0, - 'is_carry_forward': 1 - }, fields=['sum(leaves) as leaves']) - return flt(leaves[0]['leaves']) + """Returns unused leaves between the given period while skipping leave allocation expiry""" + leaves = frappe.get_all( + "Leave Ledger Entry", + filters={ + "employee": employee, + "leave_type": leave_type, + "from_date": (">=", from_date), + "to_date": ("<=", to_date), + }, + or_filters={"is_expired": 0, "is_carry_forward": 1}, + fields=["sum(leaves) as leaves"], + ) + return flt(leaves[0]["leaves"]) + def validate_carry_forward(leave_type): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py index 08861b8bce3..96e81db617c 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py @@ -1,19 +1,6 @@ - - def get_data(): - return { - 'fieldname': 'leave_allocation', - 'transactions': [ - { - 'items': ['Compensatory Leave Request'] - }, - { - 'items': ['Leave Encashment'] - } - ], - 'reports': [ - { - 'items': ['Employee Leave Balance'] - } - ] - } + return { + "fieldname": "leave_allocation", + "transactions": [{"items": ["Compensatory Leave Request"]}, {"items": ["Leave Encashment"]}], + "reports": [{"items": ["Employee Leave Balance"]}], + } diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 1310ca65ecf..a53d4a82ba6 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -32,7 +31,7 @@ class TestLeaveAllocation(unittest.TestCase): "from_date": getdate("2015-10-01"), "to_date": getdate("2015-10-31"), "new_leaves_allocated": 5, - "docstatus": 1 + "docstatus": 1, }, { "doctype": "Leave Allocation", @@ -42,39 +41,43 @@ class TestLeaveAllocation(unittest.TestCase): "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-01"), "to_date": getdate("2015-11-30"), - "new_leaves_allocated": 5 - } + "new_leaves_allocated": 5, + }, ] frappe.get_doc(leaves[0]).save() self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save) def test_invalid_period(self): - doc = frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": self.employee.name, - "employee_name": self.employee.employee_name, - "leave_type": "_Test Leave Type", - "from_date": getdate("2015-09-30"), - "to_date": getdate("2015-09-1"), - "new_leaves_allocated": 5 - }) + doc = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, + "leave_type": "_Test Leave Type", + "from_date": getdate("2015-09-30"), + "to_date": getdate("2015-09-1"), + "new_leaves_allocated": 5, + } + ) # invalid period self.assertRaises(frappe.ValidationError, doc.save) def test_allocated_leave_days_over_period(self): - doc = frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": self.employee.name, - "employee_name": self.employee.employee_name, - "leave_type": "_Test Leave Type", - "from_date": getdate("2015-09-1"), - "to_date": getdate("2015-09-30"), - "new_leaves_allocated": 35 - }) + doc = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, + "leave_type": "_Test Leave Type", + "from_date": getdate("2015-09-1"), + "to_date": getdate("2015-09-30"), + "new_leaves_allocated": 35, + } + ) # allocated leave more than period self.assertRaises(frappe.ValidationError, doc.save) @@ -92,7 +95,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave", from_date=add_months(nowdate(), -12), to_date=add_months(nowdate(), -1), - carry_forward=0) + carry_forward=0, + ) leave_allocation.submit() # carry forwarded leaves considering maximum_carry_forwarded_leaves @@ -101,7 +105,8 @@ class TestLeaveAllocation(unittest.TestCase): employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="_Test_CF_leave", - carry_forward=1) + carry_forward=1, + ) leave_allocation_1.submit() self.assertEqual(leave_allocation_1.unused_leaves, 10) @@ -115,7 +120,8 @@ class TestLeaveAllocation(unittest.TestCase): employee_name=self.employee.employee_name, leave_type="_Test_CF_leave", carry_forward=1, - new_leaves_allocated=25) + new_leaves_allocated=25, + ) leave_allocation_2.submit() self.assertEqual(leave_allocation_2.unused_leaves, 5) @@ -124,7 +130,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + ) leave_type.save() # initial leave allocation @@ -134,7 +141,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave_expiry", from_date=add_months(nowdate(), -24), to_date=add_months(nowdate(), -12), - carry_forward=0) + carry_forward=0, + ) leave_allocation.submit() leave_allocation = create_leave_allocation( @@ -143,7 +151,8 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave_expiry", from_date=add_days(nowdate(), -90), to_date=add_days(nowdate(), 100), - carry_forward=1) + carry_forward=1, + ) leave_allocation.submit() # expires all the carry forwarded leaves after 90 days @@ -156,19 +165,21 @@ class TestLeaveAllocation(unittest.TestCase): leave_type="_Test_CF_leave_expiry", carry_forward=1, from_date=add_months(nowdate(), 6), - to_date=add_months(nowdate(), 12)) + to_date=add_months(nowdate(), 12), + ) leave_allocation_1.submit() self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated) def test_creation_of_leave_ledger_entry_on_submit(self): leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_allocation.name) + ) self.assertEqual(len(leave_ledger_entry), 1) self.assertEqual(leave_ledger_entry[0].employee, leave_allocation.employee) @@ -177,12 +188,13 @@ class TestLeaveAllocation(unittest.TestCase): # check if leave ledger entry is deleted on cancellation leave_allocation.cancel() - self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) + self.assertFalse( + frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_allocation.name}) + ) def test_leave_addition_after_submit(self): leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) @@ -192,8 +204,7 @@ class TestLeaveAllocation(unittest.TestCase): def test_leave_subtraction_after_submit(self): leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) @@ -205,26 +216,29 @@ class TestLeaveAllocation(unittest.TestCase): from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list make_holiday_list() - frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List" + ) leave_allocation = create_leave_allocation( - employee=self.employee.name, - employee_name=self.employee.employee_name + employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() self.assertTrue(leave_allocation.total_leaves_allocated, 15) - leave_application = frappe.get_doc({ - "doctype": 'Leave Application', - "employee": self.employee.name, - "leave_type": "_Test Leave Type", - "from_date": add_months(nowdate(), 2), - "to_date": add_months(add_days(nowdate(), 10), 2), - "company": self.employee.company, - "docstatus": 1, - "status": "Approved", - "leave_approver": 'test@example.com' - }) + leave_application = frappe.get_doc( + { + "doctype": "Leave Application", + "employee": self.employee.name, + "leave_type": "_Test Leave Type", + "from_date": add_months(nowdate(), 2), + "to_date": add_months(add_days(nowdate(), 10), 2), + "company": self.employee.company, + "docstatus": 1, + "status": "Approved", + "leave_approver": "test@example.com", + } + ) leave_application.submit() leave_application.reload() @@ -233,22 +247,26 @@ class TestLeaveAllocation(unittest.TestCase): leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1 self.assertRaises(frappe.ValidationError, leave_allocation.submit) + def create_leave_allocation(**args): args = frappe._dict(args) emp_id = make_employee("test_emp_leave_allocation@salary.com") employee = frappe.get_doc("Employee", emp_id) - return frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": args.employee or employee.name, - "employee_name": args.employee_name or employee.employee_name, - "leave_type": args.leave_type or "_Test Leave Type", - "from_date": args.from_date or nowdate(), - "new_leaves_allocated": args.new_leaves_allocated or 15, - "carry_forward": args.carry_forward or 0, - "to_date": args.to_date or add_months(nowdate(), 12) - }) + return frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": args.employee or employee.name, + "employee_name": args.employee_name or employee.employee_name, + "leave_type": args.leave_type or "_Test Leave Type", + "from_date": args.from_date or nowdate(), + "new_leaves_allocated": args.new_leaves_allocated or 15, + "carry_forward": args.carry_forward or 0, + "to_date": args.to_date or add_months(nowdate(), 12), + } + ) + test_dependencies = ["Employee", "Leave Type"] diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 518d79aa34b..9036727f76f 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -32,15 +32,30 @@ from erpnext.hr.utils import ( ) -class LeaveDayBlockedError(frappe.ValidationError): pass -class OverlapError(frappe.ValidationError): pass -class AttendanceAlreadyMarkedError(frappe.ValidationError): pass -class NotAnOptionalHoliday(frappe.ValidationError): pass +class LeaveDayBlockedError(frappe.ValidationError): + pass + + +class OverlapError(frappe.ValidationError): + pass + + +class AttendanceAlreadyMarkedError(frappe.ValidationError): + pass + + +class NotAnOptionalHoliday(frappe.ValidationError): + pass + + class InsufficientLeaveBalanceError(frappe.ValidationError): pass + + class LeaveAcrossAllocationsError(frappe.ValidationError): pass + from frappe.model.document import Document @@ -60,7 +75,7 @@ class LeaveApplication(Document): self.validate_salary_processed_days() self.validate_attendance() self.set_half_day_date() - if frappe.db.get_value("Leave Type", self.leave_type, 'is_optional_leave'): + if frappe.db.get_value("Leave Type", self.leave_type, "is_optional_leave"): self.validate_optional_leave() self.validate_applicable_after() @@ -74,7 +89,9 @@ class LeaveApplication(Document): def on_submit(self): if self.status == "Open": - frappe.throw(_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")) + frappe.throw( + _("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted") + ) self.validate_back_dated_application() self.update_attendance() @@ -101,7 +118,9 @@ class LeaveApplication(Document): leave_type = frappe.get_doc("Leave Type", self.leave_type) if leave_type.applicable_after > 0: date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") - leave_days = get_approved_leaves_for_period(self.employee, False, date_of_joining, self.from_date) + leave_days = get_approved_leaves_for_period( + self.employee, False, date_of_joining, self.from_date + ) number_of_days = date_diff(getdate(self.from_date), date_of_joining) if number_of_days >= 0: holidays = 0 @@ -109,29 +128,48 @@ class LeaveApplication(Document): holidays = get_holidays(self.employee, date_of_joining, self.from_date) number_of_days = number_of_days - leave_days - holidays if number_of_days < leave_type.applicable_after: - frappe.throw(_("{0} applicable after {1} working days").format(self.leave_type, leave_type.applicable_after)) + frappe.throw( + _("{0} applicable after {1} working days").format( + self.leave_type, leave_type.applicable_after + ) + ) def validate_dates(self): if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"): if self.from_date and getdate(self.from_date) < getdate(): - allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application") + allowed_role = frappe.db.get_single_value( + "HR Settings", "role_allowed_to_create_backdated_leave_application" + ) user = frappe.get_doc("User", frappe.session.user) user_roles = [d.role for d in user.roles] if not allowed_role: - frappe.throw(_("Backdated Leave Application is restricted. Please set the {} in {}").format( - frappe.bold("Role Allowed to Create Backdated Leave Application"), get_link_to_form("HR Settings", "HR Settings"))) + frappe.throw( + _("Backdated Leave Application is restricted. Please set the {} in {}").format( + frappe.bold("Role Allowed to Create Backdated Leave Application"), + get_link_to_form("HR Settings", "HR Settings"), + ) + ) - if (allowed_role and allowed_role not in user_roles): - frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role)) + if allowed_role and allowed_role not in user_roles: + frappe.throw( + _("Only users with the {0} role can create backdated leave applications").format( + allowed_role + ) + ) if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)): frappe.throw(_("To date cannot be before from date")) - if self.half_day and self.half_day_date \ - and (getdate(self.half_day_date) < getdate(self.from_date) - or getdate(self.half_day_date) > getdate(self.to_date)): + if ( + self.half_day + and self.half_day_date + and ( + getdate(self.half_day_date) < getdate(self.from_date) + or getdate(self.half_day_date) > getdate(self.to_date) + ) + ): - frappe.throw(_("Half Day Date should be between From Date and To Date")) + frappe.throw(_("Half Day Date should be between From Date and To Date")) if not is_lwp(self.leave_type): self.validate_dates_across_allocation() @@ -146,10 +184,14 @@ class LeaveApplication(Document): if not (alloc_on_from_date or alloc_on_to_date): frappe.throw(_("Application period cannot be outside leave allocation period")) elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): - frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError) + frappe.throw( + _("Application period cannot be across two allocation records"), + exc=LeaveAcrossAllocationsError, + ) def get_allocation_based_on_application_dates(self) -> Tuple[Dict, Dict]: """Returns allocation name, from and to dates for application dates""" + def _get_leave_allocation_record(date): LeaveAllocation = frappe.qb.DocType("Leave Allocation") allocation = ( @@ -171,13 +213,20 @@ class LeaveApplication(Document): return allocation_based_on_from_date, allocation_based_on_to_date def validate_back_dated_application(self): - future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` + future_allocation = frappe.db.sql( + """select name, from_date from `tabLeave Allocation` where employee=%s and leave_type=%s and docstatus=1 and from_date > %s - and carry_forward=1""", (self.employee, self.leave_type, self.to_date), as_dict=1) + and carry_forward=1""", + (self.employee, self.leave_type, self.to_date), + as_dict=1, + ) if future_allocation: - frappe.throw(_("Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}") - .format(formatdate(future_allocation[0].from_date), future_allocation[0].name)) + frappe.throw( + _( + "Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}" + ).format(formatdate(future_allocation[0].from_date), future_allocation[0].name) + ) def update_attendance(self): if self.status != "Approved": @@ -189,8 +238,9 @@ class LeaveApplication(Document): for dt in daterange(getdate(self.from_date), getdate(self.to_date)): date = dt.strftime("%Y-%m-%d") - attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee, - attendance_date = date, docstatus = ('!=', 2))) + attendance_name = frappe.db.exists( + "Attendance", dict(employee=self.employee, attendance_date=date, docstatus=("!=", 2)) + ) # don't mark attendance for holidays # if leave type does not include holidays within leaves as leaves @@ -207,17 +257,17 @@ class LeaveApplication(Document): self.create_or_update_attendance(attendance_name, date) def create_or_update_attendance(self, attendance_name, date): - status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave" + status = ( + "Half Day" + if self.half_day_date and getdate(date) == getdate(self.half_day_date) + else "On Leave" + ) if attendance_name: # update existing attendance, change absent to on leave - doc = frappe.get_doc('Attendance', attendance_name) + doc = frappe.get_doc("Attendance", attendance_name) if doc.status != status: - doc.db_set({ - 'status': status, - 'leave_type': self.leave_type, - 'leave_application': self.name - }) + doc.db_set({"status": status, "leave_type": self.leave_type, "leave_application": self.name}) else: # make new attendance and submit it doc = frappe.new_doc("Attendance") @@ -234,8 +284,12 @@ class LeaveApplication(Document): def cancel_attendance(self): if self.docstatus == 2: - attendance = frappe.db.sql("""select name from `tabAttendance` where employee = %s\ - and (attendance_date between %s and %s) and docstatus < 2 and status in ('On Leave', 'Half Day')""",(self.employee, self.from_date, self.to_date), as_dict=1) + attendance = frappe.db.sql( + """select name from `tabAttendance` where employee = %s\ + and (attendance_date between %s and %s) and docstatus < 2 and status in ('On Leave', 'Half Day')""", + (self.employee, self.from_date, self.to_date), + as_dict=1, + ) for name in attendance: frappe.db.set_value("Attendance", name, "docstatus", 2) @@ -243,21 +297,29 @@ class LeaveApplication(Document): if not frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"): return - last_processed_pay_slip = frappe.db.sql(""" + last_processed_pay_slip = frappe.db.sql( + """ select start_date, end_date from `tabSalary Slip` where docstatus = 1 and employee = %s and ((%s between start_date and end_date) or (%s between start_date and end_date)) order by modified desc limit 1 - """,(self.employee, self.to_date, self.from_date)) + """, + (self.employee, self.to_date, self.from_date), + ) if last_processed_pay_slip: - frappe.throw(_("Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range.").format(formatdate(last_processed_pay_slip[0][0]), - formatdate(last_processed_pay_slip[0][1]))) - + frappe.throw( + _( + "Salary already processed for period between {0} and {1}, Leave application period cannot be between this date range." + ).format( + formatdate(last_processed_pay_slip[0][0]), formatdate(last_processed_pay_slip[0][1]) + ) + ) def show_block_day_warning(self): - block_dates = get_applicable_block_dates(self.from_date, self.to_date, - self.employee, self.company, all_lists=True) + block_dates = get_applicable_block_dates( + self.from_date, self.to_date, self.employee, self.company, all_lists=True + ) if block_dates: frappe.msgprint(_("Warning: Leave application contains following block dates") + ":") @@ -265,27 +327,41 @@ class LeaveApplication(Document): frappe.msgprint(formatdate(d.block_date) + ": " + d.reason) def validate_block_days(self): - block_dates = get_applicable_block_dates(self.from_date, self.to_date, - self.employee, self.company) + block_dates = get_applicable_block_dates( + self.from_date, self.to_date, self.employee, self.company + ) if block_dates and self.status == "Approved": frappe.throw(_("You are not authorized to approve leaves on Block Dates"), LeaveDayBlockedError) def validate_balance_leaves(self): if self.from_date and self.to_date: - self.total_leave_days = get_number_of_leave_days(self.employee, self.leave_type, - self.from_date, self.to_date, self.half_day, self.half_day_date) + self.total_leave_days = get_number_of_leave_days( + self.employee, self.leave_type, self.from_date, self.to_date, self.half_day, self.half_day_date + ) if self.total_leave_days <= 0: - frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave.")) + frappe.throw( + _( + "The day(s) on which you are applying for leave are holidays. You need not apply for leave." + ) + ) if not is_lwp(self.leave_type): - leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date, - consider_all_leaves_in_the_allocation_period=True, for_consumption=True) + leave_balance = get_leave_balance_on( + self.employee, + self.leave_type, + self.from_date, + self.to_date, + consider_all_leaves_in_the_allocation_period=True, + for_consumption=True, + ) self.leave_balance = leave_balance.get("leave_balance") leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption") - if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption): + if self.status != "Rejected" and ( + leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption + ): self.show_insufficient_balance_message(leave_balance_for_consumption) def show_insufficient_balance_message(self, leave_balance_for_consumption: float) -> None: @@ -293,39 +369,57 @@ class LeaveApplication(Document): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): if leave_balance_for_consumption != self.leave_balance: - msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type)) + msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format( + frappe.bold(self.leave_type) + ) msg += "

    " - msg += _("Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation.") + msg += _( + "Actual balances aren't available because the leave application spans over different leave allocations. You can still apply for leaves which would be compensated during the next allocation." + ) else: - msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type)) + msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format( + frappe.bold(self.leave_type) + ) frappe.msgprint(msg, title=_("Warning"), indicator="orange") else: - frappe.throw(_("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)), - exc=InsufficientLeaveBalanceError, title=_("Insufficient Balance")) + frappe.throw( + _("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)), + exc=InsufficientLeaveBalanceError, + title=_("Insufficient Balance"), + ) def validate_leave_overlap(self): if not self.name: # hack! if name is null, it could cause problems with != self.name = "New Leave Application" - for d in frappe.db.sql(""" + for d in frappe.db.sql( + """ select name, leave_type, posting_date, from_date, to_date, total_leave_days, half_day_date from `tabLeave Application` where employee = %(employee)s and docstatus < 2 and status in ("Open", "Approved") and to_date >= %(from_date)s and from_date <= %(to_date)s - and name != %(name)s""", { + and name != %(name)s""", + { "employee": self.employee, "from_date": self.from_date, "to_date": self.to_date, - "name": self.name - }, as_dict = 1): + "name": self.name, + }, + as_dict=1, + ): - if cint(self.half_day)==1 and getdate(self.half_day_date) == getdate(d.half_day_date) and ( - flt(self.total_leave_days)==0.5 - or getdate(self.from_date) == getdate(d.to_date) - or getdate(self.to_date) == getdate(d.from_date)): + if ( + cint(self.half_day) == 1 + and getdate(self.half_day_date) == getdate(d.half_day_date) + and ( + flt(self.total_leave_days) == 0.5 + or getdate(self.from_date) == getdate(d.to_date) + or getdate(self.to_date) == getdate(d.from_date) + ) + ): total_leaves_on_half_day = self.get_total_leaves_on_half_day() if total_leaves_on_half_day >= 1: @@ -335,22 +429,22 @@ class LeaveApplication(Document): def throw_overlap_error(self, d): form_link = get_link_to_form("Leave Application", d.name) - msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(self.employee, - d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date']), form_link) + msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format( + self.employee, d["leave_type"], formatdate(d["from_date"]), formatdate(d["to_date"]), form_link + ) frappe.throw(msg, OverlapError) def get_total_leaves_on_half_day(self): - leave_count_on_half_day_date = frappe.db.sql("""select count(name) from `tabLeave Application` + leave_count_on_half_day_date = frappe.db.sql( + """select count(name) from `tabLeave Application` where employee = %(employee)s and docstatus < 2 and status in ("Open", "Approved") and half_day = 1 and half_day_date = %(half_day_date)s - and name != %(name)s""", { - "employee": self.employee, - "half_day_date": self.half_day_date, - "name": self.name - })[0][0] + and name != %(name)s""", + {"employee": self.employee, "half_day_date": self.half_day_date, "name": self.name}, + )[0][0] return leave_count_on_half_day_date * 0.5 @@ -360,24 +454,36 @@ class LeaveApplication(Document): frappe.throw(_("Leave of type {0} cannot be longer than {1}").format(self.leave_type, max_days)) def validate_attendance(self): - attendance = frappe.db.sql("""select name from `tabAttendance` where employee = %s and (attendance_date between %s and %s) + attendance = frappe.db.sql( + """select name from `tabAttendance` where employee = %s and (attendance_date between %s and %s) and status = "Present" and docstatus = 1""", - (self.employee, self.from_date, self.to_date)) + (self.employee, self.from_date, self.to_date), + ) if attendance: - frappe.throw(_("Attendance for employee {0} is already marked for this day").format(self.employee), - AttendanceAlreadyMarkedError) + frappe.throw( + _("Attendance for employee {0} is already marked for this day").format(self.employee), + AttendanceAlreadyMarkedError, + ) def validate_optional_leave(self): leave_period = get_leave_period(self.from_date, self.to_date, self.company) if not leave_period: frappe.throw(_("Cannot find active Leave Period")) - optional_holiday_list = frappe.db.get_value("Leave Period", leave_period[0]["name"], "optional_holiday_list") + optional_holiday_list = frappe.db.get_value( + "Leave Period", leave_period[0]["name"], "optional_holiday_list" + ) if not optional_holiday_list: - frappe.throw(_("Optional Holiday List not set for leave period {0}").format(leave_period[0]["name"])) + frappe.throw( + _("Optional Holiday List not set for leave period {0}").format(leave_period[0]["name"]) + ) day = getdate(self.from_date) while day <= getdate(self.to_date): - if not frappe.db.exists({"doctype": "Holiday", "parent": optional_holiday_list, "holiday_date": day}): - frappe.throw(_("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday) + if not frappe.db.exists( + {"doctype": "Holiday", "parent": optional_holiday_list, "holiday_date": day} + ): + frappe.throw( + _("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday + ) day = add_days(day, 1) def set_half_day_date(self): @@ -392,44 +498,50 @@ class LeaveApplication(Document): if not employee.user_id: return - parent_doc = frappe.get_doc('Leave Application', self.name) + parent_doc = frappe.get_doc("Leave Application", self.name) args = parent_doc.as_dict() - template = frappe.db.get_single_value('HR Settings', 'leave_status_notification_template') + template = frappe.db.get_single_value("HR Settings", "leave_status_notification_template") if not template: frappe.msgprint(_("Please set default template for Leave Status Notification in HR Settings.")) return email_template = frappe.get_doc("Email Template", template) message = frappe.render_template(email_template.response, args) - self.notify({ - # for post in messages - "message": message, - "message_to": employee.user_id, - # for email - "subject": email_template.subject, - "notify": "employee" - }) + self.notify( + { + # for post in messages + "message": message, + "message_to": employee.user_id, + # for email + "subject": email_template.subject, + "notify": "employee", + } + ) def notify_leave_approver(self): if self.leave_approver: - parent_doc = frappe.get_doc('Leave Application', self.name) + parent_doc = frappe.get_doc("Leave Application", self.name) args = parent_doc.as_dict() - template = frappe.db.get_single_value('HR Settings', 'leave_approval_notification_template') + template = frappe.db.get_single_value("HR Settings", "leave_approval_notification_template") if not template: - frappe.msgprint(_("Please set default template for Leave Approval Notification in HR Settings.")) + frappe.msgprint( + _("Please set default template for Leave Approval Notification in HR Settings.") + ) return email_template = frappe.get_doc("Email Template", template) message = frappe.render_template(email_template.response, args) - self.notify({ - # for post in messages - "message": message, - "message_to": self.leave_approver, - # for email - "subject": email_template.subject - }) + self.notify( + { + # for post in messages + "message": message, + "message_to": self.leave_approver, + # for email + "subject": email_template.subject, + } + ) def notify(self, args): args = frappe._dict(args) @@ -438,29 +550,30 @@ class LeaveApplication(Document): contact = args.message_to if not isinstance(contact, list): if not args.notify == "employee": - contact = frappe.get_doc('User', contact).email or contact + contact = frappe.get_doc("User", contact).email or contact - sender = dict() - sender['email'] = frappe.get_doc('User', frappe.session.user).email - sender['full_name'] = get_fullname(sender['email']) + sender = dict() + sender["email"] = frappe.get_doc("User", frappe.session.user).email + sender["full_name"] = get_fullname(sender["email"]) try: frappe.sendmail( - recipients = contact, - sender = sender['email'], - subject = args.subject, - message = args.message, + recipients=contact, + sender=sender["email"], + subject=args.subject, + message=args.message, ) frappe.msgprint(_("Email sent to {0}").format(contact)) except frappe.OutgoingEmailError: pass def create_leave_ledger_entry(self, submit=True): - if self.status != 'Approved' and submit: + if self.status != "Approved" and submit: return - expiry_date = get_allocation_expiry_for_cf_leaves(self.employee, self.leave_type, - self.to_date, self.from_date) + expiry_date = get_allocation_expiry_for_cf_leaves( + self.employee, self.leave_type, self.to_date, self.from_date + ) lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp") if expiry_date: @@ -478,24 +591,42 @@ class LeaveApplication(Document): from_date=self.from_date, to_date=self.to_date, is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) + or "", ) create_leave_ledger_entry(self, args, submit) - def is_separate_ledger_entry_required(self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None) -> bool: + def is_separate_ledger_entry_required( + self, alloc_on_from_date: Optional[Dict] = None, alloc_on_to_date: Optional[Dict] = None + ) -> bool: """Checks if application dates fall in separate allocations""" - if ((alloc_on_from_date and not alloc_on_to_date) + if ( + (alloc_on_from_date and not alloc_on_to_date) or (not alloc_on_from_date and alloc_on_to_date) - or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)): + or ( + alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name + ) + ): return True return False def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp): """Creates separate ledger entries for application period falling into separate allocations""" # for creating separate ledger entries existing allocation periods should be consecutive - if submit and alloc_on_from_date and alloc_on_to_date and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date: - frappe.throw(_("Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}.").format( - get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date))) + if ( + submit + and alloc_on_from_date + and alloc_on_to_date + and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date + ): + frappe.throw( + _( + "Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}." + ).format( + get_link_to_form("Leave Allocation", alloc_on_from_date.name), + get_link_to_form("Leave Allocation", alloc_on_to_date), + ) + ) raise_exception = False if frappe.flags.in_patch else True @@ -506,38 +637,48 @@ class LeaveApplication(Document): first_alloc_end = add_days(alloc_on_to_date.from_date, -1) second_alloc_start = alloc_on_to_date.from_date - leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type, - self.from_date, first_alloc_end, self.half_day, self.half_day_date) - leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type, - second_alloc_start, self.to_date, self.half_day, self.half_day_date) + leaves_in_first_alloc = get_number_of_leave_days( + self.employee, + self.leave_type, + self.from_date, + first_alloc_end, + self.half_day, + self.half_day_date, + ) + leaves_in_second_alloc = get_number_of_leave_days( + self.employee, + self.leave_type, + second_alloc_start, + self.to_date, + self.half_day, + self.half_day_date, + ) args = dict( is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) + or "", ) if leaves_in_first_alloc: - args.update(dict( - from_date=self.from_date, - to_date=first_alloc_end, - leaves=leaves_in_first_alloc * -1 - )) + args.update( + dict(from_date=self.from_date, to_date=first_alloc_end, leaves=leaves_in_first_alloc * -1) + ) create_leave_ledger_entry(self, args, submit) if leaves_in_second_alloc: - args.update(dict( - from_date=second_alloc_start, - to_date=self.to_date, - leaves=leaves_in_second_alloc * -1 - )) + args.update( + dict(from_date=second_alloc_start, to_date=self.to_date, leaves=leaves_in_second_alloc * -1) + ) create_leave_ledger_entry(self, args, submit) def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp): """Splits leave application into two ledger entries to consider expiry of allocation""" raise_exception = False if frappe.flags.in_patch else True - leaves = get_number_of_leave_days(self.employee, self.leave_type, - self.from_date, expiry_date, self.half_day, self.half_day_date) + leaves = get_number_of_leave_days( + self.employee, self.leave_type, self.from_date, expiry_date, self.half_day, self.half_day_date + ) if leaves: args = dict( @@ -545,41 +686,51 @@ class LeaveApplication(Document): to_date=expiry_date, leaves=leaves * -1, is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) + or "", ) create_leave_ledger_entry(self, args, submit) if getdate(expiry_date) != getdate(self.to_date): start_date = add_days(expiry_date, 1) - leaves = get_number_of_leave_days(self.employee, self.leave_type, - start_date, self.to_date, self.half_day, self.half_day_date) + leaves = get_number_of_leave_days( + self.employee, self.leave_type, start_date, self.to_date, self.half_day, self.half_day_date + ) if leaves: - args.update(dict( - from_date=start_date, - to_date=self.to_date, - leaves=leaves * -1 - )) + args.update(dict(from_date=start_date, to_date=self.to_date, leaves=leaves * -1)) create_leave_ledger_entry(self, args, submit) -def get_allocation_expiry_for_cf_leaves(employee: str, leave_type: str, to_date: str, from_date: str) -> str: - ''' Returns expiry of carry forward allocation in leave ledger entry ''' - expiry = frappe.get_all("Leave Ledger Entry", +def get_allocation_expiry_for_cf_leaves( + employee: str, leave_type: str, to_date: str, from_date: str +) -> str: + """Returns expiry of carry forward allocation in leave ledger entry""" + expiry = frappe.get_all( + "Leave Ledger Entry", filters={ - 'employee': employee, - 'leave_type': leave_type, - 'is_carry_forward': 1, - 'transaction_type': 'Leave Allocation', - 'to_date': ['between', (from_date, to_date)], - 'docstatus': 1 - },fields=['to_date']) - return expiry[0]['to_date'] if expiry else '' + "employee": employee, + "leave_type": leave_type, + "is_carry_forward": 1, + "transaction_type": "Leave Allocation", + "to_date": ["between", (from_date, to_date)], + "docstatus": 1, + }, + fields=["to_date"], + ) + return expiry[0]["to_date"] if expiry else "" @frappe.whitelist() -def get_number_of_leave_days(employee: str, leave_type: str, from_date: str, to_date: str, half_day: Optional[int] = None, - half_day_date: Optional[str] = None, holiday_list: Optional[str] = None) -> float: +def get_number_of_leave_days( + employee: str, + leave_type: str, + from_date: str, + to_date: str, + half_day: Optional[int] = None, + half_day_date: Optional[str] = None, + holiday_list: Optional[str] = None, +) -> float: """Returns number of leave days between 2 dates after considering half day and holidays (Based on the include_holiday setting in Leave Type)""" number_of_days = 0 @@ -587,7 +738,7 @@ def get_number_of_leave_days(employee: str, leave_type: str, from_date: str, to_ if from_date == to_date: number_of_days = 0.5 elif half_day_date and half_day_date <= to_date: - number_of_days = date_diff(to_date, from_date) + .5 + number_of_days = date_diff(to_date, from_date) + 0.5 else: number_of_days = date_diff(to_date, from_date) + 1 @@ -595,7 +746,9 @@ def get_number_of_leave_days(employee: str, leave_type: str, from_date: str, to_ number_of_days = date_diff(to_date, from_date) + 1 if not frappe.db.get_value("Leave Type", leave_type, "include_holiday"): - number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list)) + number_of_days = flt(number_of_days) - flt( + get_holidays(employee, from_date, to_date, holiday_list=holiday_list) + ) return number_of_days @@ -606,54 +759,71 @@ def get_leave_details(employee, date): for d in allocation_records: allocation = allocation_records.get(d, frappe._dict()) - total_allocated_leaves = frappe.db.get_value('Leave Allocation', { - 'from_date': ('<=', date), - 'to_date': ('>=', date), - 'employee': employee, - 'leave_type': allocation.leave_type, - 'docstatus': 1 - }, 'SUM(total_leaves_allocated)') or 0 + total_allocated_leaves = ( + frappe.db.get_value( + "Leave Allocation", + { + "from_date": ("<=", date), + "to_date": (">=", date), + "employee": employee, + "leave_type": allocation.leave_type, + "docstatus": 1, + }, + "SUM(total_leaves_allocated)", + ) + or 0 + ) - remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, - consider_all_leaves_in_the_allocation_period=True) + remaining_leaves = get_leave_balance_on( + employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True + ) end_date = allocation.to_date leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1 - leaves_pending = get_leaves_pending_approval_for_period(employee, d, allocation.from_date, end_date) + leaves_pending = get_leaves_pending_approval_for_period( + employee, d, allocation.from_date, end_date + ) leave_allocation[d] = { "total_leaves": total_allocated_leaves, "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), "leaves_taken": leaves_taken, "leaves_pending_approval": leaves_pending, - "remaining_leaves": remaining_leaves} + "remaining_leaves": remaining_leaves, + } - #is used in set query + # is used in set query lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name") return { "leave_allocation": leave_allocation, "leave_approver": get_leave_approver(employee), - "lwps": lwp + "lwps": lwp, } @frappe.whitelist() -def get_leave_balance_on(employee: str, leave_type: str, date: str, to_date: str = None, - consider_all_leaves_in_the_allocation_period: bool = False, for_consumption: bool = False): - ''' - Returns leave balance till date - :param employee: employee name - :param leave_type: leave type - :param date: date to check balance on - :param to_date: future date to check for allocation expiry - :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date - :param for_consumption: flag to check if leave balance is required for consumption or display - eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave - in this case leave_balance = 10 but leave_balance_for_consumption = 1 - if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1} - else, returns leave_balance (in this case 10) - ''' +def get_leave_balance_on( + employee: str, + leave_type: str, + date: str, + to_date: str = None, + consider_all_leaves_in_the_allocation_period: bool = False, + for_consumption: bool = False, +): + """ + Returns leave balance till date + :param employee: employee name + :param leave_type: leave type + :param date: date to check balance on + :param to_date: future date to check for allocation expiry + :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date + :param for_consumption: flag to check if leave balance is required for consumption or display + eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave + in this case leave_balance = 10 but leave_balance_for_consumption = 1 + if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1} + else, returns leave_balance (in this case 10) + """ if not to_date: to_date = nowdate() @@ -671,17 +841,21 @@ def get_leave_balance_on(employee: str, leave_type: str, date: str, to_date: str if for_consumption: return remaining_leaves else: - return remaining_leaves.get('leave_balance') + return remaining_leaves.get("leave_balance") def get_leave_allocation_records(employee, date, leave_type=None): """Returns the total allocated leaves and carry forwarded leaves based on ledger entries""" Ledger = frappe.qb.DocType("Leave Ledger Entry") - cf_leave_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0) + cf_leave_case = ( + frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0) + ) sum_cf_leaves = Sum(cf_leave_case).as_("cf_leaves") - new_leaves_case = frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0) + new_leaves_case = ( + frappe.qb.terms.Case().when(Ledger.is_carry_forward == "0", Ledger.leaves).else_(0) + ) sum_new_leaves = Sum(new_leaves_case).as_("new_leaves") query = ( @@ -691,8 +865,9 @@ def get_leave_allocation_records(employee, date, leave_type=None): sum_new_leaves, Min(Ledger.from_date).as_("from_date"), Max(Ledger.to_date).as_("to_date"), - Ledger.leave_type - ).where( + Ledger.leave_type, + ) + .where( (Ledger.from_date <= date) & (Ledger.to_date >= date) & (Ledger.docstatus == 1) @@ -711,46 +886,57 @@ def get_leave_allocation_records(employee, date, leave_type=None): allocated_leaves = frappe._dict() for d in allocation_details: - allocated_leaves.setdefault(d.leave_type, frappe._dict({ - "from_date": d.from_date, - "to_date": d.to_date, - "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves), - "unused_leaves": d.cf_leaves, - "new_leaves_allocated": d.new_leaves, - "leave_type": d.leave_type - })) + allocated_leaves.setdefault( + d.leave_type, + frappe._dict( + { + "from_date": d.from_date, + "to_date": d.to_date, + "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves), + "unused_leaves": d.cf_leaves, + "new_leaves_allocated": d.new_leaves, + "leave_type": d.leave_type, + } + ), + ) return allocated_leaves -def get_leaves_pending_approval_for_period(employee: str, leave_type: str, from_date: str, to_date: str) -> float: - ''' Returns leaves that are pending for approval ''' - leaves = frappe.get_all("Leave Application", - filters={ - "employee": employee, - "leave_type": leave_type, - "status": "Open" - }, +def get_leaves_pending_approval_for_period( + employee: str, leave_type: str, from_date: str, to_date: str +) -> float: + """Returns leaves that are pending for approval""" + leaves = frappe.get_all( + "Leave Application", + filters={"employee": employee, "leave_type": leave_type, "status": "Open"}, or_filters={ "from_date": ["between", (from_date, to_date)], - "to_date": ["between", (from_date, to_date)] - }, fields=['SUM(total_leave_days) as leaves'])[0] - return leaves['leaves'] if leaves['leaves'] else 0.0 + "to_date": ["between", (from_date, to_date)], + }, + fields=["SUM(total_leave_days) as leaves"], + )[0] + return leaves["leaves"] if leaves["leaves"] else 0.0 -def get_remaining_leaves(allocation: Dict, leaves_taken: float, date: str, cf_expiry: str) -> Dict[str, float]: - '''Returns a dict of leave_balance and leave_balance_for_consumption +def get_remaining_leaves( + allocation: Dict, leaves_taken: float, date: str, cf_expiry: str +) -> Dict[str, float]: + """Returns a dict of leave_balance and leave_balance_for_consumption leave_balance returns the available leave balance leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry - ''' + """ + def _get_remaining_leaves(remaining_leaves, end_date): - ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' + """Returns minimum leaves remaining after comparing with remaining days for allocation expiry""" if remaining_leaves > 0: remaining_days = date_diff(end_date, date) + 1 remaining_leaves = min(remaining_days, remaining_leaves) return remaining_leaves - leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken) + leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt( + leaves_taken + ) # balance for carry forwarded leaves if cf_expiry and allocation.unused_leaves: @@ -764,21 +950,29 @@ def get_remaining_leaves(allocation: Dict, leaves_taken: float, date: str, cf_ex return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves) -def get_leaves_for_period(employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True) -> float: +def get_leaves_for_period( + employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True +) -> float: leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) leave_days = 0 for leave_entry in leave_entries: - inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date) + inclusive_period = leave_entry.from_date >= getdate( + from_date + ) and leave_entry.to_date <= getdate(to_date) - if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': + if inclusive_period and leave_entry.transaction_type == "Leave Encashment": leave_days += leave_entry.leaves - elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' and leave_entry.is_expired \ - and not skip_expired_leaves: + elif ( + inclusive_period + and leave_entry.transaction_type == "Leave Allocation" + and leave_entry.is_expired + and not skip_expired_leaves + ): leave_days += leave_entry.leaves - elif leave_entry.transaction_type == 'Leave Application': + elif leave_entry.transaction_type == "Leave Application": if leave_entry.from_date < getdate(from_date): leave_entry.from_date = from_date if leave_entry.to_date > getdate(to_date): @@ -789,18 +983,30 @@ def get_leaves_for_period(employee: str, leave_type: str, from_date: str, to_dat # fetch half day date for leaves with half days if leave_entry.leaves % 1: half_day = 1 - half_day_date = frappe.db.get_value('Leave Application', - {'name': leave_entry.transaction_name}, ['half_day_date']) + half_day_date = frappe.db.get_value( + "Leave Application", {"name": leave_entry.transaction_name}, ["half_day_date"] + ) - leave_days += get_number_of_leave_days(employee, leave_type, - leave_entry.from_date, leave_entry.to_date, half_day, half_day_date, holiday_list=leave_entry.holiday_list) * -1 + leave_days += ( + get_number_of_leave_days( + employee, + leave_type, + leave_entry.from_date, + leave_entry.to_date, + half_day, + half_day_date, + holiday_list=leave_entry.holiday_list, + ) + * -1 + ) return leave_days def get_leave_entries(employee, leave_type, from_date, to_date): - ''' Returns leave entries between from_date and to_date. ''' - return frappe.db.sql(""" + """Returns leave entries between from_date and to_date.""" + return frappe.db.sql( + """ SELECT employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, holiday_list, is_carry_forward, is_expired @@ -812,26 +1018,28 @@ def get_leave_entries(employee, leave_type, from_date, to_date): AND (from_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s OR (from_date < %(from_date)s AND to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + """, + {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, + as_dict=1, + ) @frappe.whitelist() -def get_holidays(employee, from_date, to_date, holiday_list = None): - '''get holidays between two dates for the given employee''' +def get_holidays(employee, from_date, to_date, holiday_list=None): + """get holidays between two dates for the given employee""" if not holiday_list: holiday_list = get_holiday_list_for_employee(employee) - holidays = frappe.db.sql("""select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2 + holidays = frappe.db.sql( + """select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2 where h1.parent = h2.name and h1.holiday_date between %s and %s - and h2.name = %s""", (from_date, to_date, holiday_list))[0][0] + and h2.name = %s""", + (from_date, to_date, holiday_list), + )[0][0] return holidays + def is_lwp(leave_type): lwp = frappe.db.sql("select is_lwp from `tabLeave Type` where name = %s", leave_type) return lwp and cint(lwp[0][0]) or 0 @@ -840,18 +1048,17 @@ def is_lwp(leave_type): @frappe.whitelist() def get_events(start, end, filters=None): from frappe.desk.reportview import get_filters_cond + events = [] - employee = frappe.db.get_value("Employee", - filters={"user_id": frappe.session.user}, - fieldname=["name", "company"], - as_dict=True + employee = frappe.db.get_value( + "Employee", filters={"user_id": frappe.session.user}, fieldname=["name", "company"], as_dict=True ) if employee: employee, company = employee.name, employee.company else: - employee = '' + employee = "" company = frappe.db.get_value("Global Defaults", None, "default_company") conditions = get_filters_cond("Leave Application", filters, []) @@ -873,18 +1080,24 @@ def add_department_leaves(events, start, end, employee, company): return # department leaves - department_employees = frappe.db.sql_list("""select name from tabEmployee where department=%s - and company=%s""", (department, company)) + department_employees = frappe.db.sql_list( + """select name from tabEmployee where department=%s + and company=%s""", + (department, company), + ) - filter_conditions = " and employee in (\"%s\")" % '", "'.join(department_employees) + filter_conditions = ' and employee in ("%s")' % '", "'.join(department_employees) add_leaves(events, start, end, filter_conditions=filter_conditions) def add_leaves(events, start, end, filter_conditions=None): from frappe.desk.reportview import build_match_conditions + conditions = [] - if not cint(frappe.db.get_value("HR Settings", None, "show_leaves_of_all_department_members_in_calendar")): + if not cint( + frappe.db.get_value("HR Settings", None, "show_leaves_of_all_department_members_in_calendar") + ): match_conditions = build_match_conditions("Leave Application") if match_conditions: @@ -909,12 +1122,12 @@ def add_leaves(events, start, end, filter_conditions=None): """ if conditions: - query += ' AND ' + ' AND '.join(conditions) + query += " AND " + " AND ".join(conditions) if filter_conditions: query += filter_conditions - for d in frappe.db.sql(query, {"start":start, "end": end}, as_dict=True): + for d in frappe.db.sql(query, {"start": start, "end": end}, as_dict=True): e = { "name": d.name, "doctype": "Leave Application", @@ -923,7 +1136,9 @@ def add_leaves(events, start, end, filter_conditions=None): "docstatus": d.docstatus, "color": d.color, "all_day": int(not d.half_day), - "title": cstr(d.employee_name) + f' ({cstr(d.leave_type)})' + (' ' + _('(Half Day)') if d.half_day else ''), + "title": cstr(d.employee_name) + + f" ({cstr(d.leave_type)})" + + (" " + _("(Half Day)") if d.half_day else ""), } if e not in events: events.append(e) @@ -937,14 +1152,16 @@ def add_block_dates(events, start, end, employee, company): block_dates = get_applicable_block_dates(start, end, employee, company, all_lists=True) for block_date in block_dates: - events.append({ - "doctype": "Leave Block List Date", - "from_date": block_date.block_date, - "to_date": block_date.block_date, - "title": _("Leave Blocked") + ": " + block_date.reason, - "name": "_" + str(cnt), - }) - cnt+=1 + events.append( + { + "doctype": "Leave Block List Date", + "from_date": block_date.block_date, + "to_date": block_date.block_date, + "title": _("Leave Blocked") + ": " + block_date.reason, + "name": "_" + str(cnt), + } + ) + cnt += 1 def add_holidays(events, start, end, employee, company): @@ -952,27 +1169,34 @@ def add_holidays(events, start, end, employee, company): if not applicable_holiday_list: return - for holiday in frappe.db.sql("""select name, holiday_date, description + for holiday in frappe.db.sql( + """select name, holiday_date, description from `tabHoliday` where parent=%s and holiday_date between %s and %s""", - (applicable_holiday_list, start, end), as_dict=True): - events.append({ + (applicable_holiday_list, start, end), + as_dict=True, + ): + events.append( + { "doctype": "Holiday", "from_date": holiday.holiday_date, - "to_date": holiday.holiday_date, + "to_date": holiday.holiday_date, "title": _("Holiday") + ": " + cstr(holiday.description), - "name": holiday.name - }) + "name": holiday.name, + } + ) @frappe.whitelist() def get_mandatory_approval(doctype): mandatory = "" if doctype == "Leave Application": - mandatory = frappe.db.get_single_value('HR Settings', - 'leave_approver_mandatory_in_leave_application') + mandatory = frappe.db.get_single_value( + "HR Settings", "leave_approver_mandatory_in_leave_application" + ) else: - mandatory = frappe.db.get_single_value('HR Settings', - 'expense_approver_mandatory_in_expense_claim') + mandatory = frappe.db.get_single_value( + "HR Settings", "expense_approver_mandatory_in_expense_claim" + ) return mandatory @@ -990,12 +1214,11 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): if leave_type: query += "and leave_type=%(leave_type)s" - leave_applications = frappe.db.sql(query,{ - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + leave_applications = frappe.db.sql( + query, + {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, + as_dict=1, + ) leave_days = 0 for leave_app in leave_applications: @@ -1007,19 +1230,24 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): if leave_app.to_date > getdate(to_date): leave_app.to_date = to_date - leave_days += get_number_of_leave_days(employee, leave_type, - leave_app.from_date, leave_app.to_date) + leave_days += get_number_of_leave_days( + employee, leave_type, leave_app.from_date, leave_app.to_date + ) return leave_days @frappe.whitelist() def get_leave_approver(employee): - leave_approver, department = frappe.db.get_value("Employee", - employee, ["leave_approver", "department"]) + leave_approver, department = frappe.db.get_value( + "Employee", employee, ["leave_approver", "department"] + ) if not leave_approver and department: - leave_approver = frappe.db.get_value('Department Approver', {'parent': department, - 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') + leave_approver = frappe.db.get_value( + "Department Approver", + {"parent": department, "parentfield": "leave_approvers", "idx": 1}, + "approver", + ) return leave_approver diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py index d56133b5660..ee5cbe99f31 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py @@ -1,19 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_application', - 'transactions': [ - { - 'items': ['Attendance'] - } - ], - 'reports': [ - { - 'label': _('Reports'), - 'items': ['Employee Leave Balance'] - } - ] - } + "fieldname": "leave_application", + "transactions": [{"items": ["Attendance"]}], + "reports": [{"label": _("Reports"), "items": ["Employee Leave Balance"]}], + } diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 4f7fcee30fe..dc8187cf5b7 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -49,7 +49,7 @@ _test_records = [ "description": "_Test Reason", "leave_type": "_Test Leave Type", "posting_date": "2013-01-02", - "to_date": "2013-05-05" + "to_date": "2013-05-05", }, { "company": "_Test Company", @@ -59,7 +59,7 @@ _test_records = [ "description": "_Test Reason", "leave_type": "_Test Leave Type", "posting_date": "2013-01-02", - "to_date": "2013-05-05" + "to_date": "2013-05-05", }, { "company": "_Test Company", @@ -69,8 +69,8 @@ _test_records = [ "description": "_Test Reason", "leave_type": "_Test Leave Type LWP", "posting_date": "2013-01-02", - "to_date": "2013-01-15" - } + "to_date": "2013-01-15", + }, ] @@ -90,19 +90,19 @@ class TestLeaveApplication(unittest.TestCase): self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date) if not frappe.db.exists("Leave Type", "_Test Leave Type"): - frappe.get_doc(dict( - leave_type_name="_Test Leave Type", - doctype="Leave Type", - include_holiday=True - )).insert() + frappe.get_doc( + dict(leave_type_name="_Test Leave Type", doctype="Leave Type", include_holiday=True) + ).insert() def tearDown(self): frappe.db.rollback() frappe.set_user("Administrator") def _clear_roles(self): - frappe.db.sql("""delete from `tabHas Role` where parent in - ("test@example.com", "test1@example.com", "test2@example.com")""") + frappe.db.sql( + """delete from `tabHas Role` where parent in + ("test@example.com", "test1@example.com", "test2@example.com")""" + ) def _clear_applications(self): frappe.db.sql("""delete from `tabLeave Application`""") @@ -113,91 +113,100 @@ class TestLeaveApplication(unittest.TestCase): application.to_date = "2013-01-05" return application - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_validate_application_across_allocations(self): # Test validation for application dates when negative balance is disabled frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name="Test Leave Validation", - doctype="Leave Type", - allow_negative=False - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=False) + ).insert() employee = get_employee() date = getdate() first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) - leave_application = frappe.get_doc(dict( - doctype='Leave Application', - employee=employee.name, - leave_type=leave_type.name, - from_date=add_days(first_sunday, 1), - to_date=add_days(first_sunday, 4), - company="_Test Company", - status="Approved", - leave_approver = 'test@example.com' - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 4), + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) # Application period cannot be outside leave allocation period self.assertRaises(frappe.ValidationError, leave_application.insert) - make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + make_allocation_record( + leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date) + ) - leave_application = frappe.get_doc(dict( - doctype='Leave Application', - employee=employee.name, - leave_type=leave_type.name, - from_date=add_days(first_sunday, -10), - to_date=add_days(first_sunday, 1), - company="_Test Company", - status="Approved", - leave_approver = 'test@example.com' - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, -10), + to_date=add_days(first_sunday, 1), + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) # Application period cannot be across two allocation records self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert) - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_insufficient_leave_balance_validation(self): # CASE 1: Validation when allow negative is disabled frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name="Test Leave Validation", - doctype="Leave Type", - allow_negative=False - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=False) + ).insert() employee = get_employee() date = getdate() first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) # allocate 2 leaves, apply for more - make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date), leaves=2) - leave_application = frappe.get_doc(dict( - doctype='Leave Application', - employee=employee.name, + make_allocation_record( leave_type=leave_type.name, - from_date=add_days(first_sunday, 1), - to_date=add_days(first_sunday, 3), - company="_Test Company", - status="Approved", - leave_approver = 'test@example.com' - )) + from_date=get_year_start(date), + to_date=get_year_ending(date), + leaves=2, + ) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 3), + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert) # CASE 2: Allows creating application with a warning message when allow negative is enabled frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True) - make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name) + make_leave_application( + employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name + ) - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_separate_leave_ledger_entry_for_boundary_applications(self): # When application falls in 2 different allocations and Allow Negative is enabled # creates separate leave ledger entries frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name="Test Leave Validation", - doctype="Leave Type", - allow_negative=True - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=True) + ).insert() employee = get_employee() date = getdate() @@ -208,13 +217,17 @@ class TestLeaveApplication(unittest.TestCase): # application across allocations # CASE 1: from date has no allocation, to date has an allocation / both dates have allocation - application = make_leave_application(employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name) + application = make_leave_application( + employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name + ) # 2 separate leave ledger entries - ledgers = frappe.db.get_all("Leave Ledger Entry", { - "transaction_type": "Leave Application", - "transaction_name": application.name - }, ["leaves", "from_date", "to_date"], order_by="from_date") + ledgers = frappe.db.get_all( + "Leave Ledger Entry", + {"transaction_type": "Leave Application", "transaction_name": application.name}, + ["leaves", "from_date", "to_date"], + order_by="from_date", + ) self.assertEqual(len(ledgers), 2) self.assertEqual(ledgers[0].from_date, application.from_date) @@ -224,13 +237,17 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(ledgers[1].to_date, application.to_date) # CASE 2: from date has an allocation, to date has no allocation - application = make_leave_application(employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name) + application = make_leave_application( + employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name + ) # 2 separate leave ledger entries - ledgers = frappe.db.get_all("Leave Ledger Entry", { - "transaction_type": "Leave Application", - "transaction_name": application.name - }, ["leaves", "from_date", "to_date"], order_by="from_date") + ledgers = frappe.db.get_all( + "Leave Ledger Entry", + {"transaction_type": "Leave Application", "transaction_name": application.name}, + ["leaves", "from_date", "to_date"], + order_by="from_date", + ) self.assertEqual(len(ledgers), 2) self.assertEqual(ledgers[0].from_date, application.from_date) @@ -240,74 +257,77 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(ledgers[1].to_date, application.to_date) def test_overwrite_attendance(self): - '''check attendance is automatically created on leave approval''' + """check attendance is automatically created on leave approval""" make_allocation_record() application = self.get_application(_test_records[0]) - application.status = 'Approved' - application.from_date = '2018-01-01' - application.to_date = '2018-01-03' + application.status = "Approved" + application.from_date = "2018-01-01" + application.to_date = "2018-01-03" application.insert() application.submit() - attendance = frappe.get_all('Attendance', ['name', 'status', 'attendance_date'], - dict(attendance_date=('between', ['2018-01-01', '2018-01-03']), docstatus=("!=", 2))) + attendance = frappe.get_all( + "Attendance", + ["name", "status", "attendance_date"], + dict(attendance_date=("between", ["2018-01-01", "2018-01-03"]), docstatus=("!=", 2)), + ) # attendance created for all 3 days self.assertEqual(len(attendance), 3) # all on leave - self.assertTrue(all([d.status == 'On Leave' for d in attendance])) + self.assertTrue(all([d.status == "On Leave" for d in attendance])) # dates dates = [d.attendance_date for d in attendance] - for d in ('2018-01-01', '2018-01-02', '2018-01-03'): + for d in ("2018-01-01", "2018-01-02", "2018-01-03"): self.assertTrue(getdate(d) in dates) - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_attendance_for_include_holidays(self): # Case 1: leave type with 'Include holidays within leaves as leaves' enabled frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name="Test Include Holidays", - doctype="Leave Type", - include_holiday=True - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Include Holidays", doctype="Leave Type", include_holiday=True) + ).insert() date = getdate() - make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + make_allocation_record( + leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date) + ) employee = get_employee() first_sunday = get_first_sunday(self.holiday_list) - leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application( + employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name + ) leave_application.reload() self.assertEqual(leave_application.total_leave_days, 4) - self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) + self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 4) leave_application.cancel() - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_attendance_update_for_exclude_holidays(self): # Case 2: leave type with 'Include holidays within leaves as leaves' disabled frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name="Test Do Not Include Holidays", - doctype="Leave Type", - include_holiday=False - )).insert() + leave_type = frappe.get_doc( + dict( + leave_type_name="Test Do Not Include Holidays", doctype="Leave Type", include_holiday=False + ) + ).insert() date = getdate() - make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + make_allocation_record( + leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date) + ) employee = get_employee() first_sunday = get_first_sunday(self.holiday_list) # already marked attendance on a holiday should be deleted in this case - config = { - "doctype": "Attendance", - "employee": employee.name, - "status": "Present" - } + config = {"doctype": "Attendance", "employee": employee.name, "status": "Present"} attendance_on_holiday = frappe.get_doc(config) attendance_on_holiday.attendance_date = first_sunday attendance_on_holiday.flags.ignore_validate = True @@ -319,7 +339,9 @@ class TestLeaveApplication(unittest.TestCase): attendance.flags.ignore_validate = True attendance.save() - leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company) + leave_application = make_leave_application( + employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company + ) leave_application.reload() # holiday should be excluded while marking attendance @@ -336,11 +358,13 @@ class TestLeaveApplication(unittest.TestCase): self._clear_roles() from frappe.utils.user import add_role + add_role("test@example.com", "HR User") clear_user_permissions_for_doctype("Employee") - frappe.db.set_value("Department", "_Test Department - _TC", - "leave_block_list", "_Test Leave Block List") + frappe.db.set_value( + "Department", "_Test Department - _TC", "leave_block_list", "_Test Leave Block List" + ) make_allocation_record() @@ -363,6 +387,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -379,6 +404,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -412,6 +438,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -434,6 +461,7 @@ class TestLeaveApplication(unittest.TestCase): self._clear_applications() from frappe.utils.user import add_role + add_role("test@example.com", "Employee") frappe.set_user("test@example.com") @@ -463,49 +491,49 @@ class TestLeaveApplication(unittest.TestCase): application.half_day_date = "2013-01-05" application.insert() - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_optional_leave(self): leave_period = get_leave_period() today = nowdate() - holiday_list = 'Test Holiday List for Optional Holiday' + holiday_list = "Test Holiday List for Optional Holiday" employee = get_employee() first_sunday = get_first_sunday(self.holiday_list) optional_leave_date = add_days(first_sunday, 1) - if not frappe.db.exists('Holiday List', holiday_list): - frappe.get_doc(dict( - doctype = 'Holiday List', - holiday_list_name = holiday_list, - from_date = add_months(today, -6), - to_date = add_months(today, 6), - holidays = [ - dict(holiday_date = optional_leave_date, description = 'Test') - ] - )).insert() + if not frappe.db.exists("Holiday List", holiday_list): + frappe.get_doc( + dict( + doctype="Holiday List", + holiday_list_name=holiday_list, + from_date=add_months(today, -6), + to_date=add_months(today, 6), + holidays=[dict(holiday_date=optional_leave_date, description="Test")], + ) + ).insert() - frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list) - leave_type = 'Test Optional Type' - if not frappe.db.exists('Leave Type', leave_type): - frappe.get_doc(dict( - leave_type_name = leave_type, - doctype = 'Leave Type', - is_optional_leave = 1 - )).insert() + frappe.db.set_value("Leave Period", leave_period.name, "optional_holiday_list", holiday_list) + leave_type = "Test Optional Type" + if not frappe.db.exists("Leave Type", leave_type): + frappe.get_doc( + dict(leave_type_name=leave_type, doctype="Leave Type", is_optional_leave=1) + ).insert() allocate_leaves(employee, leave_period, leave_type, 10) date = add_days(first_sunday, 2) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - company = '_Test Company', - description = "_Test Reason", - leave_type = leave_type, - from_date = date, - to_date = date, - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + company="_Test Company", + description="_Test Reason", + leave_type=leave_type, + from_date=date, + to_date=date, + ) + ) # can only apply on optional holidays self.assertRaises(NotAnOptionalHoliday, leave_application.insert) @@ -523,118 +551,125 @@ class TestLeaveApplication(unittest.TestCase): employee = get_employee() leave_period = get_leave_period() frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type', - doctype = 'Leave Type', - max_leaves_allowed = 5 - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Type", doctype="Leave Type", max_leaves_allowed=5) + ).insert() date = add_days(nowdate(), -7) allocate_leaves(employee, leave_period, leave_type.name, 5) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 2), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 2), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) leave_application.submit() - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = add_days(date, 4), - to_date = add_days(date, 8), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=add_days(date, 4), + to_date=add_days(date, 8), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertRaises(frappe.ValidationError, leave_application.insert) def test_applicable_after(self): employee = get_employee() leave_period = get_leave_period() frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type', - doctype = 'Leave Type', - applicable_after = 15 - )).insert() + leave_type = frappe.get_doc( + dict(leave_type_name="Test Leave Type", doctype="Leave Type", applicable_after=15) + ).insert() date = add_days(nowdate(), -7) - frappe.db.set_value('Employee', employee.name, "date_of_joining", date) + frappe.db.set_value("Employee", employee.name, "date_of_joining", date) allocate_leaves(employee, leave_period, leave_type.name, 10) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 4), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 4), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertRaises(frappe.ValidationError, leave_application.insert) frappe.delete_doc_if_exists("Leave Type", "Test Leave Type 1", force=1) - leave_type_1 = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type 1', - doctype = 'Leave Type' - )).insert() + leave_type_1 = frappe.get_doc( + dict(leave_type_name="Test Leave Type 1", doctype="Leave Type") + ).insert() allocate_leaves(employee, leave_period, leave_type_1.name, 10) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type_1.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 4), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type_1.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 4), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertTrue(leave_application.insert()) - frappe.db.set_value('Employee', employee.name, "date_of_joining", "2010-01-01") + frappe.db.set_value("Employee", employee.name, "date_of_joining", "2010-01-01") def test_max_continuous_leaves(self): employee = get_employee() leave_period = get_leave_period() frappe.delete_doc_if_exists("Leave Type", "Test Leave Type", force=1) - leave_type = frappe.get_doc(dict( - leave_type_name = 'Test Leave Type', - doctype = 'Leave Type', - max_leaves_allowed = 15, - max_continuous_days_allowed = 3 - )).insert() + leave_type = frappe.get_doc( + dict( + leave_type_name="Test Leave Type", + doctype="Leave Type", + max_leaves_allowed=15, + max_continuous_days_allowed=3, + ) + ).insert() date = add_days(nowdate(), -7) allocate_leaves(employee, leave_period, leave_type.name, 10) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - description = "_Test Reason", - from_date = date, - to_date = add_days(date, 4), - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + description="_Test Reason", + from_date=date, + to_date=add_days(date, 4), + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) self.assertRaises(frappe.ValidationError, leave_application.insert) @@ -643,59 +678,69 @@ class TestLeaveApplication(unittest.TestCase): leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + ) leave_type.insert() create_carry_forwarded_allocation(employee, leave_type) - details = get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True) + details = get_leave_balance_on( + employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True + ) self.assertEqual(details.leave_balance_for_consumption, 21) self.assertEqual(details.leave_balance, 30) def test_earned_leaves_creation(self): - frappe.db.sql('''delete from `tabLeave Period`''') - frappe.db.sql('''delete from `tabLeave Policy Assignment`''') - frappe.db.sql('''delete from `tabLeave Allocation`''') - frappe.db.sql('''delete from `tabLeave Ledger Entry`''') + frappe.db.sql("""delete from `tabLeave Period`""") + frappe.db.sql("""delete from `tabLeave Policy Assignment`""") + frappe.db.sql("""delete from `tabLeave Allocation`""") + frappe.db.sql("""delete from `tabLeave Ledger Entry`""") leave_period = get_leave_period() employee = get_employee() - leave_type = 'Test Earned Leave Type' - frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1) - frappe.get_doc(dict( - leave_type_name = leave_type, - doctype = 'Leave Type', - is_earned_leave = 1, - earned_leave_frequency = 'Monthly', - rounding = 0.5, - max_leaves_allowed = 6 - )).insert() + leave_type = "Test Earned Leave Type" + frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1) + frappe.get_doc( + dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + max_leaves_allowed=6, + ) + ).insert() - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}] - }).insert() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}], + } + ).insert() data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [employee.name], frappe._dict(data) + ) from erpnext.hr.utils import allocate_earned_leaves + i = 0 - while(i<14): + while i < 14: allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) # validate earned leaves creation without maximum leaves - frappe.db.set_value('Leave Type', leave_type, 'max_leaves_allowed', 0) + frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0) i = 0 - while(i<6): + while i < 6: allocate_earned_leaves(ignore_duplicates=True) i += 1 self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) @@ -704,34 +749,35 @@ class TestLeaveApplication(unittest.TestCase): def test_current_leave_on_submit(self): employee = get_employee() - leave_type = 'Sick Leave' - if not frappe.db.exists('Leave Type', leave_type): - frappe.get_doc(dict( - leave_type_name=leave_type, - doctype='Leave Type' - )).insert() + leave_type = "Sick Leave" + if not frappe.db.exists("Leave Type", leave_type): + frappe.get_doc(dict(leave_type_name=leave_type, doctype="Leave Type")).insert() - allocation = frappe.get_doc(dict( - doctype = 'Leave Allocation', - employee = employee.name, - leave_type = leave_type, - from_date = '2018-10-01', - to_date = '2018-10-10', - new_leaves_allocated = 1 - )) + allocation = frappe.get_doc( + dict( + doctype="Leave Allocation", + employee=employee.name, + leave_type=leave_type, + from_date="2018-10-01", + to_date="2018-10-10", + new_leaves_allocated=1, + ) + ) allocation.insert(ignore_permissions=True) allocation.submit() - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type, - description = "_Test Reason", - from_date = '2018-10-02', - to_date = '2018-10-02', - company = '_Test Company', - status = 'Approved', - leave_approver = 'test@example.com' - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type, + description="_Test Reason", + from_date="2018-10-02", + to_date="2018-10-02", + company="_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ) self.assertTrue(leave_application.insert()) leave_application.submit() self.assertEqual(leave_application.docstatus, 1) @@ -739,26 +785,31 @@ class TestLeaveApplication(unittest.TestCase): def test_creation_of_leave_ledger_entry_on_submit(self): employee = get_employee() - leave_type = create_leave_type(leave_type_name = 'Test Leave Type 1') + leave_type = create_leave_type(leave_type_name="Test Leave Type 1") leave_type.save() - leave_allocation = create_leave_allocation(employee=employee.name, employee_name=employee.employee_name, - leave_type=leave_type.name) + leave_allocation = create_leave_allocation( + employee=employee.name, employee_name=employee.employee_name, leave_type=leave_type.name + ) leave_allocation.submit() - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - from_date = add_days(nowdate(), 1), - to_date = add_days(nowdate(), 4), - description = "_Test Reason", - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(nowdate(), 1), + to_date=add_days(nowdate(), 4), + description="_Test Reason", + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) leave_application.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_application.name) + ) self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee) self.assertEqual(leave_ledger_entry[0].leave_type, leave_application.leave_type) @@ -766,32 +817,39 @@ class TestLeaveApplication(unittest.TestCase): # check if leave ledger entry is deleted on cancellation leave_application.cancel() - self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_application.name})) + self.assertFalse( + frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_application.name}) + ) def test_ledger_entry_creation_on_intermediate_allocation_expiry(self): employee = get_employee() leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + ) leave_type.submit() create_carry_forwarded_allocation(employee, leave_type) - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee.name, - leave_type = leave_type.name, - from_date = add_days(nowdate(), -3), - to_date = add_days(nowdate(), 7), - description = "_Test Reason", - company = "_Test Company", - docstatus = 1, - status = "Approved" - )) + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(nowdate(), -3), + to_date=add_days(nowdate(), 7), + description="_Test Reason", + company="_Test Company", + docstatus=1, + status="Approved", + ) + ) leave_application.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', '*', filters=dict(transaction_name=leave_application.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", "*", filters=dict(transaction_name=leave_application.name) + ) self.assertEqual(len(leave_ledger_entry), 2) self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee) @@ -805,12 +863,18 @@ class TestLeaveApplication(unittest.TestCase): leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + ) leave_type.submit() create_carry_forwarded_allocation(employee, leave_type) - self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0) + self.assertEqual( + get_leave_balance_on( + employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84) + ), + 0, + ) def test_leave_approver_perms(self): employee = get_employee() @@ -826,8 +890,8 @@ class TestLeaveApplication(unittest.TestCase): make_allocation_record(employee.name) application = self.get_application(_test_records[0]) - application.from_date = '2018-01-01' - application.to_date = '2018-01-03' + application.from_date = "2018-01-01" + application.to_date = "2018-01-03" application.leave_approver = user application.insert() self.assertTrue(application.name in frappe.share.get_shared("Leave Application", user)) @@ -853,7 +917,7 @@ class TestLeaveApplication(unittest.TestCase): employee.leave_approver = "" employee.save() - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_get_leave_details_for_dashboard(self): employee = get_employee() date = getdate() @@ -861,34 +925,44 @@ class TestLeaveApplication(unittest.TestCase): year_end = getdate(get_year_ending(date)) # ALLOCATION = 30 - allocation = make_allocation_record(employee=employee.name, from_date=year_start, to_date=year_end) + allocation = make_allocation_record( + employee=employee.name, from_date=year_start, to_date=year_end + ) # USED LEAVES = 4 first_sunday = get_first_sunday(self.holiday_list) - leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application = make_leave_application( + employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) leave_application.reload() # LEAVES PENDING APPROVAL = 1 - leave_application = make_leave_application(employee.name, add_days(first_sunday, 5), add_days(first_sunday, 5), - '_Test Leave Type', submit=False) - leave_application.status = 'Open' + leave_application = make_leave_application( + employee.name, + add_days(first_sunday, 5), + add_days(first_sunday, 5), + "_Test Leave Type", + submit=False, + ) + leave_application.status = "Open" leave_application.save() details = get_leave_details(employee.name, allocation.from_date) - leave_allocation = details['leave_allocation']['_Test Leave Type'] - self.assertEqual(leave_allocation['total_leaves'], 30) - self.assertEqual(leave_allocation['leaves_taken'], 4) - self.assertEqual(leave_allocation['expired_leaves'], 0) - self.assertEqual(leave_allocation['leaves_pending_approval'], 1) - self.assertEqual(leave_allocation['remaining_leaves'], 26) + leave_allocation = details["leave_allocation"]["_Test Leave Type"] + self.assertEqual(leave_allocation["total_leaves"], 30) + self.assertEqual(leave_allocation["leaves_taken"], 4) + self.assertEqual(leave_allocation["expired_leaves"], 0) + self.assertEqual(leave_allocation["leaves_pending_approval"], 1) + self.assertEqual(leave_allocation["remaining_leaves"], 26) - @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_get_leave_allocation_records(self): employee = get_employee() leave_type = create_leave_type( leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, - expire_carry_forwarded_leaves_after_days=90) + expire_carry_forwarded_leaves_after_days=90, + ) leave_type.insert() leave_alloc = create_carry_forwarded_allocation(employee, leave_type) @@ -899,89 +973,99 @@ class TestLeaveApplication(unittest.TestCase): "total_leaves_allocated": 30.0, "unused_leaves": 15.0, "new_leaves_allocated": 15.0, - "leave_type": leave_type.name + "leave_type": leave_type.name, } self.assertEqual(details.get(leave_type.name), expected_data) def create_carry_forwarded_allocation(employee, leave_type): - # initial leave allocation - leave_allocation = create_leave_allocation( - leave_type="_Test_CF_leave_expiry", - employee=employee.name, - employee_name=employee.employee_name, - from_date=add_months(nowdate(), -24), - to_date=add_months(nowdate(), -12), - carry_forward=0) - leave_allocation.submit() + # initial leave allocation + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + employee=employee.name, + employee_name=employee.employee_name, + from_date=add_months(nowdate(), -24), + to_date=add_months(nowdate(), -12), + carry_forward=0, + ) + leave_allocation.submit() - leave_allocation = create_leave_allocation( - leave_type="_Test_CF_leave_expiry", - employee=employee.name, - employee_name=employee.employee_name, - from_date=add_days(nowdate(), -84), - to_date=add_days(nowdate(), 100), - carry_forward=1) - leave_allocation.submit() + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + employee=employee.name, + employee_name=employee.employee_name, + from_date=add_days(nowdate(), -84), + to_date=add_days(nowdate(), 100), + carry_forward=1, + ) + leave_allocation.submit() - return leave_allocation + return leave_allocation -def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None): - allocation = frappe.get_doc({ - "doctype": "Leave Allocation", - "employee": employee or "_T-Employee-00001", - "leave_type": leave_type or "_Test Leave Type", - "from_date": from_date or "2013-01-01", - "to_date": to_date or "2019-12-31", - "new_leaves_allocated": leaves or 30, - "carry_forward": carry_forward - }) + +def make_allocation_record( + employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None +): + allocation = frappe.get_doc( + { + "doctype": "Leave Allocation", + "employee": employee or "_T-Employee-00001", + "leave_type": leave_type or "_Test Leave Type", + "from_date": from_date or "2013-01-01", + "to_date": to_date or "2019-12-31", + "new_leaves_allocated": leaves or 30, + "carry_forward": carry_forward, + } + ) allocation.insert(ignore_permissions=True) allocation.submit() return allocation + def get_employee(): return frappe.get_doc("Employee", "_T-Employee-00001") + def set_leave_approver(): employee = get_employee() dept_doc = frappe.get_doc("Department", employee.department) - dept_doc.append('leave_approvers', { - 'approver': 'test@example.com' - }) + dept_doc.append("leave_approvers", {"approver": "test@example.com"}) dept_doc.save(ignore_permissions=True) + def get_leave_period(): - leave_period_name = frappe.db.exists({ - "doctype": "Leave Period", - "company": "_Test Company" - }) + leave_period_name = frappe.db.exists({"doctype": "Leave Period", "company": "_Test Company"}) if leave_period_name: return frappe.get_doc("Leave Period", leave_period_name[0][0]) else: - return frappe.get_doc(dict( - name = 'Test Leave Period', - doctype = 'Leave Period', - from_date = add_months(nowdate(), -6), - to_date = add_months(nowdate(), 6), - company = "_Test Company", - is_active = 1 - )).insert() + return frappe.get_doc( + dict( + name="Test Leave Period", + doctype="Leave Period", + from_date=add_months(nowdate(), -6), + to_date=add_months(nowdate(), 6), + company="_Test Company", + is_active=1, + ) + ).insert() + def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, eligible_leaves=0): - allocate_leave = frappe.get_doc({ - "doctype": "Leave Allocation", - "__islocal": 1, - "employee": employee.name, - "employee_name": employee.employee_name, - "leave_type": leave_type, - "from_date": leave_period.from_date, - "to_date": leave_period.to_date, - "new_leaves_allocated": new_leaves_allocated, - "docstatus": 1 - }).insert() + allocate_leave = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": employee.name, + "employee_name": employee.employee_name, + "leave_type": leave_type, + "from_date": leave_period.from_date, + "to_date": leave_period.to_date, + "new_leaves_allocated": new_leaves_allocated, + "docstatus": 1, + } + ).insert() allocate_leave.submit() @@ -990,11 +1074,14 @@ def get_first_sunday(holiday_list, for_date=None): date = for_date or getdate() month_start_date = get_first_day(date) month_end_date = get_last_day(date) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = %s and holiday_date between %s and %s order by holiday_date - """, (holiday_list, month_start_date, month_end_date))[0][0] + """, + (holiday_list, month_start_date, month_end_date), + )[0][0] return first_sunday diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list.py b/erpnext/hr/doctype/leave_block_list/leave_block_list.py index d6b77f984cf..a57ba84e38d 100644 --- a/erpnext/hr/doctype/leave_block_list/leave_block_list.py +++ b/erpnext/hr/doctype/leave_block_list/leave_block_list.py @@ -10,7 +10,6 @@ from frappe.model.document import Document class LeaveBlockList(Document): - def validate(self): dates = [] for d in self.get("leave_block_list_dates"): @@ -20,23 +19,29 @@ class LeaveBlockList(Document): frappe.msgprint(_("Date is repeated") + ":" + d.block_date, raise_exception=1) dates.append(d.block_date) + @frappe.whitelist() -def get_applicable_block_dates(from_date, to_date, employee=None, - company=None, all_lists=False): +def get_applicable_block_dates(from_date, to_date, employee=None, company=None, all_lists=False): block_dates = [] for block_list in get_applicable_block_lists(employee, company, all_lists): - block_dates.extend(frappe.db.sql("""select block_date, reason + block_dates.extend( + frappe.db.sql( + """select block_date, reason from `tabLeave Block List Date` where parent=%s - and block_date between %s and %s""", (block_list, from_date, to_date), - as_dict=1)) + and block_date between %s and %s""", + (block_list, from_date, to_date), + as_dict=1, + ) + ) return block_dates + def get_applicable_block_lists(employee=None, company=None, all_lists=False): block_lists = [] if not employee: - employee = frappe.db.get_value("Employee", {"user_id":frappe.session.user}) + employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}) if not employee: return [] @@ -49,18 +54,25 @@ def get_applicable_block_lists(employee=None, company=None, all_lists=False): block_lists.append(block_list) # per department - department = frappe.db.get_value("Employee",employee, "department") + department = frappe.db.get_value("Employee", employee, "department") if department: block_list = frappe.db.get_value("Department", department, "leave_block_list") add_block_list(block_list) # global - for block_list in frappe.db.sql_list("""select name from `tabLeave Block List` - where applies_to_all_departments=1 and company=%s""", company): + for block_list in frappe.db.sql_list( + """select name from `tabLeave Block List` + where applies_to_all_departments=1 and company=%s""", + company, + ): add_block_list(block_list) return list(set(block_lists)) + def is_user_in_allow_list(block_list): - return frappe.session.user in frappe.db.sql_list("""select allow_user - from `tabLeave Block List Allow` where parent=%s""", block_list) + return frappe.session.user in frappe.db.sql_list( + """select allow_user + from `tabLeave Block List Allow` where parent=%s""", + block_list, + ) diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py index f91a8fe5201..afeb5ded39e 100644 --- a/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py +++ b/erpnext/hr/doctype/leave_block_list/leave_block_list_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'leave_block_list', - 'transactions': [ - { - 'items': ['Department'] - } - ] - } + return {"fieldname": "leave_block_list", "transactions": [{"items": ["Department"]}]} diff --git a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py index afbabb66a4a..be85a354149 100644 --- a/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py +++ b/erpnext/hr/doctype/leave_block_list/test_leave_block_list.py @@ -15,24 +15,36 @@ class TestLeaveBlockList(unittest.TestCase): def test_get_applicable_block_dates(self): frappe.set_user("test@example.com") - frappe.db.set_value("Department", "_Test Department - _TC", "leave_block_list", - "_Test Leave Block List") - self.assertTrue(getdate("2013-01-02") in - [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")]) + frappe.db.set_value( + "Department", "_Test Department - _TC", "leave_block_list", "_Test Leave Block List" + ) + self.assertTrue( + getdate("2013-01-02") + in [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")] + ) def test_get_applicable_block_dates_for_allowed_user(self): frappe.set_user("test1@example.com") - frappe.db.set_value("Department", "_Test Department 1 - _TC", "leave_block_list", - "_Test Leave Block List") - self.assertEqual([], [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")]) + frappe.db.set_value( + "Department", "_Test Department 1 - _TC", "leave_block_list", "_Test Leave Block List" + ) + self.assertEqual( + [], [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03")] + ) def test_get_applicable_block_dates_all_lists(self): frappe.set_user("test1@example.com") - frappe.db.set_value("Department", "_Test Department 1 - _TC", "leave_block_list", - "_Test Leave Block List") - self.assertTrue(getdate("2013-01-02") in - [d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03", all_lists=True)]) + frappe.db.set_value( + "Department", "_Test Department 1 - _TC", "leave_block_list", "_Test Leave Block List" + ) + self.assertTrue( + getdate("2013-01-02") + in [ + d.block_date for d in get_applicable_block_dates("2013-01-01", "2013-01-03", all_lists=True) + ] + ) + test_dependencies = ["Employee"] -test_records = frappe.get_test_records('Leave Block List') +test_records = frappe.get_test_records("Leave Block List") diff --git a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py index 19f97b83d47..c57f8ae72bf 100644 --- a/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py +++ b/erpnext/hr/doctype/leave_control_panel/leave_control_panel.py @@ -18,8 +18,12 @@ class LeaveControlPanel(Document): condition_str = " and " + " and ".join(conditions) if len(conditions) else "" - e = frappe.db.sql("select name from tabEmployee where status='Active' {condition}" - .format(condition=condition_str), tuple(values)) + e = frappe.db.sql( + "select name from tabEmployee where status='Active' {condition}".format( + condition=condition_str + ), + tuple(values), + ) return e @@ -27,7 +31,7 @@ class LeaveControlPanel(Document): for f in ["from_date", "to_date", "leave_type", "no_of_days"]: if not self.get(f): frappe.throw(_("{0} is required").format(self.meta.get_label(f))) - self.validate_from_to_dates('from_date', 'to_date') + self.validate_from_to_dates("from_date", "to_date") @frappe.whitelist() def allocate_leave(self): @@ -39,10 +43,10 @@ class LeaveControlPanel(Document): for d in self.get_employees(): try: - la = frappe.new_doc('Leave Allocation') + la = frappe.new_doc("Leave Allocation") la.set("__islocal", 1) la.employee = cstr(d[0]) - la.employee_name = frappe.db.get_value('Employee',cstr(d[0]),'employee_name') + la.employee_name = frappe.db.get_value("Employee", cstr(d[0]), "employee_name") la.leave_type = self.leave_type la.from_date = self.from_date la.to_date = self.to_date diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 8ef0e36fb8d..0f655e3e0fc 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -26,9 +26,12 @@ class LeaveEncashment(Document): self.encashment_date = getdate(nowdate()) def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) - + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def before_submit(self): if self.encashment_amount <= 0: @@ -36,7 +39,7 @@ class LeaveEncashment(Document): def on_submit(self): if not self.leave_allocation: - self.leave_allocation = self.get_leave_allocation().get('name') + self.leave_allocation = self.get_leave_allocation().get("name") additional_salary = frappe.new_doc("Additional Salary") additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.employee = self.employee @@ -52,8 +55,13 @@ class LeaveEncashment(Document): additional_salary.submit() # Set encashed leaves in Allocation - frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", - frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days) + frappe.db.set_value( + "Leave Allocation", + self.leave_allocation, + "total_leaves_encashed", + frappe.db.get_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed") + + self.encashable_days, + ) self.create_leave_ledger_entry() @@ -63,40 +71,69 @@ class LeaveEncashment(Document): self.db_set("additional_salary", "") if self.leave_allocation: - frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", - frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days) + frappe.db.set_value( + "Leave Allocation", + self.leave_allocation, + "total_leaves_encashed", + frappe.db.get_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed") + - self.encashable_days, + ) self.create_leave_ledger_entry(submit=False) @frappe.whitelist() def get_leave_details_for_encashment(self): - salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate())) + salary_structure = get_assigned_salary_structure( + self.employee, self.encashment_date or getdate(nowdate()) + ) if not salary_structure: - frappe.throw(_("No Salary Structure assigned for Employee {0} on given date {1}").format(self.employee, self.encashment_date)) + frappe.throw( + _("No Salary Structure assigned for Employee {0} on given date {1}").format( + self.employee, self.encashment_date + ) + ) - if not frappe.db.get_value("Leave Type", self.leave_type, 'allow_encashment'): + if not frappe.db.get_value("Leave Type", self.leave_type, "allow_encashment"): frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type)) allocation = self.get_leave_allocation() if not allocation: - frappe.throw(_("No Leaves Allocated to Employee: {0} for Leave Type: {1}").format(self.employee, self.leave_type)) + frappe.throw( + _("No Leaves Allocated to Employee: {0} for Leave Type: {1}").format( + self.employee, self.leave_type + ) + ) - self.leave_balance = allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count\ + self.leave_balance = ( + allocation.total_leaves_allocated + - allocation.carry_forwarded_leaves_count - get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date) + ) - encashable_days = self.leave_balance - frappe.db.get_value('Leave Type', self.leave_type, 'encashment_threshold_days') + encashable_days = self.leave_balance - frappe.db.get_value( + "Leave Type", self.leave_type, "encashment_threshold_days" + ) self.encashable_days = encashable_days if encashable_days > 0 else 0 - per_day_encashment = frappe.db.get_value('Salary Structure', salary_structure , 'leave_encashment_amount_per_day') - self.encashment_amount = self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0 + per_day_encashment = frappe.db.get_value( + "Salary Structure", salary_structure, "leave_encashment_amount_per_day" + ) + self.encashment_amount = ( + self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0 + ) self.leave_allocation = allocation.name return True def get_leave_allocation(self): - leave_allocation = frappe.db.sql("""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}' + leave_allocation = frappe.db.sql( + """select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}' between from_date and to_date and docstatus=1 and leave_type='{1}' - and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee), as_dict=1) #nosec + and employee= '{2}'""".format( + self.encashment_date or getdate(nowdate()), self.leave_type, self.employee + ), + as_dict=1, + ) # nosec return leave_allocation[0] if leave_allocation else None @@ -105,7 +142,7 @@ class LeaveEncashment(Document): leaves=self.encashable_days * -1, from_date=self.encashment_date, to_date=self.encashment_date, - is_carry_forward=0 + is_carry_forward=0, ) create_leave_ledger_entry(self, args, submit) @@ -114,27 +151,26 @@ class LeaveEncashment(Document): if not leave_allocation: return - to_date = leave_allocation.get('to_date') + to_date = leave_allocation.get("to_date") if to_date < getdate(nowdate()): args = frappe._dict( - leaves=self.encashable_days, - from_date=to_date, - to_date=to_date, - is_carry_forward=0 + leaves=self.encashable_days, from_date=to_date, to_date=to_date, is_carry_forward=0 ) create_leave_ledger_entry(self, args, submit) def create_leave_encashment(leave_allocation): - ''' Creates leave encashment for the given allocations ''' + """Creates leave encashment for the given allocations""" for allocation in leave_allocation: if not get_assigned_salary_structure(allocation.employee, allocation.to_date): continue - leave_encashment = frappe.get_doc(dict( - doctype="Leave Encashment", - leave_period=allocation.leave_period, - employee=allocation.employee, - leave_type=allocation.leave_type, - encashment_date=allocation.to_date - )) + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + leave_period=allocation.leave_period, + employee=allocation.employee, + leave_type=allocation.leave_type, + encashment_date=allocation.to_date, + ) + ) leave_encashment.insert(ignore_permissions=True) diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 99a479d3e5c..83eb969feb0 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -16,18 +16,19 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_ test_dependencies = ["Leave Type"] + class TestLeaveEncashment(unittest.TestCase): def setUp(self): - frappe.db.sql('''delete from `tabLeave Period`''') - frappe.db.sql('''delete from `tabLeave Policy Assignment`''') - frappe.db.sql('''delete from `tabLeave Allocation`''') - frappe.db.sql('''delete from `tabLeave Ledger Entry`''') - frappe.db.sql('''delete from `tabAdditional Salary`''') + frappe.db.sql("""delete from `tabLeave Period`""") + frappe.db.sql("""delete from `tabLeave Policy Assignment`""") + frappe.db.sql("""delete from `tabLeave Allocation`""") + frappe.db.sql("""delete from `tabLeave Ledger Entry`""") + frappe.db.sql("""delete from `tabAdditional Salary`""") # create the leave policy leave_policy = create_leave_policy( - leave_type="_Test Leave Type Encashment", - annual_allocation=10) + leave_type="_Test Leave Type Encashment", annual_allocation=10 + ) leave_policy.submit() # create employee, salary structure and assignment @@ -38,28 +39,44 @@ class TestLeaveEncashment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": self.leave_period.name + "leave_period": self.leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee], frappe._dict(data) + ) - salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee, - other_details={"leave_encashment_amount_per_day": 50}) + salary_structure = make_salary_structure( + "Salary Structure for Encashment", + "Monthly", + self.employee, + other_details={"leave_encashment_amount_per_day": 50}, + ) def tearDown(self): - for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]: + for dt in [ + "Leave Period", + "Leave Allocation", + "Leave Ledger Entry", + "Additional Salary", + "Leave Encashment", + "Salary Structure", + "Leave Policy", + ]: frappe.db.sql("delete from `tab%s`" % dt) def test_leave_balance_value_and_amount(self): - frappe.db.sql('''delete from `tabLeave Encashment`''') - leave_encashment = frappe.get_doc(dict( - doctype='Leave Encashment', - employee=self.employee, - leave_type="_Test Leave Type Encashment", - leave_period=self.leave_period.name, - payroll_date=today(), - currency="INR" - )).insert() + frappe.db.sql("""delete from `tabLeave Encashment`""") + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + payroll_date=today(), + currency="INR", + ) + ).insert() self.assertEqual(leave_encashment.leave_balance, 10) self.assertEqual(leave_encashment.encashable_days, 5) @@ -68,23 +85,27 @@ class TestLeaveEncashment(unittest.TestCase): leave_encashment.submit() # assert links - add_sal = frappe.get_all("Additional Salary", filters = {"ref_docname": leave_encashment.name})[0] + add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0] self.assertTrue(add_sal) def test_creation_of_leave_ledger_entry_on_submit(self): - frappe.db.sql('''delete from `tabLeave Encashment`''') - leave_encashment = frappe.get_doc(dict( - doctype='Leave Encashment', - employee=self.employee, - leave_type="_Test Leave Type Encashment", - leave_period=self.leave_period.name, - payroll_date=today(), - currency="INR" - )).insert() + frappe.db.sql("""delete from `tabLeave Encashment`""") + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + payroll_date=today(), + currency="INR", + ) + ).insert() leave_encashment.submit() - leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_encashment.name)) + leave_ledger_entry = frappe.get_all( + "Leave Ledger Entry", fields="*", filters=dict(transaction_name=leave_encashment.name) + ) self.assertEqual(len(leave_ledger_entry), 1) self.assertEqual(leave_ledger_entry[0].employee, leave_encashment.employee) @@ -93,7 +114,11 @@ class TestLeaveEncashment(unittest.TestCase): # check if leave ledger entry is deleted on cancellation - frappe.db.sql("Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name) ) + frappe.db.sql( + "Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name) + ) leave_encashment.cancel() - self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name})) + self.assertFalse( + frappe.db.exists("Leave Ledger Entry", {"transaction_name": leave_encashment.name}) + ) diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index a5923e0021c..fed9f770dfc 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -20,9 +20,11 @@ class LeaveLedgerEntry(Document): else: frappe.throw(_("Only expired allocation can be cancelled")) + def validate_leave_allocation_against_leave_application(ledger): - ''' Checks that leave allocation has no leave application against it ''' - leave_application_records = frappe.db.sql_list(""" + """Checks that leave allocation has no leave application against it""" + leave_application_records = frappe.db.sql_list( + """ SELECT transaction_name FROM `tabLeave Ledger Entry` WHERE @@ -31,15 +33,21 @@ def validate_leave_allocation_against_leave_application(ledger): AND transaction_type='Leave Application' AND from_date>=%s AND to_date<=%s - """, (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date)) + """, + (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date), + ) if leave_application_records: - frappe.throw(_("Leave allocation {0} is linked with the Leave Application {1}").format( - ledger.transaction_name, ', '.join(leave_application_records))) + frappe.throw( + _("Leave allocation {0} is linked with the Leave Application {1}").format( + ledger.transaction_name, ", ".join(leave_application_records) + ) + ) + def create_leave_ledger_entry(ref_doc, args, submit=True): ledger = frappe._dict( - doctype='Leave Ledger Entry', + doctype="Leave Ledger Entry", employee=ref_doc.employee, employee_name=ref_doc.employee_name, leave_type=ref_doc.leave_type, @@ -47,7 +55,7 @@ def create_leave_ledger_entry(ref_doc, args, submit=True): transaction_name=ref_doc.name, is_carry_forward=0, is_expired=0, - is_lwp=0 + is_lwp=0, ) ledger.update(args) @@ -58,54 +66,69 @@ def create_leave_ledger_entry(ref_doc, args, submit=True): else: delete_ledger_entry(ledger) + def delete_ledger_entry(ledger): - ''' Delete ledger entry on cancel of leave application/allocation/encashment ''' + """Delete ledger entry on cancel of leave application/allocation/encashment""" if ledger.transaction_type == "Leave Allocation": validate_leave_allocation_against_leave_application(ledger) expired_entry = get_previous_expiry_ledger_entry(ledger) - frappe.db.sql("""DELETE + frappe.db.sql( + """DELETE FROM `tabLeave Ledger Entry` WHERE `transaction_name`=%s - OR `name`=%s""", (ledger.transaction_name, expired_entry)) + OR `name`=%s""", + (ledger.transaction_name, expired_entry), + ) + def get_previous_expiry_ledger_entry(ledger): - ''' Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled ''' - creation_date = frappe.db.get_value("Leave Ledger Entry", filters={ - 'transaction_name': ledger.transaction_name, - 'is_expired': 0, - 'transaction_type': 'Leave Allocation' - }, fieldname=['creation']) + """Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled""" + creation_date = frappe.db.get_value( + "Leave Ledger Entry", + filters={ + "transaction_name": ledger.transaction_name, + "is_expired": 0, + "transaction_type": "Leave Allocation", + }, + fieldname=["creation"], + ) - creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else '' + creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else "" + + return frappe.db.get_value( + "Leave Ledger Entry", + filters={ + "creation": ("like", creation_date + "%"), + "employee": ledger.employee, + "leave_type": ledger.leave_type, + "is_expired": 1, + "docstatus": 1, + "is_carry_forward": 0, + }, + fieldname=["name"], + ) - return frappe.db.get_value("Leave Ledger Entry", filters={ - 'creation': ('like', creation_date+"%"), - 'employee': ledger.employee, - 'leave_type': ledger.leave_type, - 'is_expired': 1, - 'docstatus': 1, - 'is_carry_forward': 0 - }, fieldname=['name']) def process_expired_allocation(): - ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry - Case 1: carry forwarded expiry period is set for the leave type, - create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves - Case 2: leave type has no specific expiry period for carry forwarded leaves - and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves. - ''' + """Check if a carry forwarded allocation has expired and create a expiry ledger entry + Case 1: carry forwarded expiry period is set for the leave type, + create a separate leave expiry entry against each entry of carry forwarded and non carry forwarded leaves + Case 2: leave type has no specific expiry period for carry forwarded leaves + and there is no carry forwarded leave allocation, create a single expiry against the remaining leaves. + """ # fetch leave type records that has carry forwarded leaves expiry - leave_type_records = frappe.db.get_values("Leave Type", filters={ - 'expire_carry_forwarded_leaves_after_days': (">", 0) - }, fieldname=['name']) + leave_type_records = frappe.db.get_values( + "Leave Type", filters={"expire_carry_forwarded_leaves_after_days": (">", 0)}, fieldname=["name"] + ) - leave_type = [record[0] for record in leave_type_records] or [''] + leave_type = [record[0] for record in leave_type_records] or [""] # fetch non expired leave ledger entry of transaction_type allocation - expire_allocation = frappe.db.sql(""" + expire_allocation = frappe.db.sql( + """ SELECT leaves, to_date, employee, leave_type, is_carry_forward, transaction_name as name, transaction_type @@ -123,32 +146,41 @@ def process_expired_allocation(): OR (is_carry_forward = 0 AND leave_type not in %s) ))) AND transaction_type = 'Leave Allocation' - AND to_date < %s""", (leave_type, today()), as_dict=1) + AND to_date < %s""", + (leave_type, today()), + as_dict=1, + ) if expire_allocation: create_expiry_ledger_entry(expire_allocation) + def create_expiry_ledger_entry(allocations): - ''' Create ledger entry for expired allocation ''' + """Create ledger entry for expired allocation""" for allocation in allocations: if allocation.is_carry_forward: expire_carried_forward_allocation(allocation) else: expire_allocation(allocation) + def get_remaining_leaves(allocation): - ''' Returns remaining leaves from the given allocation ''' - return frappe.db.get_value("Leave Ledger Entry", + """Returns remaining leaves from the given allocation""" + return frappe.db.get_value( + "Leave Ledger Entry", filters={ - 'employee': allocation.employee, - 'leave_type': allocation.leave_type, - 'to_date': ('<=', allocation.to_date), - 'docstatus': 1 - }, fieldname=['SUM(leaves)']) + "employee": allocation.employee, + "leave_type": allocation.leave_type, + "to_date": ("<=", allocation.to_date), + "docstatus": 1, + }, + fieldname=["SUM(leaves)"], + ) + @frappe.whitelist() def expire_allocation(allocation, expiry_date=None): - ''' expires non-carry forwarded allocation ''' + """expires non-carry forwarded allocation""" leaves = get_remaining_leaves(allocation) expiry_date = expiry_date if expiry_date else allocation.to_date @@ -157,21 +189,28 @@ def expire_allocation(allocation, expiry_date=None): args = dict( leaves=flt(leaves) * -1, transaction_name=allocation.name, - transaction_type='Leave Allocation', + transaction_type="Leave Allocation", from_date=expiry_date, to_date=expiry_date, is_carry_forward=0, - is_expired=1 + is_expired=1, ) create_leave_ledger_entry(allocation, args) frappe.db.set_value("Leave Allocation", allocation.name, "expired", 1) + def expire_carried_forward_allocation(allocation): - ''' Expires remaining leaves in the on carried forward allocation ''' + """Expires remaining leaves in the on carried forward allocation""" from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period - leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, - allocation.from_date, allocation.to_date, skip_expired_leaves=False) + + leaves_taken = get_leaves_for_period( + allocation.employee, + allocation.leave_type, + allocation.from_date, + allocation.to_date, + skip_expired_leaves=False, + ) leaves = flt(allocation.leaves) + flt(leaves_taken) # allow expired leaves entry to be created @@ -183,6 +222,6 @@ def expire_carried_forward_allocation(allocation): is_carry_forward=allocation.is_carry_forward, is_expired=1, from_date=allocation.to_date, - to_date=allocation.to_date + to_date=allocation.to_date, ) create_leave_ledger_entry(allocation, args) diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index b1cb6887d99..6e62bb58765 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -11,7 +11,6 @@ from erpnext.hr.utils import validate_overlap class LeavePeriod(Document): - def validate(self): self.validate_dates() validate_overlap(self, self.from_date, self.to_date, self.company) diff --git a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py index fbe56e2b700..854f988f35b 100644 --- a/erpnext/hr/doctype/leave_period/leave_period_dashboard.py +++ b/erpnext/hr/doctype/leave_period/leave_period_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_period', - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Leave Allocation'] - } - ] + "fieldname": "leave_period", + "transactions": [{"label": _("Transactions"), "items": ["Leave Allocation"]}], } diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 10936dddc98..09235741b6f 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -9,23 +9,32 @@ import erpnext test_dependencies = ["Employee", "Leave Type", "Leave Policy"] + class TestLeavePeriod(unittest.TestCase): pass + def create_leave_period(from_date, to_date, company=None): - leave_period = frappe.db.get_value('Leave Period', - dict(company=company or erpnext.get_default_company(), + leave_period = frappe.db.get_value( + "Leave Period", + dict( + company=company or erpnext.get_default_company(), from_date=from_date, to_date=to_date, - is_active=1), 'name') + is_active=1, + ), + "name", + ) if leave_period: return frappe.get_doc("Leave Period", leave_period) - leave_period = frappe.get_doc({ - "doctype": "Leave Period", - "company": company or erpnext.get_default_company(), - "from_date": from_date, - "to_date": to_date, - "is_active": 1 - }).insert() + leave_period = frappe.get_doc( + { + "doctype": "Leave Period", + "company": company or erpnext.get_default_company(), + "from_date": from_date, + "to_date": to_date, + "is_active": 1, + } + ).insert() return leave_period diff --git a/erpnext/hr/doctype/leave_policy/leave_policy.py b/erpnext/hr/doctype/leave_policy/leave_policy.py index 80450d5d6e0..33c949354cc 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy.py @@ -11,6 +11,12 @@ class LeavePolicy(Document): def validate(self): if self.leave_policy_details: for lp_detail in self.leave_policy_details: - max_leaves_allowed = frappe.db.get_value("Leave Type", lp_detail.leave_type, "max_leaves_allowed") + max_leaves_allowed = frappe.db.get_value( + "Leave Type", lp_detail.leave_type, "max_leaves_allowed" + ) if max_leaves_allowed > 0 and lp_detail.annual_allocation > max_leaves_allowed: - frappe.throw(_("Maximum leave allowed in the leave type {0} is {1}").format(lp_detail.leave_type, max_leaves_allowed)) + frappe.throw( + _("Maximum leave allowed in the leave type {0} is {1}").format( + lp_detail.leave_type, max_leaves_allowed + ) + ) diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py index 8311fd2f93e..57ea93ee466 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_policy', - 'transactions': [ - { - 'label': _('Leaves'), - 'items': ['Leave Policy Assignment', 'Leave Allocation'] - }, - ] + "fieldname": "leave_policy", + "transactions": [ + {"label": _("Leaves"), "items": ["Leave Policy Assignment", "Leave Allocation"]}, + ], } diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py index 3dbbef857ec..0e1ccad6019 100644 --- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py +++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py @@ -15,17 +15,24 @@ class TestLeavePolicy(unittest.TestCase): leave_type.max_leaves_allowed = 2 leave_type.save() - leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1) + leave_policy = create_leave_policy( + leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1 + ) self.assertRaises(frappe.ValidationError, leave_policy.insert) + def create_leave_policy(**args): - ''' Returns an object of leave policy ''' + """Returns an object of leave policy""" args = frappe._dict(args) - return frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{ - "leave_type": args.leave_type or "_Test Leave Type", - "annual_allocation": args.annual_allocation or 10 - }] - }) + return frappe.get_doc( + { + "doctype": "Leave Policy", + "leave_policy_details": [ + { + "leave_type": args.leave_type or "_Test Leave Type", + "annual_allocation": args.annual_allocation or 10, + } + ], + } + ) diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index ff1668003e7..bb19ffa9d1e 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -23,22 +23,33 @@ class LeavePolicyAssignment(Document): def set_dates(self): if self.assignment_based_on == "Leave Period": - self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"]) + self.effective_from, self.effective_to = frappe.db.get_value( + "Leave Period", self.leave_period, ["from_date", "to_date"] + ) elif self.assignment_based_on == "Joining Date": self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining") def validate_policy_assignment_overlap(self): - leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = { - "employee": self.employee, - "name": ("!=", self.name), - "docstatus": 1, - "effective_to": (">=", self.effective_from), - "effective_from": ("<=", self.effective_to) - }) + leave_policy_assignments = frappe.get_all( + "Leave Policy Assignment", + filters={ + "employee": self.employee, + "name": ("!=", self.name), + "docstatus": 1, + "effective_to": (">=", self.effective_from), + "effective_from": ("<=", self.effective_to), + }, + ) if len(leave_policy_assignments): - frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") - .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) + frappe.throw( + _("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}").format( + bold(self.leave_policy), + bold(self.employee), + bold(formatdate(self.effective_from)), + bold(formatdate(self.effective_to)), + ) + ) def warn_about_carry_forwarding(self): if not self.carry_forward: @@ -50,8 +61,9 @@ class LeavePolicyAssignment(Document): for policy in leave_policy.leave_policy_details: leave_type = leave_types.get(policy.leave_type) if not leave_type.is_carry_forward: - msg = _("Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled.").format( - frappe.bold(get_link_to_form("Leave Type", leave_type.name))) + msg = _( + "Leaves for the Leave Type {0} won't be carry-forwarded since carry-forwarding is disabled." + ).format(frappe.bold(get_link_to_form("Leave Type", leave_type.name))) frappe.msgprint(msg, indicator="orange", alert=True) @frappe.whitelist() @@ -68,41 +80,54 @@ class LeavePolicyAssignment(Document): for leave_policy_detail in leave_policy.leave_policy_details: if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: leave_allocation, new_leaves_allocated = self.create_leave_allocation( - leave_policy_detail.leave_type, leave_policy_detail.annual_allocation, - leave_type_details, date_of_joining + leave_policy_detail.leave_type, + leave_policy_detail.annual_allocation, + leave_type_details, + date_of_joining, ) - leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated} + leave_allocations[leave_policy_detail.leave_type] = { + "name": leave_allocation, + "leaves": new_leaves_allocated, + } self.db_set("leaves_allocated", 1) return leave_allocations - def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + def create_leave_allocation( + self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ): # Creates leave allocation for the given employee in the provided leave period carry_forward = self.carry_forward if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward: carry_forward = 0 - new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated, - leave_type_details, date_of_joining) + new_leaves_allocated = self.get_new_leaves( + leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ) - allocation = frappe.get_doc(dict( - doctype="Leave Allocation", - employee=self.employee, - leave_type=leave_type, - from_date=self.effective_from, - to_date=self.effective_to, - new_leaves_allocated=new_leaves_allocated, - leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else '', - leave_policy_assignment = self.name, - leave_policy = self.leave_policy, - carry_forward=carry_forward - )) - allocation.save(ignore_permissions = True) + allocation = frappe.get_doc( + dict( + doctype="Leave Allocation", + employee=self.employee, + leave_type=leave_type, + from_date=self.effective_from, + to_date=self.effective_to, + new_leaves_allocated=new_leaves_allocated, + leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else "", + leave_policy_assignment=self.name, + leave_policy=self.leave_policy, + carry_forward=carry_forward, + ) + ) + allocation.save(ignore_permissions=True) allocation.submit() return allocation.name, new_leaves_allocated def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): from frappe.model.meta import get_field_precision - precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated")) + + precision = get_field_precision( + frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated") + ) # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 if leave_type_details.get(leave_type).is_compensatory == 1: @@ -113,16 +138,22 @@ class LeavePolicyAssignment(Document): new_leaves_allocated = 0 else: # get leaves for past months if assignment is based on Leave Period / Joining Date - new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining) + new_leaves_allocated = self.get_leaves_for_passed_months( + leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ) # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period elif getdate(date_of_joining) > getdate(self.effective_from): - remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) + remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / ( + date_diff(self.effective_to, self.effective_from) + 1 + ) new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) return flt(new_leaves_allocated, precision) - def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + def get_leaves_for_passed_months( + self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining + ): from erpnext.hr.utils import get_monthly_earned_leave current_date = frappe.flags.current_date or getdate() @@ -145,8 +176,11 @@ class LeavePolicyAssignment(Document): months_passed = add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj) if months_passed > 0: - monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, - leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding) + monthly_earned_leave = get_monthly_earned_leave( + new_leaves_allocated, + leave_type_details.get(leave_type).earned_leave_frequency, + leave_type_details.get(leave_type).rounding, + ) new_leaves_allocated = monthly_earned_leave * months_passed else: new_leaves_allocated = 0 @@ -175,7 +209,7 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj def create_assignment_for_multiple_employees(employees, data): if isinstance(employees, string_types): - employees= json.loads(employees) + employees = json.loads(employees) if isinstance(data, string_types): data = frappe._dict(json.loads(data)) @@ -202,11 +236,23 @@ def create_assignment_for_multiple_employees(employees, data): return docs_name + def get_leave_type_details(): leave_type_details = frappe._dict() - leave_types = frappe.get_all("Leave Type", - fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "based_on_date_of_joining", - "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"]) + leave_types = frappe.get_all( + "Leave Type", + fields=[ + "name", + "is_lwp", + "is_earned_leave", + "is_compensatory", + "based_on_date_of_joining", + "is_carry_forward", + "expire_carry_forwarded_leaves_after_days", + "earned_leave_frequency", + "rounding", + ], + ) for d in leave_types: leave_type_details.setdefault(d.name, d) return leave_type_details diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py index ec6592cb72a..13b39c7ee6e 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'leave_policy_assignment', - 'transactions': [ - { - 'label': _('Leaves'), - 'items': ['Leave Allocation'] - }, - ] + "fieldname": "leave_policy_assignment", + "transactions": [ + {"label": _("Leaves"), "items": ["Leave Allocation"]}, + ], } diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index 08680425a02..20249b38ef7 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -17,9 +17,16 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( test_dependencies = ["Employee"] + class TestLeavePolicyAssignment(unittest.TestCase): def setUp(self): - for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + for doctype in [ + "Leave Period", + "Leave Application", + "Leave Allocation", + "Leave Policy Assignment", + "Leave Ledger Entry", + ]: frappe.db.delete(doctype) employee = get_employee() @@ -35,16 +42,25 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) - self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) + self.assertEqual( + frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), + 1, + ) - leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": self.employee.name, - "leave_policy":leave_policy.name, - "leave_policy_assignment": leave_policy_assignments[0], - "docstatus": 1})[0] + leave_allocation = frappe.get_list( + "Leave Allocation", + filters={ + "employee": self.employee.name, + "leave_policy": leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1, + }, + )[0] leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) @@ -63,67 +79,93 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) # every leave is allocated no more leave can be granted now - self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 1) - leave_allocation = frappe.get_list("Leave Allocation", filters={ - "employee": self.employee.name, - "leave_policy":leave_policy.name, - "leave_policy_assignment": leave_policy_assignments[0], - "docstatus": 1})[0] + self.assertEqual( + frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), + 1, + ) + leave_allocation = frappe.get_list( + "Leave Allocation", + filters={ + "employee": self.employee.name, + "leave_policy": leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1, + }, + )[0] leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) leave_alloc_doc.cancel() leave_alloc_doc.delete() - self.assertEqual(frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), 0) + self.assertEqual( + frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "leaves_allocated"), + 0, + ) def test_earned_leave_allocation(self): leave_period = create_leave_period("Test Earned Leave Period") leave_type = create_earned_leave_type("Test Earned Leave") - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] - }).submit() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}], + } + ).submit() data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } # second last day of the month # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency frappe.flags.current_date = add_days(get_last_day(getdate()), -1) - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 0) def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1))) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -1)) + ) # Case 1: assignment created one month after the leave period, should allocate 1 leave frappe.flags.current_date = get_first_day(getdate()) data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 1) def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self): - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -2)) + ) # Case 2: assignment created on the last day of the leave period's latter month # should allocate 1 leave for current month even though the month has not ended # since the daily job might have already executed @@ -132,32 +174,48 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) # if the daily job is not completed yet, there is another check present # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -2)) + ) # initial leave allocation = 5 - leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave", - from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0) + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, + leave_type="Test Earned Leave", + from_date=add_months(getdate(), -12), + to_date=add_months(getdate(), -3), + new_leaves_allocated=5, + carry_forward=0, + ) leave_allocation.submit() # Case 3: assignment created on the last day of the leave period's latter month with carry forwarding @@ -166,14 +224,19 @@ class TestLeavePolicyAssignment(unittest.TestCase): "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, "leave_period": leave_period.name, - "carry_forward": 1 + "carry_forward": 1, } # carry forwarded leaves = 5, 3 leaves allocated for passed months - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - details = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True) + details = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], + as_dict=True, + ) self.assertEqual(details.new_leaves_allocated, 2) self.assertEqual(details.unused_leaves, 5) self.assertEqual(details.total_leaves_allocated, 7) @@ -181,20 +244,27 @@ class TestLeavePolicyAssignment(unittest.TestCase): # if the daily job is not completed yet, there is another check present # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import is_earned_leave_already_allocated + frappe.flags.current_date = get_last_day(getdate()) allocation = frappe.get_doc("Leave Allocation", details.name) # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves - self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation)) + self.assertFalse( + is_earned_leave_already_allocated( + allocation, leave_policy.leave_policy_details[0].annual_allocation + ) + ) def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): # tests leave alloc for earned leaves for assignment based on joining date in policy assignment leave_type = create_earned_leave_type("Test Earned Leave") - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).submit() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}], + } + ).submit() # joining date set to 2 months back self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) @@ -202,29 +272,39 @@ class TestLeavePolicyAssignment(unittest.TestCase): # assignment created on the last day of the current month frappe.flags.current_date = get_last_day(getdate()) - data = { - "assignment_based_on": "Joining Date", - "leave_policy": leave_policy.name - } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") - effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + data = {"assignment_based_on": "Joining Date", "leave_policy": leave_policy.name} + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) + effective_from = frappe.db.get_value( + "Leave Policy Assignment", leave_policy_assignments[0], "effective_from" + ) self.assertEqual(effective_from, self.employee.date_of_joining) self.assertEqual(leaves_allocated, 3) # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_last_day(getdate()) allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type - leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True) + leave_period, leave_policy = setup_leave_period_and_policy( + get_first_day(add_months(getdate(), -2)), based_on_doj=True + ) # joining date set to 2 months back self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) @@ -236,34 +316,43 @@ class TestLeavePolicyAssignment(unittest.TestCase): data = { "assignment_based_on": "Leave Period", "leave_policy": leave_policy.name, - "leave_period": leave_period.name + "leave_period": leave_period.name, } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) # if the daily job is not completed yet, there is another check present # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", { - "leave_policy_assignment": leave_policy_assignments[0] - }, "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).submit() + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}], + } + ).submit() # joining date set to 2 months back # leave should be allocated for current month too since this day is same as the joining day @@ -272,24 +361,32 @@ class TestLeavePolicyAssignment(unittest.TestCase): # assignment created on the first day of the current month frappe.flags.current_date = get_first_day(getdate()) - data = { - "assignment_based_on": "Joining Date", - "leave_policy": leave_policy.name - } - leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") - effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + data = {"assignment_based_on": "Joining Date", "leave_policy": leave_policy.name} + leave_policy_assignments = create_assignment_for_multiple_employees( + [self.employee.name], frappe._dict(data) + ) + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) + effective_from = frappe.db.get_value( + "Leave Policy Assignment", leave_policy_assignments[0], "effective_from" + ) self.assertEqual(effective_from, self.employee.date_of_joining) self.assertEqual(leaves_allocated, 3) # to ensure leave is not already allocated to avoid duplication from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) allocate_earned_leaves() - leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, - "total_leaves_allocated") + leaves_allocated = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated", + ) self.assertEqual(leaves_allocated, 3) def tearDown(self): @@ -301,15 +398,17 @@ class TestLeavePolicyAssignment(unittest.TestCase): def create_earned_leave_type(leave_type, based_on_doj=False): frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) - return frappe.get_doc(dict( - leave_type_name=leave_type, - doctype="Leave Type", - is_earned_leave=1, - earned_leave_frequency="Monthly", - rounding=0.5, - is_carry_forward=1, - based_on_date_of_joining=based_on_doj - )).insert() + return frappe.get_doc( + dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + is_carry_forward=1, + based_on_date_of_joining=based_on_doj, + ) + ).insert() def create_leave_period(name, start_date=None): @@ -317,24 +416,27 @@ def create_leave_period(name, start_date=None): if not start_date: start_date = get_first_day(getdate()) - return frappe.get_doc(dict( - name=name, - doctype="Leave Period", - from_date=start_date, - to_date=add_months(start_date, 12), - company="_Test Company", - is_active=1 - )).insert() + return frappe.get_doc( + dict( + name=name, + doctype="Leave Period", + from_date=start_date, + to_date=add_months(start_date, 12), + company="_Test Company", + is_active=1, + ) + ).insert() def setup_leave_period_and_policy(start_date, based_on_doj=False): leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj) - leave_period = create_leave_period("Test Earned Leave Period", - start_date=start_date) - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] - }).insert() + leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date) + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}], + } + ).insert() - return leave_period, leave_policy \ No newline at end of file + return leave_period, leave_policy diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py index 4b59c2c09b4..82b9bd65753 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.py +++ b/erpnext/hr/doctype/leave_type/leave_type.py @@ -11,17 +11,23 @@ from frappe.utils import today class LeaveType(Document): def validate(self): if self.is_lwp: - leave_allocation = frappe.get_all("Leave Allocation", filters={ - 'leave_type': self.name, - 'from_date': ("<=", today()), - 'to_date': (">=", today()) - }, fields=['name']) - leave_allocation = [l['name'] for l in leave_allocation] + leave_allocation = frappe.get_all( + "Leave Allocation", + filters={"leave_type": self.name, "from_date": ("<=", today()), "to_date": (">=", today())}, + fields=["name"], + ) + leave_allocation = [l["name"] for l in leave_allocation] if leave_allocation: - frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec + frappe.throw( + _( + "Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay" + ).format(", ".join(leave_allocation)) + ) # nosec if self.is_lwp and self.is_ppl: frappe.throw(_("Leave Type can be either without pay or partial pay")) - if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1): + if self.is_ppl and ( + self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1 + ): frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1")) diff --git a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py index 8dc9402d1eb..269a1ecc696 100644 --- a/erpnext/hr/doctype/leave_type/leave_type_dashboard.py +++ b/erpnext/hr/doctype/leave_type/leave_type_dashboard.py @@ -1,14 +1,10 @@ - - def get_data(): return { - 'fieldname': 'leave_type', - 'transactions': [ + "fieldname": "leave_type", + "transactions": [ { - 'items': ['Leave Allocation', 'Leave Application'], + "items": ["Leave Allocation", "Leave Application"], }, - { - 'items': ['Attendance', 'Leave Encashment'] - } - ] + {"items": ["Attendance", "Leave Encashment"]}, + ], } diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index c1b64e99eff..69f9e125203 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -3,27 +3,30 @@ import frappe -test_records = frappe.get_test_records('Leave Type') +test_records = frappe.get_test_records("Leave Type") + def create_leave_type(**args): - args = frappe._dict(args) - if frappe.db.exists("Leave Type", args.leave_type_name): - return frappe.get_doc("Leave Type", args.leave_type_name) - leave_type = frappe.get_doc({ - "doctype": "Leave Type", - "leave_type_name": args.leave_type_name or "_Test Leave Type", - "include_holiday": args.include_holidays or 1, - "allow_encashment": args.allow_encashment or 0, - "is_earned_leave": args.is_earned_leave or 0, - "is_lwp": args.is_lwp or 0, - "is_ppl":args.is_ppl or 0, - "is_carry_forward": args.is_carry_forward or 0, - "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0, - "encashment_threshold_days": args.encashment_threshold_days or 5, - "earning_component": "Leave Encashment" - }) + args = frappe._dict(args) + if frappe.db.exists("Leave Type", args.leave_type_name): + return frappe.get_doc("Leave Type", args.leave_type_name) + leave_type = frappe.get_doc( + { + "doctype": "Leave Type", + "leave_type_name": args.leave_type_name or "_Test Leave Type", + "include_holiday": args.include_holidays or 1, + "allow_encashment": args.allow_encashment or 0, + "is_earned_leave": args.is_earned_leave or 0, + "is_lwp": args.is_lwp or 0, + "is_ppl": args.is_ppl or 0, + "is_carry_forward": args.is_carry_forward or 0, + "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0, + "encashment_threshold_days": args.encashment_threshold_days or 5, + "earning_component": "Leave Encashment", + } + ) - if leave_type.is_ppl: - leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 + if leave_type.is_ppl: + leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5 - return leave_type + return leave_type diff --git a/erpnext/hr/doctype/offer_term/test_offer_term.py b/erpnext/hr/doctype/offer_term/test_offer_term.py index 2e5ed75438b..2bea7b2597f 100644 --- a/erpnext/hr/doctype/offer_term/test_offer_term.py +++ b/erpnext/hr/doctype/offer_term/test_offer_term.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Offer Term') + class TestOfferTerm(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 517730281fc..5a1248698c2 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -20,7 +20,7 @@ class ShiftAssignment(Document): self.validate_overlapping_dates() if self.end_date: - self.validate_from_to_dates('start_date', 'end_date') + self.validate_from_to_dates("start_date", "end_date") def validate_overlapping_dates(self): if not self.name: @@ -33,7 +33,7 @@ class ShiftAssignment(Document): """ if self.end_date: - condition += """ or + condition += """ or %(end_date)s between start_date and end_date or start_date between %(start_date)s and %(end_date)s @@ -41,7 +41,8 @@ class ShiftAssignment(Document): else: condition += """ ) """ - assigned_shifts = frappe.db.sql(""" + assigned_shifts = frappe.db.sql( + """ select name, shift_type, start_date ,end_date, docstatus, status from `tabShift Assignment` where @@ -49,13 +50,18 @@ class ShiftAssignment(Document): and name != %(name)s and status = "Active" {0} - """.format(condition), { - "employee": self.employee, - "shift_type": self.shift_type, - "start_date": self.start_date, - "end_date": self.end_date, - "name": self.name - }, as_dict = 1) + """.format( + condition + ), + { + "employee": self.employee, + "shift_type": self.shift_type, + "start_date": self.start_date, + "end_date": self.end_date, + "name": self.name, + }, + as_dict=1, + ) if len(assigned_shifts): self.throw_overlap_error(assigned_shifts[0]) @@ -63,7 +69,9 @@ class ShiftAssignment(Document): def throw_overlap_error(self, shift_details): shift_details = frappe._dict(shift_details) if shift_details.docstatus == 1 and shift_details.status == "Active": - msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)) + msg = _("Employee {0} already has Active Shift {1}: {2}").format( + frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name) + ) if shift_details.start_date: msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y")) title = "Ongoing Shift" @@ -73,23 +81,27 @@ class ShiftAssignment(Document): if msg: frappe.throw(msg, title=title) + @frappe.whitelist() def get_events(start, end, filters=None): events = [] - employee = frappe.db.get_value("Employee", {"user_id": frappe.session.user}, ["name", "company"], - as_dict=True) + employee = frappe.db.get_value( + "Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True + ) if employee: employee, company = employee.name, employee.company else: - employee='' - company=frappe.db.get_value("Global Defaults", None, "default_company") + employee = "" + company = frappe.db.get_value("Global Defaults", None, "default_company") from frappe.desk.reportview import get_filters_cond + conditions = get_filters_cond("Shift Assignment", filters, []) add_assignments(events, start, end, conditions=conditions) return events + def add_assignments(events, start, end, conditions=None): query = """select name, start_date, end_date, employee_name, employee, docstatus, shift_type @@ -101,7 +113,7 @@ def add_assignments(events, start, end, conditions=None): if conditions: query += conditions - records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True) + records = frappe.db.sql(query, {"start_date": start, "end_date": end}, as_dict=True) shift_timing_map = get_shift_type_timing([d.shift_type for d in records]) for d in records: @@ -109,27 +121,33 @@ def add_assignments(events, start, end, conditions=None): daily_event_end = d.end_date if d.end_date else getdate() delta = timedelta(days=1) while daily_event_start <= daily_event_end: - start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time'] - end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time'] + start_timing = ( + frappe.utils.get_datetime(daily_event_start) + shift_timing_map[d.shift_type]["start_time"] + ) + end_timing = ( + frappe.utils.get_datetime(daily_event_start) + shift_timing_map[d.shift_type]["end_time"] + ) daily_event_start += delta e = { "name": d.name, "doctype": "Shift Assignment", "start_date": start_timing, "end_date": end_timing, - "title": cstr(d.employee_name) + ": "+ \ - cstr(d.shift_type), + "title": cstr(d.employee_name) + ": " + cstr(d.shift_type), "docstatus": d.docstatus, - "allDay": 0 + "allDay": 0, } if e not in events: events.append(e) return events + def get_shift_type_timing(shift_types): shift_timing_map = {} - data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time']) + data = frappe.get_all( + "Shift Type", filters={"name": ("IN", shift_types)}, fields=["name", "start_time", "end_time"] + ) for d in data: shift_timing_map[d.name] = d @@ -137,7 +155,9 @@ def get_shift_type_timing(shift_types): return shift_timing_map -def get_employee_shift(employee, for_date=None, consider_default_shift=False, next_shift_direction=None): +def get_employee_shift( + employee, for_date=None, consider_default_shift=False, next_shift_direction=None +): """Returns a Shift Type for the given employee on the given date. (excluding the holidays) :param employee: Employee for which shift is required. @@ -147,21 +167,25 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne """ if for_date is None: for_date = nowdate() - default_shift = frappe.db.get_value('Employee', employee, 'default_shift') + default_shift = frappe.db.get_value("Employee", employee, "default_shift") shift_type_name = None - shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date']) + shift_assignment_details = frappe.db.get_value( + "Shift Assignment", + {"employee": employee, "start_date": ("<=", for_date), "docstatus": "1", "status": "Active"}, + ["shift_type", "end_date"], + ) if shift_assignment_details: shift_type_name = shift_assignment_details[0] # if end_date present means that shift is over after end_date else it is a ongoing shift. - if shift_assignment_details[1] and for_date >= shift_assignment_details[1] : + if shift_assignment_details[1] and for_date >= shift_assignment_details[1]: shift_type_name = None if not shift_type_name and consider_default_shift: shift_type_name = default_shift if shift_type_name: - holiday_list_name = frappe.db.get_value('Shift Type', shift_type_name, 'holiday_list') + holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list") if not holiday_list_name: holiday_list_name = get_holiday_list_for_employee(employee, False) if holiday_list_name and is_holiday(holiday_list_name, for_date): @@ -170,22 +194,30 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne if not shift_type_name and next_shift_direction: MAX_DAYS = 366 if consider_default_shift and default_shift: - direction = -1 if next_shift_direction == 'reverse' else +1 + direction = -1 if next_shift_direction == "reverse" else +1 for i in range(MAX_DAYS): - date = for_date+timedelta(days=direction*(i+1)) + date = for_date + timedelta(days=direction * (i + 1)) shift_details = get_employee_shift(employee, date, consider_default_shift, None) if shift_details: shift_type_name = shift_details.shift_type.name for_date = date break else: - direction = '<' if next_shift_direction == 'reverse' else '>' - sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc' - dates = frappe.db.get_all('Shift Assignment', - ['start_date', 'end_date'], - {'employee':employee, 'start_date':(direction, for_date), 'docstatus': '1', "status": "Active"}, + direction = "<" if next_shift_direction == "reverse" else ">" + sort_order = "desc" if next_shift_direction == "reverse" else "asc" + dates = frappe.db.get_all( + "Shift Assignment", + ["start_date", "end_date"], + { + "employee": employee, + "start_date": (direction, for_date), + "docstatus": "1", + "status": "Active", + }, as_list=True, - limit=MAX_DAYS, order_by="start_date "+sort_order) + limit=MAX_DAYS, + order_by="start_date " + sort_order, + ) if dates: for date in dates: @@ -201,35 +233,57 @@ def get_employee_shift(employee, for_date=None, consider_default_shift=False, ne def get_employee_shift_timings(employee, for_timestamp=None, consider_default_shift=False): - """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee - """ + """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee""" if for_timestamp is None: for_timestamp = now_datetime() # write and verify a test case for midnight shift. prev_shift = curr_shift = next_shift = None - curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward') + curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, "forward") if curr_shift: - next_shift = get_employee_shift(employee, curr_shift.start_datetime.date()+timedelta(days=1), consider_default_shift, 'forward') - prev_shift = get_employee_shift(employee, for_timestamp.date()+timedelta(days=-1), consider_default_shift, 'reverse') + next_shift = get_employee_shift( + employee, + curr_shift.start_datetime.date() + timedelta(days=1), + consider_default_shift, + "forward", + ) + prev_shift = get_employee_shift( + employee, for_timestamp.date() + timedelta(days=-1), consider_default_shift, "reverse" + ) if curr_shift: if prev_shift: - curr_shift.actual_start = prev_shift.end_datetime if curr_shift.actual_start < prev_shift.end_datetime else curr_shift.actual_start - prev_shift.actual_end = curr_shift.actual_start if prev_shift.actual_end > curr_shift.actual_start else prev_shift.actual_end + curr_shift.actual_start = ( + prev_shift.end_datetime + if curr_shift.actual_start < prev_shift.end_datetime + else curr_shift.actual_start + ) + prev_shift.actual_end = ( + curr_shift.actual_start + if prev_shift.actual_end > curr_shift.actual_start + else prev_shift.actual_end + ) if next_shift: - next_shift.actual_start = curr_shift.end_datetime if next_shift.actual_start < curr_shift.end_datetime else next_shift.actual_start - curr_shift.actual_end = next_shift.actual_start if curr_shift.actual_end > next_shift.actual_start else curr_shift.actual_end + next_shift.actual_start = ( + curr_shift.end_datetime + if next_shift.actual_start < curr_shift.end_datetime + else next_shift.actual_start + ) + curr_shift.actual_end = ( + next_shift.actual_start + if curr_shift.actual_end > next_shift.actual_start + else curr_shift.actual_end + ) return prev_shift, curr_shift, next_shift def get_shift_details(shift_type_name, for_date=None): """Returns Shift Details which contain some additional information as described below. 'shift_details' contains the following keys: - 'shift_type' - Object of DocType Shift Type, - 'start_datetime' - Date and Time of shift start on given date, - 'end_datetime' - Date and Time of shift end on given date, - 'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time', - 'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero) + 'shift_type' - Object of DocType Shift Type, + 'start_datetime' - Date and Time of shift start on given date, + 'end_datetime' - Date and Time of shift end on given date, + 'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time', + 'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero) :param shift_type_name: shift type name for which shift_details is required. :param for_date: Date on which shift_details are required @@ -238,30 +292,38 @@ def get_shift_details(shift_type_name, for_date=None): return None if not for_date: for_date = nowdate() - shift_type = frappe.get_doc('Shift Type', shift_type_name) + shift_type = frappe.get_doc("Shift Type", shift_type_name) start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time - for_date = for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date + for_date = ( + for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date + ) end_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.end_time - actual_start = start_datetime - timedelta(minutes=shift_type.begin_check_in_before_shift_start_time) + actual_start = start_datetime - timedelta( + minutes=shift_type.begin_check_in_before_shift_start_time + ) actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time) - return frappe._dict({ - 'shift_type': shift_type, - 'start_datetime': start_datetime, - 'end_datetime': end_datetime, - 'actual_start': actual_start, - 'actual_end': actual_end - }) + return frappe._dict( + { + "shift_type": shift_type, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "actual_start": actual_start, + "actual_end": actual_end, + } + ) def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_default_shift=False): """Takes a datetime and returns the 'actual' start datetime and end datetime of the shift in which the timestamp belongs. - Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time". - None is returned if the timestamp is outside any actual shift timings. - Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned) + Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time". + None is returned if the timestamp is outside any actual shift timings. + Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned) """ actual_shift_start = actual_shift_end = shift_details = None - shift_timings_as_per_timestamp = get_employee_shift_timings(employee, for_datetime, consider_default_shift) + shift_timings_as_per_timestamp = get_employee_shift_timings( + employee, for_datetime, consider_default_shift + ) timestamp_list = [] for shift in shift_timings_as_per_timestamp: if shift: @@ -273,11 +335,11 @@ def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_defa if timestamp and for_datetime <= timestamp: timestamp_index = index break - if timestamp_index and timestamp_index%2 == 1: - shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)] + if timestamp_index and timestamp_index % 2 == 1: + shift_details = shift_timings_as_per_timestamp[int((timestamp_index - 1) / 2)] actual_shift_start = shift_details.actual_start actual_shift_end = shift_details.actual_end elif timestamp_index: - shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)] + shift_details = shift_timings_as_per_timestamp[int(timestamp_index / 2)] return actual_shift_start, actual_shift_end, shift_details diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py index d4900814ffe..4a1ec293bde 100644 --- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py @@ -8,19 +8,21 @@ from frappe.utils import add_days, nowdate test_dependencies = ["Shift Type"] -class TestShiftAssignment(unittest.TestCase): +class TestShiftAssignment(unittest.TestCase): def setUp(self): frappe.db.sql("delete from `tabShift Assignment`") def test_make_shift_assignment(self): - shift_assignment = frappe.get_doc({ - "doctype": "Shift Assignment", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "start_date": nowdate() - }).insert() + shift_assignment = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + } + ).insert() shift_assignment.submit() self.assertEqual(shift_assignment.docstatus, 1) @@ -28,52 +30,59 @@ class TestShiftAssignment(unittest.TestCase): def test_overlapping_for_ongoing_shift(self): # shift should be Ongoing if Only start_date is present and status = Active - shift_assignment_1 = frappe.get_doc({ - "doctype": "Shift Assignment", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "start_date": nowdate(), - "status": 'Active' - }).insert() + shift_assignment_1 = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + "status": "Active", + } + ).insert() shift_assignment_1.submit() self.assertEqual(shift_assignment_1.docstatus, 1) - shift_assignment = frappe.get_doc({ - "doctype": "Shift Assignment", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "start_date": add_days(nowdate(), 2) - }) + shift_assignment = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": add_days(nowdate(), 2), + } + ) self.assertRaises(frappe.ValidationError, shift_assignment.save) def test_overlapping_for_fixed_period_shift(self): # shift should is for Fixed period if Only start_date and end_date both are present and status = Active - shift_assignment_1 = frappe.get_doc({ + shift_assignment_1 = frappe.get_doc( + { "doctype": "Shift Assignment", "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", "start_date": nowdate(), "end_date": add_days(nowdate(), 30), - "status": 'Active' - }).insert() - shift_assignment_1.submit() + "status": "Active", + } + ).insert() + shift_assignment_1.submit() - - # it should not allowed within period of any shift. - shift_assignment_3 = frappe.get_doc({ + # it should not allowed within period of any shift. + shift_assignment_3 = frappe.get_doc( + { "doctype": "Shift Assignment", "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", - "start_date":add_days(nowdate(), 10), + "start_date": add_days(nowdate(), 10), "end_date": add_days(nowdate(), 35), - "status": 'Active' - }) + "status": "Active", + } + ) - self.assertRaises(frappe.ValidationError, shift_assignment_3.save) + self.assertRaises(frappe.ValidationError, shift_assignment_3.save) diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py index d4fcf99d7d8..1e3e8ff6464 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -10,7 +10,9 @@ from frappe.utils import formatdate, getdate from erpnext.hr.utils import share_doc_with_approver, validate_active_employee -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + class ShiftRequest(Document): def validate(self): @@ -39,24 +41,35 @@ class ShiftRequest(Document): assignment_doc.insert() assignment_doc.submit() - frappe.msgprint(_("Shift Assignment: {0} created for Employee: {1}").format(frappe.bold(assignment_doc.name), frappe.bold(self.employee))) + frappe.msgprint( + _("Shift Assignment: {0} created for Employee: {1}").format( + frappe.bold(assignment_doc.name), frappe.bold(self.employee) + ) + ) def on_cancel(self): - shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name}) + shift_assignment_list = frappe.get_list( + "Shift Assignment", {"employee": self.employee, "shift_request": self.name} + ) if shift_assignment_list: for shift in shift_assignment_list: - shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name']) + shift_assignment_doc = frappe.get_doc("Shift Assignment", shift["name"]) shift_assignment_doc.cancel() def validate_default_shift(self): default_shift = frappe.get_value("Employee", self.employee, "default_shift") if self.shift_type == default_shift: - frappe.throw(_("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type))) + frappe.throw( + _("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type)) + ) def validate_approver(self): department = frappe.get_value("Employee", self.employee, "department") shift_approver = frappe.get_value("Employee", self.employee, "shift_request_approver") - approvers = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department)) + approvers = frappe.db.sql( + """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", + (department), + ) approvers = [approver[0] for approver in approvers] approvers.append(shift_approver) if self.approver not in approvers: @@ -67,10 +80,11 @@ class ShiftRequest(Document): frappe.throw(_("To date cannot be before from date")) def validate_shift_request_overlap_dates(self): - if not self.name: - self.name = "New Shift Request" + if not self.name: + self.name = "New Shift Request" - d = frappe.db.sql(""" + d = frappe.db.sql( + """ select name, shift_type, from_date, to_date from `tabShift Request` @@ -79,20 +93,23 @@ class ShiftRequest(Document): and %(from_date)s <= to_date) or ( %(to_date)s >= from_date and %(to_date)s <= to_date )) - and name != %(name)s""", { - "employee": self.employee, - "shift_type": self.shift_type, - "from_date": self.from_date, - "to_date": self.to_date, - "name": self.name - }, as_dict=1) + and name != %(name)s""", + { + "employee": self.employee, + "shift_type": self.shift_type, + "from_date": self.from_date, + "to_date": self.to_date, + "name": self.name, + }, + as_dict=1, + ) - for date_overlap in d: - if date_overlap ['name']: - self.throw_overlap_error(date_overlap) + for date_overlap in d: + if date_overlap["name"]: + self.throw_overlap_error(date_overlap) def throw_overlap_error(self, d): - msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, - d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ - + """ {0}""".format(d["name"]) + msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format( + self.employee, d["shift_type"], formatdate(d["from_date"]), formatdate(d["to_date"]) + ) + """ {0}""".format(d["name"]) frappe.throw(msg, OverlapError) diff --git a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py index cd4519e8797..2859b8f7717 100644 --- a/erpnext/hr/doctype/shift_request/shift_request_dashboard.py +++ b/erpnext/hr/doctype/shift_request/shift_request_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'shift_request', - 'transactions': [ - { - 'items': ['Shift Assignment'] - }, - ], - } + return { + "fieldname": "shift_request", + "transactions": [ + {"items": ["Shift Assignment"]}, + ], + } diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py index 3633c9b3003..b4f51772159 100644 --- a/erpnext/hr/doctype/shift_request/test_shift_request.py +++ b/erpnext/hr/doctype/shift_request/test_shift_request.py @@ -10,6 +10,7 @@ from erpnext.hr.doctype.employee.test_employee import make_employee test_dependencies = ["Shift Type"] + class TestShiftRequest(unittest.TestCase): def setUp(self): for doctype in ["Shift Request", "Shift Assignment"]: @@ -20,9 +21,12 @@ class TestShiftRequest(unittest.TestCase): def test_make_shift_request(self): "Test creation/updation of Shift Assignment from Shift Request." - department = frappe.get_value("Employee", "_T-Employee-00001", 'department') + department = frappe.get_value("Employee", "_T-Employee-00001", "department") set_shift_approver(department) - approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] + approver = frappe.db.sql( + """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", + (department), + )[0][0] shift_request = make_shift_request(approver) @@ -31,7 +35,7 @@ class TestShiftRequest(unittest.TestCase): "Shift Assignment", filters={"shift_request": shift_request.name}, fieldname=["employee", "docstatus"], - as_dict=True + as_dict=True, ) self.assertEqual(shift_request.employee, shift_assignment.employee) self.assertEqual(shift_assignment.docstatus, 1) @@ -39,9 +43,7 @@ class TestShiftRequest(unittest.TestCase): shift_request.cancel() shift_assignment_docstatus = frappe.db.get_value( - "Shift Assignment", - filters={"shift_request": shift_request.name}, - fieldname="docstatus" + "Shift Assignment", filters={"shift_request": shift_request.name}, fieldname="docstatus" ) self.assertEqual(shift_assignment_docstatus, 2) @@ -62,7 +64,10 @@ class TestShiftRequest(unittest.TestCase): shift_request.reload() department = frappe.get_value("Employee", "_T-Employee-00001", "department") set_shift_approver(department) - department_approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] + department_approver = frappe.db.sql( + """select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", + (department), + )[0][0] shift_request.approver = department_approver shift_request.save() self.assertTrue(shift_request.name not in frappe.share.get_shared("Shift Request", user)) @@ -85,22 +90,25 @@ class TestShiftRequest(unittest.TestCase): def set_shift_approver(department): department_doc = frappe.get_doc("Department", department) - department_doc.append('shift_request_approver',{'approver': "test1@example.com"}) + department_doc.append("shift_request_approver", {"approver": "test1@example.com"}) department_doc.save() department_doc.reload() + def make_shift_request(approver, do_not_submit=0): - shift_request = frappe.get_doc({ - "doctype": "Shift Request", - "shift_type": "Day Shift", - "company": "_Test Company", - "employee": "_T-Employee-00001", - "employee_name": "_Test Employee", - "from_date": nowdate(), - "to_date": add_days(nowdate(), 10), - "approver": approver, - "status": "Approved" - }).insert() + shift_request = frappe.get_doc( + { + "doctype": "Shift Request", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "employee_name": "_Test Employee", + "from_date": nowdate(), + "to_date": add_days(nowdate(), 10), + "approver": approver, + "status": "Approved", + } + ).insert() if do_not_submit: return shift_request diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 562a5739d67..3f5cb222bfa 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -24,20 +24,45 @@ from erpnext.hr.doctype.shift_assignment.shift_assignment import ( class ShiftType(Document): @frappe.whitelist() def process_auto_attendance(self): - if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin: + if ( + not cint(self.enable_auto_attendance) + or not self.process_attendance_after + or not self.last_sync_of_checkin + ): return filters = { - 'skip_auto_attendance':'0', - 'attendance':('is', 'not set'), - 'time':('>=', self.process_attendance_after), - 'shift_actual_end': ('<', self.last_sync_of_checkin), - 'shift': self.name + "skip_auto_attendance": "0", + "attendance": ("is", "not set"), + "time": (">=", self.process_attendance_after), + "shift_actual_end": ("<", self.last_sync_of_checkin), + "shift": self.name, } - logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") - for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])): + logs = frappe.db.get_list( + "Employee Checkin", fields="*", filters=filters, order_by="employee,time" + ) + for key, group in itertools.groupby( + logs, key=lambda x: (x["employee"], x["shift_actual_start"]) + ): single_shift_logs = list(group) - attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs) - mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name) + ( + attendance_status, + working_hours, + late_entry, + early_exit, + in_time, + out_time, + ) = self.get_attendance(single_shift_logs) + mark_attendance_and_link_log( + single_shift_logs, + attendance_status, + key[1].date(), + working_hours, + late_entry, + early_exit, + in_time, + out_time, + self.name, + ) for employee in self.get_assigned_employee(self.process_attendance_after, True): self.mark_absent_for_dates_with_no_attendance(employee) @@ -45,36 +70,66 @@ class ShiftType(Document): """Return attendance_status, working_hours, late_entry, early_exit, in_time, out_time for a set of logs belonging to a single shift. Assumtion: - 1. These logs belongs to an single shift, single employee and is not in a holiday date. - 2. Logs are in chronological order + 1. These logs belongs to an single shift, single employee and is not in a holiday date. + 2. Logs are in chronological order """ late_entry = early_exit = False - total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on) - if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)): + total_working_hours, in_time, out_time = calculate_working_hours( + logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on + ) + if ( + cint(self.enable_entry_grace_period) + and in_time + and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)) + ): late_entry = True - if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)): + if ( + cint(self.enable_exit_grace_period) + and out_time + and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)) + ): early_exit = True - if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: - return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time - if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day: - return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time - return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time + if ( + self.working_hours_threshold_for_absent + and total_working_hours < self.working_hours_threshold_for_absent + ): + return "Absent", total_working_hours, late_entry, early_exit, in_time, out_time + if ( + self.working_hours_threshold_for_half_day + and total_working_hours < self.working_hours_threshold_for_half_day + ): + return "Half Day", total_working_hours, late_entry, early_exit, in_time, out_time + return "Present", total_working_hours, late_entry, early_exit, in_time, out_time def mark_absent_for_dates_with_no_attendance(self, employee): """Marks Absents for the given employee on working days in this shift which have no attendance marked. The Absent is marked starting from 'process_attendance_after' or employee creation date. """ - date_of_joining, relieving_date, employee_creation = frappe.db.get_value("Employee", employee, ["date_of_joining", "relieving_date", "creation"]) + date_of_joining, relieving_date, employee_creation = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date", "creation"] + ) if not date_of_joining: date_of_joining = employee_creation.date() start_date = max(getdate(self.process_attendance_after), date_of_joining) - actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, get_datetime(self.last_sync_of_checkin), True) - last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else get_datetime(self.last_sync_of_checkin) - prev_shift = get_employee_shift(employee, last_shift_time.date()-timedelta(days=1), True, 'reverse') + actual_shift_datetime = get_actual_start_end_datetime_of_shift( + employee, get_datetime(self.last_sync_of_checkin), True + ) + last_shift_time = ( + actual_shift_datetime[0] + if actual_shift_datetime[0] + else get_datetime(self.last_sync_of_checkin) + ) + prev_shift = get_employee_shift( + employee, last_shift_time.date() - timedelta(days=1), True, "reverse" + ) if prev_shift: - end_date = min(prev_shift.start_datetime.date(), relieving_date) if relieving_date else prev_shift.start_datetime.date() + end_date = ( + min(prev_shift.start_datetime.date(), relieving_date) + if relieving_date + else prev_shift.start_datetime.date() + ) else: return holiday_list_name = self.holiday_list @@ -84,37 +139,40 @@ class ShiftType(Document): for date in dates: shift_details = get_employee_shift(employee, date, True) if shift_details and shift_details.shift_type.name == self.name: - mark_attendance(employee, date, 'Absent', self.name) + mark_attendance(employee, date, "Absent", self.name) def get_assigned_employee(self, from_date=None, consider_default_shift=False): - filters = {'start_date':('>', from_date), 'shift_type': self.name, 'docstatus': '1'} + filters = {"start_date": (">", from_date), "shift_type": self.name, "docstatus": "1"} if not from_date: del filters["start_date"] - assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True) + assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True) assigned_employees = [x[0] for x in assigned_employees] if consider_default_shift: - filters = {'default_shift': self.name, 'status': ['!=', 'Inactive']} - default_shift_employees = frappe.get_all('Employee', 'name', filters, as_list=True) + filters = {"default_shift": self.name, "status": ["!=", "Inactive"]} + default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True) default_shift_employees = [x[0] for x in default_shift_employees] - return list(set(assigned_employees+default_shift_employees)) + return list(set(assigned_employees + default_shift_employees)) return assigned_employees + def process_auto_attendance_for_all_shifts(): - shift_list = frappe.get_all('Shift Type', 'name', {'enable_auto_attendance':'1'}, as_list=True) + shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True) for shift in shift_list: - doc = frappe.get_doc('Shift Type', shift[0]) + doc = frappe.get_doc("Shift Type", shift[0]) doc.process_auto_attendance() -def get_filtered_date_list(employee, start_date, end_date, filter_attendance=True, holiday_list=None): - """Returns a list of dates after removing the dates with attendance and holidays - """ + +def get_filtered_date_list( + employee, start_date, end_date, filter_attendance=True, holiday_list=None +): + """Returns a list of dates after removing the dates with attendance and holidays""" base_dates_query = """select adddate(%(start_date)s, t2.i*100 + t1.i*10 + t0.i) selected_date from (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0, (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1, (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2""" - condition_query = '' + condition_query = "" if filter_attendance: condition_query += """ and a.selected_date not in ( select attendance_date from `tabAttendance` @@ -126,10 +184,20 @@ def get_filtered_date_list(employee, start_date, end_date, filter_attendance=Tru parentfield = 'holidays' and parent = %(holiday_list)s and holiday_date between %(start_date)s and %(end_date)s)""" - dates = frappe.db.sql("""select * from + dates = frappe.db.sql( + """select * from ({base_dates_query}) as a where a.selected_date <= %(end_date)s {condition_query} - """.format(base_dates_query=base_dates_query, condition_query=condition_query), - {"employee":employee, "start_date":start_date, "end_date":end_date, "holiday_list":holiday_list}, as_list=True) + """.format( + base_dates_query=base_dates_query, condition_query=condition_query + ), + { + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "holiday_list": holiday_list, + }, + as_list=True, + ) return [getdate(date[0]) for date in dates] diff --git a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py index b523f0e01ce..920d8fd5475 100644 --- a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py +++ b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py @@ -1,15 +1,8 @@ - - def get_data(): return { - 'fieldname': 'shift', - 'non_standard_fieldnames': { - 'Shift Request': 'shift_type', - 'Shift Assignment': 'shift_type' - }, - 'transactions': [ - { - 'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment'] - } - ] + "fieldname": "shift", + "non_standard_fieldnames": {"Shift Request": "shift_type", "Shift Assignment": "shift_type"}, + "transactions": [ + {"items": ["Attendance", "Employee Checkin", "Shift Request", "Shift Assignment"]} + ], } diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 7b2ea215ad8..93a493c9d25 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -9,8 +9,13 @@ from frappe.utils import cint, flt, getdate, nowdate from frappe.utils.nestedset import get_descendants_of -class SubsidiaryCompanyError(frappe.ValidationError): pass -class ParentCompanyError(frappe.ValidationError): pass +class SubsidiaryCompanyError(frappe.ValidationError): + pass + + +class ParentCompanyError(frappe.ValidationError): + pass + class StaffingPlan(Document): def validate(self): @@ -33,11 +38,11 @@ class StaffingPlan(Document): self.total_estimated_budget = 0 for detail in self.get("staffing_details"): - #Set readonly fields + # Set readonly fields self.set_number_of_positions(detail) designation_counts = get_designation_counts(detail.designation, self.company) - detail.current_count = designation_counts['employee_count'] - detail.current_openings = designation_counts['job_openings'] + detail.current_count = designation_counts["employee_count"] + detail.current_openings = designation_counts["job_openings"] detail.total_estimated_cost = 0 if detail.number_of_positions > 0: @@ -52,80 +57,122 @@ class StaffingPlan(Document): def validate_overlap(self, staffing_plan_detail): # Validate if any submitted Staffing Plan exist for any Designations in this plan # and spd.vacancies>0 ? - overlap = frappe.db.sql("""select spd.parent + overlap = frappe.db.sql( + """select spd.parent from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where spd.designation=%s and sp.docstatus=1 and sp.to_date >= %s and sp.from_date <= %s and sp.company = %s - """, (staffing_plan_detail.designation, self.from_date, self.to_date, self.company)) - if overlap and overlap [0][0]: - frappe.throw(_("Staffing Plan {0} already exist for designation {1}") - .format(overlap[0][0], staffing_plan_detail.designation)) + """, + (staffing_plan_detail.designation, self.from_date, self.to_date, self.company), + ) + if overlap and overlap[0][0]: + frappe.throw( + _("Staffing Plan {0} already exist for designation {1}").format( + overlap[0][0], staffing_plan_detail.designation + ) + ) def validate_with_parent_plan(self, staffing_plan_detail): - if not frappe.get_cached_value('Company', self.company, "parent_company"): - return # No parent, nothing to validate + if not frappe.get_cached_value("Company", self.company, "parent_company"): + return # No parent, nothing to validate # Get staffing plan applicable for the company (Parent Company) - parent_plan_details = get_active_staffing_plan_details(self.company, staffing_plan_detail.designation, self.from_date, self.to_date) + parent_plan_details = get_active_staffing_plan_details( + self.company, staffing_plan_detail.designation, self.from_date, self.to_date + ) if not parent_plan_details: - return #no staffing plan for any parent Company in hierarchy + return # no staffing plan for any parent Company in hierarchy # Fetch parent company which owns the staffing plan. NOTE: Parent could be higher up in the hierarchy parent_company = frappe.db.get_value("Staffing Plan", parent_plan_details[0].name, "company") # Parent plan available, validate with parent, siblings as well as children of staffing plan Company - if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ - flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): - frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ - for {2} as per staffing plan {3} for parent company {4}.").format( + if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or flt( + staffing_plan_detail.total_estimated_cost + ) > flt(parent_plan_details[0].total_estimated_cost): + frappe.throw( + _( + "You can only plan for upto {0} vacancies and budget {1} \ + for {2} as per staffing plan {3} for parent company {4}." + ).format( cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_plan_details[0].name, - parent_company), ParentCompanyError) + parent_company, + ), + ParentCompanyError, + ) - #Get vacanices already planned for all companies down the hierarchy of Parent Company - lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) - all_sibling_details = frappe.db.sql("""select sum(spd.vacancies) as vacancies, + # Get vacanices already planned for all companies down the hierarchy of Parent Company + lft, rgt = frappe.get_cached_value("Company", parent_company, ["lft", "rgt"]) + all_sibling_details = frappe.db.sql( + """select sum(spd.vacancies) as vacancies, sum(spd.total_estimated_cost) as total_estimated_cost from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where spd.designation=%s and sp.docstatus=1 and sp.to_date >= %s and sp.from_date <=%s and sp.company in (select name from tabCompany where lft > %s and rgt < %s) - """, (staffing_plan_detail.designation, self.from_date, self.to_date, lft, rgt), as_dict = 1)[0] + """, + (staffing_plan_detail.designation, self.from_date, self.to_date, lft, rgt), + as_dict=1, + )[0] - if (cint(parent_plan_details[0].vacancies) < \ - (cint(staffing_plan_detail.vacancies) + cint(all_sibling_details.vacancies))) or \ - (flt(parent_plan_details[0].total_estimated_cost) < \ - (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): - frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ - You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format( + if ( + cint(parent_plan_details[0].vacancies) + < (cint(staffing_plan_detail.vacancies) + cint(all_sibling_details.vacancies)) + ) or ( + flt(parent_plan_details[0].total_estimated_cost) + < ( + flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost) + ) + ): + frappe.throw( + _( + "{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ + You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." + ).format( cint(all_sibling_details.vacancies), all_sibling_details.total_estimated_cost, frappe.bold(staffing_plan_detail.designation), parent_company, cint(parent_plan_details[0].vacancies), parent_plan_details[0].total_estimated_cost, - parent_plan_details[0].name)) + parent_plan_details[0].name, + ) + ) def validate_with_subsidiary_plans(self, staffing_plan_detail): - #Valdate this plan with all child company plan - children_details = frappe.db.sql("""select sum(spd.vacancies) as vacancies, + # Valdate this plan with all child company plan + children_details = frappe.db.sql( + """select sum(spd.vacancies) as vacancies, sum(spd.total_estimated_cost) as total_estimated_cost from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where spd.designation=%s and sp.docstatus=1 and sp.to_date >= %s and sp.from_date <=%s and sp.company in (select name from tabCompany where parent_company = %s) - """, (staffing_plan_detail.designation, self.from_date, self.to_date, self.company), as_dict = 1)[0] + """, + (staffing_plan_detail.designation, self.from_date, self.to_date, self.company), + as_dict=1, + )[0] - if children_details and \ - cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ - flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): - frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ - Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format( + if ( + children_details + and cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) + or flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost) + ): + frappe.throw( + _( + "Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ + Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" + ).format( self.company, cint(children_details.vacancies), children_details.total_estimated_cost, - frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError) + frappe.bold(staffing_plan_detail.designation), + ), + SubsidiaryCompanyError, + ) + @frappe.whitelist() def get_designation_counts(designation, company): @@ -133,25 +180,24 @@ def get_designation_counts(designation, company): return False employee_counts = {} - company_set = get_descendants_of('Company', company) + company_set = get_descendants_of("Company", company) company_set.append(company) - employee_counts["employee_count"] = frappe.db.get_value("Employee", - filters={ - 'designation': designation, - 'status': 'Active', - 'company': ('in', company_set) - }, fieldname=['count(name)']) + employee_counts["employee_count"] = frappe.db.get_value( + "Employee", + filters={"designation": designation, "status": "Active", "company": ("in", company_set)}, + fieldname=["count(name)"], + ) - employee_counts['job_openings'] = frappe.db.get_value("Job Opening", - filters={ - 'designation': designation, - 'status': 'Open', - 'company': ('in', company_set) - }, fieldname=['count(name)']) + employee_counts["job_openings"] = frappe.db.get_value( + "Job Opening", + filters={"designation": designation, "status": "Open", "company": ("in", company_set)}, + fieldname=["count(name)"], + ) return employee_counts + @frappe.whitelist() def get_active_staffing_plan_details(company, designation, from_date=None, to_date=None): if from_date is None: @@ -161,17 +207,22 @@ def get_active_staffing_plan_details(company, designation, from_date=None, to_da if not company or not designation: frappe.throw(_("Please select Company and Designation")) - staffing_plan = frappe.db.sql(""" + staffing_plan = frappe.db.sql( + """ select sp.name, spd.vacancies, spd.total_estimated_cost from `tabStaffing Plan Detail` spd join `tabStaffing Plan` sp on spd.parent=sp.name where company=%s and spd.designation=%s and sp.docstatus=1 - and to_date >= %s and from_date <= %s """, (company, designation, from_date, to_date), as_dict = 1) + and to_date >= %s and from_date <= %s """, + (company, designation, from_date, to_date), + as_dict=1, + ) if not staffing_plan: - parent_company = frappe.get_cached_value('Company', company, "parent_company") + parent_company = frappe.get_cached_value("Company", company, "parent_company") if parent_company: - staffing_plan = get_active_staffing_plan_details(parent_company, - designation, from_date, to_date) + staffing_plan = get_active_staffing_plan_details( + parent_company, designation, from_date, to_date + ) # Only a single staffing plan can be active for a designation on given date return staffing_plan if staffing_plan else None diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py index c04e5853a52..0f555d9db2d 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): - return { - 'fieldname': 'staffing_plan', - 'transactions': [ - { - 'items': ['Job Opening'] - } - ], - } + return { + "fieldname": "staffing_plan", + "transactions": [{"items": ["Job Opening"]}], + } diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py index 8ff0dbbc28e..a3adbbd56a5 100644 --- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py @@ -13,6 +13,7 @@ from erpnext.hr.doctype.staffing_plan.staffing_plan import ( test_dependencies = ["Designation"] + class TestStaffingPlan(unittest.TestCase): def test_staffing_plan(self): _set_up() @@ -24,11 +25,10 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 6, - "estimated_cost_per_position": 50000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 6, "estimated_cost_per_position": 50000}, + ) staffing_plan.insert() staffing_plan.submit() self.assertEqual(staffing_plan.total_estimated_budget, 300000.00) @@ -42,11 +42,10 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test 1" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 3, - "estimated_cost_per_position": 45000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 3, "estimated_cost_per_position": 45000}, + ) self.assertRaises(SubsidiaryCompanyError, staffing_plan.insert) def test_staffing_plan_parent_company(self): @@ -58,11 +57,10 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 7, - "estimated_cost_per_position": 50000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 7, "estimated_cost_per_position": 50000}, + ) staffing_plan.insert() staffing_plan.submit() self.assertEqual(staffing_plan.total_estimated_budget, 350000.00) @@ -73,19 +71,20 @@ class TestStaffingPlan(unittest.TestCase): staffing_plan.name = "Test 1" staffing_plan.from_date = nowdate() staffing_plan.to_date = add_days(nowdate(), 10) - staffing_plan.append("staffing_details", { - "designation": "Designer", - "vacancies": 7, - "estimated_cost_per_position": 60000 - }) + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 7, "estimated_cost_per_position": 60000}, + ) staffing_plan.insert() self.assertRaises(ParentCompanyError, staffing_plan.submit) + def _set_up(): for doctype in ["Staffing Plan", "Staffing Plan Detail"]: frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) make_company() + def make_company(): if frappe.db.exists("Company", "_Test Company 10"): return diff --git a/erpnext/hr/doctype/training_event/test_training_event.py b/erpnext/hr/doctype/training_event/test_training_event.py index f4329c9fe70..ec7eb74da9e 100644 --- a/erpnext/hr/doctype/training_event/test_training_event.py +++ b/erpnext/hr/doctype/training_event/test_training_event.py @@ -14,10 +14,7 @@ class TestTrainingEvent(unittest.TestCase): create_training_program("Basic Training") employee = make_employee("robert_loan@trainig.com") employee2 = make_employee("suzie.tan@trainig.com") - self.attendees = [ - {"employee": employee}, - {"employee": employee2} - ] + self.attendees = [{"employee": employee}, {"employee": employee2}] def test_training_event_status_update(self): training_event = create_training_event(self.attendees) @@ -43,20 +40,25 @@ class TestTrainingEvent(unittest.TestCase): def create_training_program(training_program): if not frappe.db.get_value("Training Program", training_program): - frappe.get_doc({ - "doctype": "Training Program", - "training_program": training_program, - "description": training_program - }).insert() + frappe.get_doc( + { + "doctype": "Training Program", + "training_program": training_program, + "description": training_program, + } + ).insert() + def create_training_event(attendees): - return frappe.get_doc({ - "doctype": "Training Event", - "event_name": "Basic Training Event", - "training_program": "Basic Training", - "location": "Union Square", - "start_time": add_days(today(), 5), - "end_time": add_days(today(), 6), - "introduction": "Welcome to the Basic Training Event", - "employees": attendees - }).insert() + return frappe.get_doc( + { + "doctype": "Training Event", + "event_name": "Basic Training Event", + "training_program": "Basic Training", + "location": "Union Square", + "start_time": add_days(today(), 5), + "end_time": add_days(today(), 6), + "introduction": "Welcome to the Basic Training Event", + "employees": attendees, + } + ).insert() diff --git a/erpnext/hr/doctype/training_event/training_event.py b/erpnext/hr/doctype/training_event/training_event.py index c8c8bbe7339..59972bb2f3f 100644 --- a/erpnext/hr/doctype/training_event/training_event.py +++ b/erpnext/hr/doctype/training_event/training_event.py @@ -19,21 +19,20 @@ class TrainingEvent(Document): self.set_status_for_attendees() def set_employee_emails(self): - self.employee_emails = ', '.join(get_employee_emails([d.employee - for d in self.employees])) + self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees])) def validate_period(self): if time_diff_in_seconds(self.end_time, self.start_time) <= 0: - frappe.throw(_('End time cannot be before start time')) + frappe.throw(_("End time cannot be before start time")) def set_status_for_attendees(self): - if self.event_status == 'Completed': + if self.event_status == "Completed": for employee in self.employees: - if employee.attendance == 'Present' and employee.status != 'Feedback Submitted': - employee.status = 'Completed' + if employee.attendance == "Present" and employee.status != "Feedback Submitted": + employee.status = "Completed" - elif self.event_status == 'Scheduled': + elif self.event_status == "Scheduled": for employee in self.employees: - employee.status = 'Open' + employee.status = "Open" self.db_update_all() diff --git a/erpnext/hr/doctype/training_event/training_event_dashboard.py b/erpnext/hr/doctype/training_event/training_event_dashboard.py index 8c4162d5010..ca13938e585 100644 --- a/erpnext/hr/doctype/training_event/training_event_dashboard.py +++ b/erpnext/hr/doctype/training_event/training_event_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'training_event', - 'transactions': [ - { - 'items': ['Training Result', 'Training Feedback'] - }, - ], - } + return { + "fieldname": "training_event", + "transactions": [ + {"items": ["Training Result", "Training Feedback"]}, + ], + } diff --git a/erpnext/hr/doctype/training_feedback/test_training_feedback.py b/erpnext/hr/doctype/training_feedback/test_training_feedback.py index 58ed6231003..c787b7038fe 100644 --- a/erpnext/hr/doctype/training_feedback/test_training_feedback.py +++ b/erpnext/hr/doctype/training_feedback/test_training_feedback.py @@ -32,10 +32,9 @@ class TestTrainingFeedback(unittest.TestCase): self.assertRaises(frappe.ValidationError, feedback.save) # cannot record feedback for absent employee - employee = frappe.db.get_value("Training Event Employee", { - "parent": training_event.name, - "employee": self.employee - }, "name") + employee = frappe.db.get_value( + "Training Event Employee", {"parent": training_event.name, "employee": self.employee}, "name" + ) frappe.db.set_value("Training Event Employee", employee, "attendance", "Absent") feedback = create_training_feedback(training_event.name, self.employee) @@ -52,10 +51,9 @@ class TestTrainingFeedback(unittest.TestCase): feedback = create_training_feedback(training_event.name, self.employee) feedback.submit() - status = frappe.db.get_value("Training Event Employee", { - "parent": training_event.name, - "employee": self.employee - }, "status") + status = frappe.db.get_value( + "Training Event Employee", {"parent": training_event.name, "employee": self.employee}, "status" + ) self.assertEqual(status, "Feedback Submitted") @@ -64,9 +62,11 @@ class TestTrainingFeedback(unittest.TestCase): def create_training_feedback(event, employee): - return frappe.get_doc({ - "doctype": "Training Feedback", - "training_event": event, - "employee": employee, - "feedback": "Test" - }) + return frappe.get_doc( + { + "doctype": "Training Feedback", + "training_event": event, + "employee": employee, + "feedback": "Test", + } + ) diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.py b/erpnext/hr/doctype/training_feedback/training_feedback.py index 1f9ec3b0b8a..d5de28ed2d5 100644 --- a/erpnext/hr/doctype/training_feedback/training_feedback.py +++ b/erpnext/hr/doctype/training_feedback/training_feedback.py @@ -13,32 +13,35 @@ class TrainingFeedback(Document): if training_event.docstatus != 1: frappe.throw(_("{0} must be submitted").format(_("Training Event"))) - emp_event_details = frappe.db.get_value("Training Event Employee", { - "parent": self.training_event, - "employee": self.employee - }, ["name", "attendance"], as_dict=True) + emp_event_details = frappe.db.get_value( + "Training Event Employee", + {"parent": self.training_event, "employee": self.employee}, + ["name", "attendance"], + as_dict=True, + ) if not emp_event_details: - frappe.throw(_("Employee {0} not found in Training Event Participants.").format( - frappe.bold(self.employee_name))) + frappe.throw( + _("Employee {0} not found in Training Event Participants.").format( + frappe.bold(self.employee_name) + ) + ) if emp_event_details.attendance == "Absent": frappe.throw(_("Feedback cannot be recorded for an absent Employee.")) def on_submit(self): - employee = frappe.db.get_value("Training Event Employee", { - "parent": self.training_event, - "employee": self.employee - }) + employee = frappe.db.get_value( + "Training Event Employee", {"parent": self.training_event, "employee": self.employee} + ) if employee: frappe.db.set_value("Training Event Employee", employee, "status", "Feedback Submitted") def on_cancel(self): - employee = frappe.db.get_value("Training Event Employee", { - "parent": self.training_event, - "employee": self.employee - }) + employee = frappe.db.get_value( + "Training Event Employee", {"parent": self.training_event, "employee": self.employee} + ) if employee: frappe.db.set_value("Training Event Employee", employee, "status", "Completed") diff --git a/erpnext/hr/doctype/training_program/training_program_dashboard.py b/erpnext/hr/doctype/training_program/training_program_dashboard.py index 51137d162c7..1735db18e12 100644 --- a/erpnext/hr/doctype/training_program/training_program_dashboard.py +++ b/erpnext/hr/doctype/training_program/training_program_dashboard.py @@ -1,14 +1,10 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'training_program', - 'transactions': [ - { - 'label': _('Training Events'), - 'items': ['Training Event'] - }, - ] + "fieldname": "training_program", + "transactions": [ + {"label": _("Training Events"), "items": ["Training Event"]}, + ], } diff --git a/erpnext/hr/doctype/training_result/test_training_result.py b/erpnext/hr/doctype/training_result/test_training_result.py index 1735ff4e341..136543cbe1a 100644 --- a/erpnext/hr/doctype/training_result/test_training_result.py +++ b/erpnext/hr/doctype/training_result/test_training_result.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Training Result') + class TestTrainingResult(unittest.TestCase): pass diff --git a/erpnext/hr/doctype/training_result/training_result.py b/erpnext/hr/doctype/training_result/training_result.py index bb5c71e7a15..48a5b2c2e9b 100644 --- a/erpnext/hr/doctype/training_result/training_result.py +++ b/erpnext/hr/doctype/training_result/training_result.py @@ -13,22 +13,22 @@ class TrainingResult(Document): def validate(self): training_event = frappe.get_doc("Training Event", self.training_event) if training_event.docstatus != 1: - frappe.throw(_('{0} must be submitted').format(_('Training Event'))) + frappe.throw(_("{0} must be submitted").format(_("Training Event"))) - self.employee_emails = ', '.join(get_employee_emails([d.employee - for d in self.employees])) + self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees])) def on_submit(self): training_event = frappe.get_doc("Training Event", self.training_event) - training_event.status = 'Completed' + training_event.status = "Completed" for e in self.employees: for e1 in training_event.employees: if e1.employee == e.employee: - e1.status = 'Completed' + e1.status = "Completed" break training_event.save() + @frappe.whitelist() def get_employees(training_event): return frappe.get_doc("Training Event", training_event).employees diff --git a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py index 4c7bd805f98..537c20633be 100644 --- a/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/test_upload_attendance.py @@ -10,12 +10,15 @@ import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.upload_attendance.upload_attendance import get_data -test_dependencies = ['Holiday List'] +test_dependencies = ["Holiday List"] + class TestUploadAttendance(unittest.TestCase): @classmethod def setUpClass(cls): - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List') + frappe.db.set_value( + "Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List" + ) def test_date_range(self): employee = make_employee("test_employee@company.com") @@ -27,14 +30,13 @@ class TestUploadAttendance(unittest.TestCase): employee_doc.date_of_joining = date_of_joining employee_doc.relieving_date = relieving_date employee_doc.save() - args = { - "from_date": from_date, - "to_date": to_date - } + args = {"from_date": from_date, "to_date": to_date} data = get_data(args) filtered_data = [] for row in data: if row[1] == employee: filtered_data.append(row) for row in filtered_data: - self.assertTrue(getdate(row[3]) >= getdate(date_of_joining) and getdate(row[3]) <= getdate(relieving_date)) + self.assertTrue( + getdate(row[3]) >= getdate(date_of_joining) and getdate(row[3]) <= getdate(relieving_date) + ) diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index 94eb3001009..a66a48124da 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -17,6 +17,7 @@ from erpnext.hr.utils import get_holiday_dates_for_employee class UploadAttendance(Document): pass + @frappe.whitelist() def get_template(): if not frappe.has_permission("Attendance", "create"): @@ -38,29 +39,37 @@ def get_template(): return # write out response as a type csv - frappe.response['result'] = cstr(w.getvalue()) - frappe.response['type'] = 'csv' - frappe.response['doctype'] = "Attendance" + frappe.response["result"] = cstr(w.getvalue()) + frappe.response["type"] = "csv" + frappe.response["doctype"] = "Attendance" + def add_header(w): - status = ", ".join((frappe.get_meta("Attendance").get_field("status").options or "").strip().split("\n")) + status = ", ".join( + (frappe.get_meta("Attendance").get_field("status").options or "").strip().split("\n") + ) w.writerow(["Notes:"]) w.writerow(["Please do not change the template headings"]) w.writerow(["Status should be one of these values: " + status]) w.writerow(["If you are overwriting existing attendance records, 'ID' column mandatory"]) - w.writerow(["ID", "Employee", "Employee Name", "Date", "Status", "Leave Type", - "Company", "Naming Series"]) + w.writerow( + ["ID", "Employee", "Employee Name", "Date", "Status", "Leave Type", "Company", "Naming Series"] + ) return w + def add_data(w, args): data = get_data(args) writedata(w, data) return w + def get_data(args): dates = get_dates(args) employees = get_active_employees() - holidays = get_holidays_for_employees([employee.name for employee in employees], args["from_date"], args["to_date"]) + holidays = get_holidays_for_employees( + [employee.name for employee in employees], args["from_date"], args["to_date"] + ) existing_attendance_records = get_existing_attendance_records(args) data = [] for date in dates: @@ -71,27 +80,33 @@ def get_data(args): if getdate(date) > getdate(employee.relieving_date): continue existing_attendance = {} - if existing_attendance_records \ - and tuple([getdate(date), employee.name]) in existing_attendance_records \ - and getdate(employee.date_of_joining) <= getdate(date) \ - and getdate(employee.relieving_date) >= getdate(date): - existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])] + if ( + existing_attendance_records + and tuple([getdate(date), employee.name]) in existing_attendance_records + and getdate(employee.date_of_joining) <= getdate(date) + and getdate(employee.relieving_date) >= getdate(date) + ): + existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])] employee_holiday_list = get_holiday_list_for_employee(employee.name) row = [ existing_attendance and existing_attendance.name or "", - employee.name, employee.employee_name, date, + employee.name, + employee.employee_name, + date, existing_attendance and existing_attendance.status or "", - existing_attendance and existing_attendance.leave_type or "", employee.company, + existing_attendance and existing_attendance.leave_type or "", + employee.company, existing_attendance and existing_attendance.naming_series or get_naming_series(), ] if date in holidays[employee_holiday_list]: - row[4] = "Holiday" + row[4] = "Holiday" data.append(row) return data + def get_holidays_for_employees(employees, from_date, to_date): holidays = {} for employee in employees: @@ -102,30 +117,35 @@ def get_holidays_for_employees(employees, from_date, to_date): return holidays + def writedata(w, data): for row in data: w.writerow(row) + def get_dates(args): """get list of dates in between from date and to date""" no_of_days = date_diff(add_days(args["to_date"], 1), args["from_date"]) dates = [add_days(args["from_date"], i) for i in range(0, no_of_days)] return dates + def get_active_employees(): - employees = frappe.db.get_all('Employee', - fields=['name', 'employee_name', 'date_of_joining', 'company', 'relieving_date'], - filters={ - 'docstatus': ['<', 2], - 'status': 'Active' - } + employees = frappe.db.get_all( + "Employee", + fields=["name", "employee_name", "date_of_joining", "company", "relieving_date"], + filters={"docstatus": ["<", 2], "status": "Active"}, ) return employees + def get_existing_attendance_records(args): - attendance = frappe.db.sql("""select name, attendance_date, employee, status, leave_type, naming_series + attendance = frappe.db.sql( + """select name, attendance_date, employee, status, leave_type, naming_series from `tabAttendance` where attendance_date between %s and %s and docstatus < 2""", - (args["from_date"], args["to_date"]), as_dict=1) + (args["from_date"], args["to_date"]), + as_dict=1, + ) existing_attendance = {} for att in attendance: @@ -133,6 +153,7 @@ def get_existing_attendance_records(args): return existing_attendance + def get_naming_series(): series = frappe.get_meta("Attendance").get_field("naming_series").options.strip().split("\n") if not series: @@ -146,15 +167,16 @@ def upload(): raise frappe.PermissionError from frappe.utils.csvutils import read_csv_content + rows = read_csv_content(frappe.local.uploaded_file) if not rows: frappe.throw(_("Please select a csv file")) frappe.enqueue(import_attendances, rows=rows, now=True if len(rows) < 200 else False) -def import_attendances(rows): +def import_attendances(rows): def remove_holidays(rows): - rows = [ row for row in rows if row[4] != "Holiday"] + rows = [row for row in rows if row[4] != "Holiday"] return rows from frappe.modules import scrub @@ -172,7 +194,8 @@ def import_attendances(rows): from frappe.utils.csvutils import check_record, import_doc for i, row in enumerate(rows): - if not row: continue + if not row: + continue row_idx = i + 5 d = frappe._dict(zip(columns, row)) @@ -183,16 +206,12 @@ def import_attendances(rows): try: check_record(d) ret.append(import_doc(d, "Attendance", 1, row_idx, submit=True)) - frappe.publish_realtime('import_attendance', dict( - progress=i, - total=len(rows) - )) + frappe.publish_realtime("import_attendance", dict(progress=i, total=len(rows))) except AttributeError: pass except Exception as e: error = True - ret.append('Error for row (#%d) %s : %s' % (row_idx, - len(row)>1 and row[1] or "", cstr(e))) + ret.append("Error for row (#%d) %s : %s" % (row_idx, len(row) > 1 and row[1] or "", cstr(e))) frappe.errprint(frappe.get_traceback()) if error: @@ -200,7 +219,4 @@ def import_attendances(rows): else: frappe.db.commit() - frappe.publish_realtime('import_attendance', dict( - messages=ret, - error=error - )) + frappe.publish_realtime("import_attendance", dict(messages=ret, error=error)) diff --git a/erpnext/hr/doctype/vehicle/test_vehicle.py b/erpnext/hr/doctype/vehicle/test_vehicle.py index c5ea5a38c86..97fe651122c 100644 --- a/erpnext/hr/doctype/vehicle/test_vehicle.py +++ b/erpnext/hr/doctype/vehicle/test_vehicle.py @@ -8,18 +8,21 @@ from frappe.utils import random_string # test_records = frappe.get_test_records('Vehicle') + class TestVehicle(unittest.TestCase): def test_make_vehicle(self): - vehicle = frappe.get_doc({ - "doctype": "Vehicle", - "license_plate": random_string(10).upper(), - "make": "Maruti", - "model": "PCM", - "last_odometer":5000, - "acquisition_date":frappe.utils.nowdate(), - "location": "Mumbai", - "chassis_no": "1234ABCD", - "uom": "Litre", - "vehicle_value":frappe.utils.flt(500000) - }) + vehicle = frappe.get_doc( + { + "doctype": "Vehicle", + "license_plate": random_string(10).upper(), + "make": "Maruti", + "model": "PCM", + "last_odometer": 5000, + "acquisition_date": frappe.utils.nowdate(), + "location": "Mumbai", + "chassis_no": "1234ABCD", + "uom": "Litre", + "vehicle_value": frappe.utils.flt(500000), + } + ) vehicle.insert() diff --git a/erpnext/hr/doctype/vehicle/vehicle.py b/erpnext/hr/doctype/vehicle/vehicle.py index 946233b5481..22c14c37278 100644 --- a/erpnext/hr/doctype/vehicle/vehicle.py +++ b/erpnext/hr/doctype/vehicle/vehicle.py @@ -15,9 +15,15 @@ class Vehicle(Document): if getdate(self.carbon_check_date) > getdate(): frappe.throw(_("Last carbon check date cannot be a future date")) + def get_timeline_data(doctype, name): - '''Return timeline for vehicle log''' - return dict(frappe.db.sql('''select unix_timestamp(date), count(*) + """Return timeline for vehicle log""" + return dict( + frappe.db.sql( + """select unix_timestamp(date), count(*) from `tabVehicle Log` where license_plate=%s and date > date_sub(curdate(), interval 1 year) - group by date''', name)) + group by date""", + name, + ) + ) diff --git a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py index bb38ab9d6b5..758dfbd60a4 100644 --- a/erpnext/hr/doctype/vehicle/vehicle_dashboard.py +++ b/erpnext/hr/doctype/vehicle/vehicle_dashboard.py @@ -1,21 +1,13 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on logs against this Vehicle. See timeline below for details'), - 'fieldname': 'license_plate', - 'non_standard_fieldnames':{ - 'Delivery Trip': 'vehicle' - }, - 'transactions': [ - { - 'items': ['Vehicle Log'] - }, - { - 'items': ['Delivery Trip'] - } - ] + "heatmap": True, + "heatmap_message": _( + "This is based on logs against this Vehicle. See timeline below for details" + ), + "fieldname": "license_plate", + "non_standard_fieldnames": {"Delivery Trip": "vehicle"}, + "transactions": [{"items": ["Vehicle Log"]}, {"items": ["Delivery Trip"]}], } diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py index acd50f278cd..7c6fd8cb212 100644 --- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py @@ -12,7 +12,9 @@ from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim class TestVehicleLog(unittest.TestCase): def setUp(self): - employee_id = frappe.db.sql("""select name from `tabEmployee` where name='testdriver@example.com'""") + employee_id = frappe.db.sql( + """select name from `tabEmployee` where name='testdriver@example.com'""" + ) self.employee_id = employee_id[0][0] if employee_id else None if not self.employee_id: @@ -27,11 +29,11 @@ class TestVehicleLog(unittest.TestCase): def test_make_vehicle_log_and_syncing_of_odometer_value(self): vehicle_log = make_vehicle_log(self.license_plate, self.employee_id) - #checking value of vehicle odometer value on submit. + # checking value of vehicle odometer value on submit. vehicle = frappe.get_doc("Vehicle", self.license_plate) self.assertEqual(vehicle.last_odometer, vehicle_log.odometer) - #checking value vehicle odometer on vehicle log cancellation. + # checking value vehicle odometer on vehicle log cancellation. last_odometer = vehicle_log.last_odometer current_odometer = vehicle_log.odometer distance_travelled = current_odometer - last_odometer @@ -48,7 +50,7 @@ class TestVehicleLog(unittest.TestCase): expense_claim = make_expense_claim(vehicle_log.name) fuel_expense = expense_claim.expenses[0].amount - self.assertEqual(fuel_expense, 50*500) + self.assertEqual(fuel_expense, 50 * 500) vehicle_log.cancel() frappe.delete_doc("Expense Claim", expense_claim.name) @@ -67,8 +69,9 @@ class TestVehicleLog(unittest.TestCase): def get_vehicle(employee_id): - license_plate=random_string(10).upper() - vehicle = frappe.get_doc({ + license_plate = random_string(10).upper() + vehicle = frappe.get_doc( + { "doctype": "Vehicle", "license_plate": cstr(license_plate), "make": "Maruti", @@ -79,8 +82,9 @@ def get_vehicle(employee_id): "location": "Mumbai", "chassis_no": "1234ABCD", "uom": "Litre", - "vehicle_value": flt(500000) - }) + "vehicle_value": flt(500000), + } + ) try: vehicle.insert() except frappe.DuplicateEntryError: @@ -89,29 +93,37 @@ def get_vehicle(employee_id): def make_vehicle_log(license_plate, employee_id, with_services=False): - vehicle_log = frappe.get_doc({ - "doctype": "Vehicle Log", - "license_plate": cstr(license_plate), - "employee": employee_id, - "date": nowdate(), - "odometer": 5010, - "fuel_qty": flt(50), - "price": flt(500) - }) + vehicle_log = frappe.get_doc( + { + "doctype": "Vehicle Log", + "license_plate": cstr(license_plate), + "employee": employee_id, + "date": nowdate(), + "odometer": 5010, + "fuel_qty": flt(50), + "price": flt(500), + } + ) if with_services: - vehicle_log.append("service_detail", { - "service_item": "Oil Change", - "type": "Inspection", - "frequency": "Mileage", - "expense_amount": flt(500) - }) - vehicle_log.append("service_detail", { - "service_item": "Wheels", - "type": "Change", - "frequency": "Half Yearly", - "expense_amount": flt(1500) - }) + vehicle_log.append( + "service_detail", + { + "service_item": "Oil Change", + "type": "Inspection", + "frequency": "Mileage", + "expense_amount": flt(500), + }, + ) + vehicle_log.append( + "service_detail", + { + "service_item": "Wheels", + "type": "Change", + "frequency": "Half Yearly", + "expense_amount": flt(1500), + }, + ) vehicle_log.save() vehicle_log.submit() diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.py b/erpnext/hr/doctype/vehicle_log/vehicle_log.py index e414141efb5..2c1d9a4efec 100644 --- a/erpnext/hr/doctype/vehicle_log/vehicle_log.py +++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.py @@ -11,17 +11,24 @@ from frappe.utils import flt class VehicleLog(Document): def validate(self): if flt(self.odometer) < flt(self.last_odometer): - frappe.throw(_("Current Odometer Value should be greater than Last Odometer Value {0}").format(self.last_odometer)) + frappe.throw( + _("Current Odometer Value should be greater than Last Odometer Value {0}").format( + self.last_odometer + ) + ) def on_submit(self): frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", self.odometer) def on_cancel(self): distance_travelled = self.odometer - self.last_odometer - if(distance_travelled > 0): - updated_odometer_value = int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled + if distance_travelled > 0: + updated_odometer_value = ( + int(frappe.db.get_value("Vehicle", self.license_plate, "last_odometer")) - distance_travelled + ) frappe.db.set_value("Vehicle", self.license_plate, "last_odometer", updated_odometer_value) + @frappe.whitelist() def make_expense_claim(docname): expense_claim = frappe.db.exists("Expense Claim", {"vehicle_log": docname}) @@ -39,9 +46,8 @@ def make_expense_claim(docname): exp_claim.employee = vehicle_log.employee exp_claim.vehicle_log = vehicle_log.name exp_claim.remark = _("Expense Claim for Vehicle Log {0}").format(vehicle_log.name) - exp_claim.append("expenses", { - "expense_date": vehicle_log.date, - "description": _("Vehicle Expenses"), - "amount": claim_amount - }) + exp_claim.append( + "expenses", + {"expense_date": vehicle_log.date, "description": _("Vehicle Expenses"), "amount": claim_amount}, + ) return exp_claim.as_dict() diff --git a/erpnext/hr/notification/training_feedback/training_feedback.py b/erpnext/hr/notification/training_feedback/training_feedback.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/hr/notification/training_feedback/training_feedback.py +++ b/erpnext/hr/notification/training_feedback/training_feedback.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.py b/erpnext/hr/notification/training_scheduled/training_scheduled.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.py +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py index 01d95a7051a..3674912dc06 100644 --- a/erpnext/hr/page/organizational_chart/organizational_chart.py +++ b/erpnext/hr/page/organizational_chart/organizational_chart.py @@ -1,29 +1,29 @@ - import frappe @frappe.whitelist() def get_children(parent=None, company=None, exclude_node=None): - filters = [['status', '!=', 'Left']] - if company and company != 'All Companies': - filters.append(['company', '=', company]) + filters = [["status", "!=", "Left"]] + if company and company != "All Companies": + filters.append(["company", "=", company]) if parent and company and parent != company: - filters.append(['reports_to', '=', parent]) + filters.append(["reports_to", "=", parent]) else: - filters.append(['reports_to', '=', '']) + filters.append(["reports_to", "=", ""]) if exclude_node: - filters.append(['name', '!=', exclude_node]) + filters.append(["name", "!=", exclude_node]) - employees = frappe.get_list('Employee', - fields=['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'], + employees = frappe.get_list( + "Employee", + fields=["employee_name as name", "name as id", "reports_to", "image", "designation as title"], filters=filters, - order_by='name' + order_by="name", ) for employee in employees: - is_expandable = frappe.db.count('Employee', filters={'reports_to': employee.get('id')}) + is_expandable = frappe.db.count("Employee", filters={"reports_to": employee.get("id")}) employee.connections = get_connections(employee.id) employee.expandable = 1 if is_expandable else 0 @@ -33,16 +33,12 @@ def get_children(parent=None, company=None, exclude_node=None): def get_connections(employee): num_connections = 0 - nodes_to_expand = frappe.get_list('Employee', filters=[ - ['reports_to', '=', employee] - ]) + nodes_to_expand = frappe.get_list("Employee", filters=[["reports_to", "=", employee]]) num_connections += len(nodes_to_expand) while nodes_to_expand: parent = nodes_to_expand.pop(0) - descendants = frappe.get_list('Employee', filters=[ - ['reports_to', '=', parent.name] - ]) + descendants = frappe.get_list("Employee", filters=[["reports_to", "=", parent.name]]) num_connections += len(descendants) nodes_to_expand.extend(descendants) diff --git a/erpnext/hr/page/team_updates/team_updates.py b/erpnext/hr/page/team_updates/team_updates.py index 126ed898c97..c1fcb735850 100644 --- a/erpnext/hr/page/team_updates/team_updates.py +++ b/erpnext/hr/page/team_updates/team_updates.py @@ -1,19 +1,23 @@ - import frappe from email_reply_parser import EmailReplyParser @frappe.whitelist() def get_data(start=0): - #frappe.only_for('Employee', 'System Manager') - data = frappe.get_all('Communication', - fields=('content', 'text_content', 'sender', 'creation'), - filters=dict(reference_doctype='Daily Work Summary'), - order_by='creation desc', limit=40, start=start) + # frappe.only_for('Employee', 'System Manager') + data = frappe.get_all( + "Communication", + fields=("content", "text_content", "sender", "creation"), + filters=dict(reference_doctype="Daily Work Summary"), + order_by="creation desc", + limit=40, + start=start, + ) for d in data: - d.sender_name = frappe.db.get_value("Employee", {"user_id": d.sender}, - "employee_name") or d.sender + d.sender_name = ( + frappe.db.get_value("Employee", {"user_id": d.sender}, "employee_name") or d.sender + ) if d.text_content: d.content = frappe.utils.md_to_html(EmailReplyParser.parse_reply(d.text_content)) diff --git a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py index 63764bb8a9c..d93688a4925 100644 --- a/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py +++ b/erpnext/hr/report/daily_work_summary_replies/daily_work_summary_replies.py @@ -9,51 +9,53 @@ from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_user_em def execute(filters=None): - if not filters.group: return [], [] + if not filters.group: + return [], [] columns, data = get_columns(), get_data(filters) return columns, data + def get_columns(filters=None): columns = [ - { - "label": _("User"), - "fieldname": "user", - "fieldtype": "Data", - "width": 300 - }, + {"label": _("User"), "fieldname": "user", "fieldtype": "Data", "width": 300}, { "label": _("Replies"), "fieldname": "count", "fieldtype": "data", "width": 100, - "align": 'right', + "align": "right", }, { "label": _("Total"), "fieldname": "total", "fieldtype": "data", "width": 100, - "align": 'right', - } + "align": "right", + }, ] return columns + def get_data(filters): - daily_summary_emails = frappe.get_all('Daily Work Summary', - fields=["name"], - filters=[["creation","Between", filters.range]]) - daily_summary_emails = [d.get('name') for d in daily_summary_emails] - replies = frappe.get_all('Communication', - fields=['content', 'text_content', 'sender'], - filters=[['reference_doctype','=', 'Daily Work Summary'], - ['reference_name', 'in', daily_summary_emails], - ['communication_type', '=', 'Communication'], - ['sent_or_received', '=', 'Received']], - order_by='creation asc') + daily_summary_emails = frappe.get_all( + "Daily Work Summary", fields=["name"], filters=[["creation", "Between", filters.range]] + ) + daily_summary_emails = [d.get("name") for d in daily_summary_emails] + replies = frappe.get_all( + "Communication", + fields=["content", "text_content", "sender"], + filters=[ + ["reference_doctype", "=", "Daily Work Summary"], + ["reference_name", "in", daily_summary_emails], + ["communication_type", "=", "Communication"], + ["sent_or_received", "=", "Received"], + ], + order_by="creation asc", + ) data = [] total = len(daily_summary_emails) for user in get_user_emails_from_group(filters.group): - user_name = frappe.get_value('User', user, 'full_name') + user_name = frappe.get_value("User", user, "full_name") count = len([d for d in replies if d.sender == user]) data.append([user_name, count, total]) return data diff --git a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py index 62b83f26a61..29532f7680a 100644 --- a/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py +++ b/erpnext/hr/report/employee_advance_summary/employee_advance_summary.py @@ -7,7 +7,8 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} advances_list = get_advances(filters) columns = get_columns() @@ -18,8 +19,16 @@ def execute(filters=None): data = [] for advance in advances_list: - row = [advance.name, advance.employee, advance.company, advance.posting_date, - advance.advance_amount, advance.paid_amount, advance.claimed_amount, advance.status] + row = [ + advance.name, + advance.employee, + advance.company, + advance.posting_date, + advance.advance_amount, + advance.paid_amount, + advance.claimed_amount, + advance.status, + ] data.append(row) return columns, data @@ -32,54 +41,40 @@ def get_columns(): "fieldname": "title", "fieldtype": "Link", "options": "Employee Advance", - "width": 120 + "width": 120, }, { "label": _("Employee"), "fieldname": "employee", "fieldtype": "Link", "options": "Employee", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, { "label": _("Advance Amount"), "fieldname": "advance_amount", "fieldtype": "Currency", - "width": 120 - }, - { - "label": _("Paid Amount"), - "fieldname": "paid_amount", - "fieldtype": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", "width": 120}, { "label": _("Claimed Amount"), "fieldname": "claimed_amount", "fieldtype": "Currency", - "width": 120 + "width": 120, }, - { - "label": _("Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 120 - } + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 120}, ] + def get_conditions(filters): conditions = "" @@ -96,10 +91,15 @@ def get_conditions(filters): return conditions + def get_advances(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select name, employee, paid_amount, status, advance_amount, claimed_amount, company, + return frappe.db.sql( + """select name, employee, paid_amount, status, advance_amount, claimed_amount, company, posting_date, purpose from `tabEmployee Advance` - where docstatus<2 %s order by posting_date, name desc""" % - conditions, filters, as_dict=1) + where docstatus<2 %s order by posting_date, name desc""" + % conditions, + filters, + as_dict=1, + ) diff --git a/erpnext/hr/report/employee_analytics/employee_analytics.py b/erpnext/hr/report/employee_analytics/employee_analytics.py index 3a75276cb07..12be156ab9b 100644 --- a/erpnext/hr/report/employee_analytics/employee_analytics.py +++ b/erpnext/hr/report/employee_analytics/employee_analytics.py @@ -7,10 +7,11 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if not filters["company"]: - frappe.throw(_('{0} is mandatory').format(_('Company'))) + frappe.throw(_("{0} is mandatory").format(_("Company"))) columns = get_columns() employees = get_employees(filters) @@ -20,28 +21,41 @@ def execute(filters=None): for department in parameters_result: parameters.append(department) - chart = get_chart_data(parameters,employees, filters) + chart = get_chart_data(parameters, employees, filters) return columns, employees, None, chart + def get_columns(): return [ - _("Employee") + ":Link/Employee:120", _("Name") + ":Data:200", _("Date of Birth")+ ":Date:100", - _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", _("Gender") + "::100", _("Company") + ":Link/Company:120" + _("Employee") + ":Link/Employee:120", + _("Name") + ":Data:200", + _("Date of Birth") + ":Date:100", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Gender") + "::100", + _("Company") + ":Link/Company:120", ] -def get_conditions(filters): - conditions = " and "+filters.get("parameter").lower().replace(" ","_")+" IS NOT NULL " - if filters.get("company"): conditions += " and company = '%s'" % \ - filters["company"].replace("'", "\\'") +def get_conditions(filters): + conditions = " and " + filters.get("parameter").lower().replace(" ", "_") + " IS NOT NULL " + + if filters.get("company"): + conditions += " and company = '%s'" % filters["company"].replace("'", "\\'") return conditions + def get_employees(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select name, employee_name, date_of_birth, + return frappe.db.sql( + """select name, employee_name, date_of_birth, branch, department, designation, - gender, company from `tabEmployee` where status = 'Active' %s""" % conditions, as_list=1) + gender, company from `tabEmployee` where status = 'Active' %s""" + % conditions, + as_list=1, + ) + def get_parameters(filters): if filters.get("parameter") == "Grade": @@ -49,36 +63,37 @@ def get_parameters(filters): else: parameter = filters.get("parameter") - return frappe.db.sql("""select name from `tab"""+ parameter +"""` """, as_list=1) + return frappe.db.sql("""select name from `tab""" + parameter + """` """, as_list=1) -def get_chart_data(parameters,employees, filters): + +def get_chart_data(parameters, employees, filters): if not parameters: parameters = [] datasets = [] - parameter_field_name = filters.get("parameter").lower().replace(" ","_") + parameter_field_name = filters.get("parameter").lower().replace(" ", "_") label = [] for parameter in parameters: if parameter: - total_employee = frappe.db.sql("""select count(*) from - `tabEmployee` where """+ - parameter_field_name + """ = %s and company = %s""" ,( parameter[0], filters.get("company")), as_list=1) + total_employee = frappe.db.sql( + """select count(*) from + `tabEmployee` where """ + + parameter_field_name + + """ = %s and company = %s""", + (parameter[0], filters.get("company")), + as_list=1, + ) if total_employee[0][0]: label.append(parameter) datasets.append(total_employee[0][0]) - values = [ value for value in datasets if value !=0] + values = [value for value in datasets if value != 0] - total_employee = frappe.db.count('Employee', {'status':'Active'}) + total_employee = frappe.db.count("Employee", {"status": "Active"}) others = total_employee - sum(values) label.append(["Not Set"]) values.append(others) - chart = { - "data": { - 'labels': label, - 'datasets': [{'name': 'Employees','values': values}] - } - } + chart = {"data": {"labels": label, "datasets": [{"name": "Employees", "values": values}]}} chart["type"] = "donut" return chart diff --git a/erpnext/hr/report/employee_birthday/employee_birthday.py b/erpnext/hr/report/employee_birthday/employee_birthday.py index cec5a48c199..a6a13d8a4d7 100644 --- a/erpnext/hr/report/employee_birthday/employee_birthday.py +++ b/erpnext/hr/report/employee_birthday/employee_birthday.py @@ -7,34 +7,59 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() data = get_employees(filters) return columns, data + def get_columns(): return [ - _("Employee") + ":Link/Employee:120", _("Name") + ":Data:200", _("Date of Birth")+ ":Date:100", - _("Branch") + ":Link/Branch:120", _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", _("Gender") + "::60", _("Company") + ":Link/Company:120" + _("Employee") + ":Link/Employee:120", + _("Name") + ":Data:200", + _("Date of Birth") + ":Date:100", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Gender") + "::60", + _("Company") + ":Link/Company:120", ] + def get_employees(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select name, employee_name, date_of_birth, + return frappe.db.sql( + """select name, employee_name, date_of_birth, branch, department, designation, - gender, company from tabEmployee where status = 'Active' %s""" % conditions, as_list=1) + gender, company from tabEmployee where status = 'Active' %s""" + % conditions, + as_list=1, + ) + def get_conditions(filters): conditions = "" if filters.get("month"): - month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", - "Dec"].index(filters["month"]) + 1 + month = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ].index(filters["month"]) + 1 conditions += " and month(date_of_birth) = '%s'" % month - if filters.get("company"): conditions += " and company = '%s'" % \ - filters["company"].replace("'", "\\'") + if filters.get("company"): + conditions += " and company = '%s'" % filters["company"].replace("'", "\\'") return conditions diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 66c1d25d593..ca352f197dc 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -17,7 +17,8 @@ from erpnext.hr.doctype.leave_application.leave_application import ( Filters = frappe._dict -def execute(filters: Optional[Filters] = None) -> Tuple: + +def execute(filters: Optional[Filters] = None) -> Tuple: if filters.to_date <= filters.from_date: frappe.throw(_('"From Date" can not be greater than or equal to "To Date"')) @@ -28,91 +29,105 @@ def execute(filters: Optional[Filters] = None) -> Tuple: def get_columns() -> List[Dict]: - return [{ - 'label': _('Leave Type'), - 'fieldtype': 'Link', - 'fieldname': 'leave_type', - 'width': 200, - 'options': 'Leave Type' - }, { - 'label': _('Employee'), - 'fieldtype': 'Link', - 'fieldname': 'employee', - 'width': 100, - 'options': 'Employee' - }, { - 'label': _('Employee Name'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'employee_name', - 'width': 100, - 'options': 'employee' - }, { - 'label': _('Opening Balance'), - 'fieldtype': 'float', - 'fieldname': 'opening_balance', - 'width': 150, - }, { - 'label': _('New Leave(s) Allocated'), - 'fieldtype': 'float', - 'fieldname': 'leaves_allocated', - 'width': 200, - }, { - 'label': _('Leave(s) Taken'), - 'fieldtype': 'float', - 'fieldname': 'leaves_taken', - 'width': 150, - }, { - 'label': _('Leave(s) Expired'), - 'fieldtype': 'float', - 'fieldname': 'leaves_expired', - 'width': 150, - }, { - 'label': _('Closing Balance'), - 'fieldtype': 'float', - 'fieldname': 'closing_balance', - 'width': 150, - }] + return [ + { + "label": _("Leave Type"), + "fieldtype": "Link", + "fieldname": "leave_type", + "width": 200, + "options": "Leave Type", + }, + { + "label": _("Employee"), + "fieldtype": "Link", + "fieldname": "employee", + "width": 100, + "options": "Employee", + }, + { + "label": _("Employee Name"), + "fieldtype": "Dynamic Link", + "fieldname": "employee_name", + "width": 100, + "options": "employee", + }, + { + "label": _("Opening Balance"), + "fieldtype": "float", + "fieldname": "opening_balance", + "width": 150, + }, + { + "label": _("New Leave(s) Allocated"), + "fieldtype": "float", + "fieldname": "leaves_allocated", + "width": 200, + }, + { + "label": _("Leave(s) Taken"), + "fieldtype": "float", + "fieldname": "leaves_taken", + "width": 150, + }, + { + "label": _("Leave(s) Expired"), + "fieldtype": "float", + "fieldname": "leaves_expired", + "width": 150, + }, + { + "label": _("Closing Balance"), + "fieldtype": "float", + "fieldname": "closing_balance", + "width": 150, + }, + ] def get_data(filters: Filters) -> List: - leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name') + leave_types = frappe.db.get_list("Leave Type", pluck="name", order_by="name") conditions = get_conditions(filters) user = frappe.session.user - department_approver_map = get_department_leave_approver_map(filters.get('department')) + department_approver_map = get_department_leave_approver_map(filters.get("department")) - active_employees = frappe.get_list('Employee', + active_employees = frappe.get_list( + "Employee", filters=conditions, - fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver']) + fields=["name", "employee_name", "department", "user_id", "leave_approver"], + ) data = [] for leave_type in leave_types: if len(active_employees) > 1: - data.append({ - 'leave_type': leave_type - }) + data.append({"leave_type": leave_type}) else: - row = frappe._dict({ - 'leave_type': leave_type - }) + row = frappe._dict({"leave_type": leave_type}) for employee in active_employees: - leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) + leave_approvers = department_approver_map.get(employee.department_name, []).append( + employee.leave_approver + ) - if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ - or ("HR Manager" in frappe.get_roles(user)): + if ( + (leave_approvers and len(leave_approvers) and user in leave_approvers) + or (user in ["Administrator", employee.user_id]) + or ("HR Manager" in frappe.get_roles(user)) + ): if len(active_employees) > 1: row = frappe._dict() row.employee = employee.name row.employee_name = employee.employee_name - leaves_taken = get_leaves_for_period(employee.name, leave_type, - filters.from_date, filters.to_date) * -1 + leaves_taken = ( + get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1 + ) new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves( - filters.from_date, filters.to_date, employee.name, leave_type) + filters.from_date, filters.to_date, employee.name, leave_type + ) opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves) row.leaves_allocated = new_allocation @@ -121,21 +136,27 @@ def get_data(filters: Filters) -> List: row.leaves_taken = leaves_taken # not be shown on the basis of days left it create in user mind for carry_forward leave - row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken)) + row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken) row.indent = 1 data.append(row) return data -def get_opening_balance(employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float) -> float: +def get_opening_balance( + employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float +) -> float: # allocation boundary condition # opening balance is the closing leave balance 1 day before the filter start date opening_balance_date = add_days(filters.from_date, -1) allocation = get_previous_allocation(filters.from_date, leave_type, employee) - if allocation and allocation.get("to_date") and opening_balance_date and \ - getdate(allocation.get("to_date")) == getdate(opening_balance_date): + if ( + allocation + and allocation.get("to_date") + and opening_balance_date + and getdate(allocation.get("to_date")) == getdate(opening_balance_date) + ): # if opening balance date is same as the previous allocation's expiry # then opening balance should only consider carry forwarded leaves opening_balance = carry_forwarded_leaves @@ -147,39 +168,35 @@ def get_opening_balance(employee: str, leave_type: str, filters: Filters, carry_ def get_conditions(filters: Filters) -> Dict: - conditions={ - 'status': 'Active', + conditions = { + "status": "Active", } - if filters.get('employee'): - conditions['name'] = filters.get('employee') + if filters.get("employee"): + conditions["name"] = filters.get("employee") - if filters.get('company'): - conditions['company'] = filters.get('company') + if filters.get("company"): + conditions["company"] = filters.get("company") - if filters.get('department'): - conditions['department'] = filters.get('department') + if filters.get("department"): + conditions["department"] = filters.get("department") return conditions def get_department_leave_approver_map(department: Optional[str] = None): # get current department and all its child - department_list = frappe.get_list('Department', - filters={'disabled': 0}, - or_filters={ - 'name': department, - 'parent_department': department - }, - pluck='name' + department_list = frappe.get_list( + "Department", + filters={"disabled": 0}, + or_filters={"name": department, "parent_department": department}, + pluck="name", ) # retrieve approvers list from current department and from its subsequent child departments - approver_list = frappe.get_all('Department Approver', - filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, - fields=['parent', 'approver'], - as_list=True + approver_list = frappe.get_all( + "Department Approver", + filters={"parentfield": "leave_approvers", "parent": ("in", department_list)}, + fields=["parent", "approver"], + as_list=True, ) approvers = {} @@ -190,7 +207,9 @@ def get_department_leave_approver_map(department: Optional[str] = None): return approvers -def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str, leave_type: str) -> Tuple[float, float, float]: +def get_allocated_and_expired_leaves( + from_date: str, to_date: str, employee: str, leave_type: str +) -> Tuple[float, float, float]: new_allocation = 0 expired_leaves = 0 carry_forwarded_leaves = 0 @@ -207,8 +226,7 @@ def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str # leave allocations ending before to_date, reduce leaves taken within that period # since they are already used, they won't expire expired_leaves += record.leaves - expired_leaves += get_leaves_for_period(employee, leave_type, - record.from_date, record.to_date) + expired_leaves += get_leaves_for_period(employee, leave_type, record.from_date, record.to_date) if record.from_date >= getdate(from_date): if record.is_carry_forward: @@ -219,22 +237,31 @@ def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str return new_allocation, expired_leaves, carry_forwarded_leaves -def get_leave_ledger_entries(from_date: str, to_date: str, employee: str, leave_type: str) -> List[Dict]: - ledger = frappe.qb.DocType('Leave Ledger Entry') +def get_leave_ledger_entries( + from_date: str, to_date: str, employee: str, leave_type: str +) -> List[Dict]: + ledger = frappe.qb.DocType("Leave Ledger Entry") records = ( frappe.qb.from_(ledger) .select( - ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date, - ledger.leaves, ledger.transaction_name, ledger.transaction_type, - ledger.is_carry_forward, ledger.is_expired - ).where( + ledger.employee, + ledger.leave_type, + ledger.from_date, + ledger.to_date, + ledger.leaves, + ledger.transaction_name, + ledger.transaction_type, + ledger.is_carry_forward, + ledger.is_expired, + ) + .where( (ledger.docstatus == 1) - & (ledger.transaction_type == 'Leave Allocation') + & (ledger.transaction_type == "Leave Allocation") & (ledger.employee == employee) & (ledger.leave_type == leave_type) & ( - (ledger.from_date[from_date: to_date]) - | (ledger.to_date[from_date: to_date]) + (ledger.from_date[from_date:to_date]) + | (ledger.to_date[from_date:to_date]) | ((ledger.from_date < from_date) & (ledger.to_date > to_date)) ) ) @@ -248,16 +275,13 @@ def get_chart_data(data: List) -> Dict: datasets = [] employee_data = data - if data and data[0].get('employee_name'): + if data and data[0].get("employee_name"): get_dataset_for_chart(employee_data, datasets, labels) chart = { - 'data': { - 'labels': labels, - 'datasets': datasets - }, - 'type': 'bar', - 'colors': ['#456789', '#EE8888', '#7E77BF'] + "data": {"labels": labels, "datasets": datasets}, + "type": "bar", + "colors": ["#456789", "#EE8888", "#7E77BF"], } return chart @@ -265,18 +289,17 @@ def get_chart_data(data: List) -> Dict: def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List: leaves = [] - employee_data = sorted(employee_data, key=lambda k: k['employee_name']) + employee_data = sorted(employee_data, key=lambda k: k["employee_name"]) - for key, group in groupby(employee_data, lambda x: x['employee_name']): + for key, group in groupby(employee_data, lambda x: x["employee_name"]): for grp in group: if grp.closing_balance: - leaves.append(frappe._dict({ - 'leave_type': grp.leave_type, - 'closing_balance': grp.closing_balance - })) + leaves.append( + frappe._dict({"leave_type": grp.leave_type, "closing_balance": grp.closing_balance}) + ) if leaves: labels.append(key) for leave in leaves: - datasets.append({'name': leave.leave_type, 'values': [leave.closing_balance]}) + datasets.append({"name": leave.leave_type, "values": [leave.closing_balance]}) diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py index b2ed72c04d7..dc0f4d2c944 100644 --- a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -21,141 +21,189 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_leave_application, ) -test_records = frappe.get_test_records('Leave Type') +test_records = frappe.get_test_records("Leave Type") + class TestEmployeeLeaveBalance(unittest.TestCase): def setUp(self): - for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']: + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Leave Ledger Entry", + "Leave Type", + ]: frappe.db.delete(dt) - frappe.set_user('Administrator') + frappe.set_user("Administrator") - self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company") self.date = getdate() self.year_start = getdate(get_year_start(self.date)) self.mid_year = add_months(self.year_start, 6) self.year_end = getdate(get_year_ending(self.date)) - self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end) + self.holiday_list = make_holiday_list( + "_Test Emp Balance Holiday List", self.year_start, self.year_end + ) def tearDown(self): frappe.db.rollback() - @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") def test_employee_leave_balance(self): frappe.get_doc(test_records[0]).insert() # 5 leaves - allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11), - to_date=add_days(self.year_start, -1), leaves=5) + allocation1 = make_allocation_record( + employee=self.employee_id, + from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), + leaves=5, + ) # 30 leaves - allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + allocation2 = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) # expires 5 leaves process_expired_allocation() # 4 days leave first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) - leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) leave_application.reload() - filters = frappe._dict({ - 'from_date': allocation1.from_date, - 'to_date': allocation2.to_date, - 'employee': self.employee_id - }) + filters = frappe._dict( + { + "from_date": allocation1.from_date, + "to_date": allocation2.to_date, + "employee": self.employee_id, + } + ) report = execute(filters) - expected_data = [{ - 'leave_type': '_Test Leave Type', - 'employee': self.employee_id, - 'employee_name': 'test_emp_leave_balance@example.com', - 'leaves_allocated': flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated), - 'leaves_expired': flt(allocation1.new_leaves_allocated), - 'opening_balance': flt(0), - 'leaves_taken': flt(leave_application.total_leave_days), - 'closing_balance': flt(allocation2.new_leaves_allocated - leave_application.total_leave_days), - 'indent': 1 - }] + expected_data = [ + { + "leave_type": "_Test Leave Type", + "employee": self.employee_id, + "employee_name": "test_emp_leave_balance@example.com", + "leaves_allocated": flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated), + "leaves_expired": flt(allocation1.new_leaves_allocated), + "opening_balance": flt(0), + "leaves_taken": flt(leave_application.total_leave_days), + "closing_balance": flt(allocation2.new_leaves_allocated - leave_application.total_leave_days), + "indent": 1, + } + ] self.assertEqual(report[1], expected_data) - @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") def test_opening_balance_on_alloc_boundary_dates(self): frappe.get_doc(test_records[0]).insert() # 30 leaves allocated - allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + allocation1 = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) # 4 days leave application in the first allocation first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) - leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) leave_application.reload() # Case 1: opening balance for first alloc boundary - filters = frappe._dict({ - 'from_date': self.year_start, - 'to_date': self.year_end, - 'employee': self.employee_id - }) + filters = frappe._dict( + {"from_date": self.year_start, "to_date": self.year_end, "employee": self.employee_id} + ) report = execute(filters) self.assertEqual(report[1][0].opening_balance, 0) # Case 2: opening balance after leave application date - filters = frappe._dict({ - 'from_date': add_days(leave_application.to_date, 1), - 'to_date': self.year_end, - 'employee': self.employee_id - }) + filters = frappe._dict( + { + "from_date": add_days(leave_application.to_date, 1), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) report = execute(filters) - self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) + self.assertEqual( + report[1][0].opening_balance, + (allocation1.new_leaves_allocated - leave_application.total_leave_days), + ) # Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 - filters = frappe._dict({ - 'from_date': add_days(self.year_end, -3), - 'to_date': self.year_end, - 'employee': self.employee_id - }) + filters = frappe._dict( + { + "from_date": add_days(self.year_end, -3), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) report = execute(filters) - self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) + self.assertEqual( + report[1][0].opening_balance, + (allocation1.new_leaves_allocated - leave_application.total_leave_days), + ) - @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") def test_opening_balance_considers_carry_forwarded_leaves(self): - leave_type = create_leave_type( - leave_type_name="_Test_CF_leave_expiry", - is_carry_forward=1) + leave_type = create_leave_type(leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1) leave_type.insert() # 30 leaves allocated for first half of the year - allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, - to_date=self.mid_year, leave_type=leave_type.name) + allocation1 = make_allocation_record( + employee=self.employee_id, + from_date=self.year_start, + to_date=self.mid_year, + leave_type=leave_type.name, + ) # 4 days leave application in the first allocation first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) - leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application = make_leave_application( + self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name + ) leave_application.reload() # 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation - allocation2 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.mid_year, 1), to_date=self.year_end, - carry_forward=True, leave_type=leave_type.name) + allocation2 = make_allocation_record( + employee=self.employee_id, + from_date=add_days(self.mid_year, 1), + to_date=self.year_end, + carry_forward=True, + leave_type=leave_type.name, + ) # Case 1: carry forwarded leaves considered in opening balance for second alloc - filters = frappe._dict({ - 'from_date': add_days(self.mid_year, 1), - 'to_date': self.year_end, - 'employee': self.employee_id - }) + filters = frappe._dict( + { + "from_date": add_days(self.mid_year, 1), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) report = execute(filters) # available leaves from old alloc opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days self.assertEqual(report[1][0].opening_balance, opening_balance) # Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc - filters = frappe._dict({ - 'from_date': add_days(self.mid_year, 2), - 'to_date': self.year_end, - 'employee': self.employee_id - }) + filters = frappe._dict( + { + "from_date": add_days(self.mid_year, 2), + "to_date": self.year_end, + "employee": self.employee_id, + } + ) report = execute(filters) # available leaves from old alloc - opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days) + opening_balance = allocation2.new_leaves_allocated + ( + allocation1.new_leaves_allocated - leave_application.total_leave_days + ) self.assertEqual(report[1][0].opening_balance, opening_balance) diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 936184a9c0d..f0087eb1b9e 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -19,11 +19,12 @@ def execute(filters=None): return columns, data + def get_columns(leave_types): columns = [ _("Employee") + ":Link.Employee:150", _("Employee Name") + "::200", - _("Department") +"::150" + _("Department") + "::150", ] for leave_type in leave_types: @@ -31,6 +32,7 @@ def get_columns(leave_types): return columns + def get_conditions(filters): conditions = { "status": "Active", @@ -43,15 +45,18 @@ def get_conditions(filters): return conditions + def get_data(filters, leave_types): user = frappe.session.user conditions = get_conditions(filters) - active_employees = frappe.get_all("Employee", + active_employees = frappe.get_all( + "Employee", filters=conditions, - fields=["name", "employee_name", "department", "user_id", "leave_approver"]) + fields=["name", "employee_name", "department", "user_id", "leave_approver"], + ) - department_approver_map = get_department_leave_approver_map(filters.get('department')) + department_approver_map = get_department_leave_approver_map(filters.get("department")) data = [] for employee in active_employees: @@ -59,14 +64,18 @@ def get_data(filters, leave_types): if employee.leave_approver: leave_approvers.append(employee.leave_approver) - if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)): + if ( + (len(leave_approvers) and user in leave_approvers) + or (user in ["Administrator", employee.user_id]) + or ("HR Manager" in frappe.get_roles(user)) + ): row = [employee.name, employee.employee_name, employee.department] available_leave = get_leave_details(employee.name, filters.date) for leave_type in leave_types: remaining = 0 if leave_type in available_leave["leave_allocation"]: # opening balance - remaining = available_leave["leave_allocation"][leave_type]['remaining_leaves'] + remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"] row += [remaining] diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py index 6f16a8d58cb..34b665fa9f0 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py @@ -20,40 +20,59 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_leave_application, ) -test_records = frappe.get_test_records('Leave Type') +test_records = frappe.get_test_records("Leave Type") + class TestEmployeeLeaveBalance(unittest.TestCase): def setUp(self): - for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']: + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Leave Ledger Entry", + "Leave Type", + ]: frappe.db.delete(dt) - frappe.set_user('Administrator') + frappe.set_user("Administrator") - self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') - self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company") + self.employee_id = make_employee("test_emp_leave_balance@example.com", company="_Test Company") self.date = getdate() self.year_start = getdate(get_year_start(self.date)) self.year_end = getdate(get_year_ending(self.date)) - self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end) + self.holiday_list = make_holiday_list( + "_Test Emp Balance Holiday List", self.year_start, self.year_end + ) def tearDown(self): frappe.db.rollback() - @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") def test_employee_leave_balance_summary(self): frappe.get_doc(test_records[0]).insert() # 5 leaves - allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11), - to_date=add_days(self.year_start, -1), leaves=5) + allocation1 = make_allocation_record( + employee=self.employee_id, + from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), + leaves=5, + ) # 30 leaves - allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + allocation2 = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) # 2 days leave within the first allocation - leave_application1 = make_leave_application(self.employee_id, add_days(self.year_start, -11), add_days(self.year_start, -10), - '_Test Leave Type') + leave_application1 = make_leave_application( + self.employee_id, + add_days(self.year_start, -11), + add_days(self.year_start, -10), + "_Test Leave Type", + ) leave_application1.reload() # expires 3 leaves @@ -61,57 +80,69 @@ class TestEmployeeLeaveBalance(unittest.TestCase): # 4 days leave within the second allocation first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) - leave_application2 = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application2 = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) leave_application2.reload() - filters = frappe._dict({ - 'date': add_days(leave_application2.to_date, 1), - 'company': '_Test Company', - 'employee': self.employee_id - }) + filters = frappe._dict( + { + "date": add_days(leave_application2.to_date, 1), + "company": "_Test Company", + "employee": self.employee_id, + } + ) report = execute(filters) - expected_data = [[ - self.employee_id, - 'test_emp_leave_balance@example.com', - frappe.db.get_value('Employee', self.employee_id, 'department'), - flt( - allocation1.new_leaves_allocated # allocated = 5 - + allocation2.new_leaves_allocated # allocated = 30 - - leave_application1.total_leave_days # leaves taken in the 1st alloc = 2 - - (allocation1.new_leaves_allocated - leave_application1.total_leave_days) # leaves expired from 1st alloc = 3 - - leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4 - ) - ]] + expected_data = [ + [ + self.employee_id, + "test_emp_leave_balance@example.com", + frappe.db.get_value("Employee", self.employee_id, "department"), + flt( + allocation1.new_leaves_allocated # allocated = 5 + + allocation2.new_leaves_allocated # allocated = 30 + - leave_application1.total_leave_days # leaves taken in the 1st alloc = 2 + - ( + allocation1.new_leaves_allocated - leave_application1.total_leave_days + ) # leaves expired from 1st alloc = 3 + - leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4 + ), + ] + ] self.assertEqual(report[1], expected_data) - @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + @set_holiday_list("_Test Emp Balance Holiday List", "_Test Company") def test_get_leave_balance_near_alloc_expiry(self): frappe.get_doc(test_records[0]).insert() # 30 leaves allocated - allocation = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + allocation = make_allocation_record( + employee=self.employee_id, from_date=self.year_start, to_date=self.year_end + ) # 4 days leave application in the first allocation first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) - leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application = make_leave_application( + self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), "_Test Leave Type" + ) leave_application.reload() # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 - filters = frappe._dict({ - 'date': add_days(self.year_end, -3), - 'company': '_Test Company', - 'employee': self.employee_id - }) + filters = frappe._dict( + {"date": add_days(self.year_end, -3), "company": "_Test Company", "employee": self.employee_id} + ) report = execute(filters) - expected_data = [[ - self.employee_id, - 'test_emp_leave_balance@example.com', - frappe.db.get_value('Employee', self.employee_id, 'department'), - flt(allocation.new_leaves_allocated - leave_application.total_leave_days) - ]] + expected_data = [ + [ + self.employee_id, + "test_emp_leave_balance@example.com", + frappe.db.get_value("Employee", self.employee_id, "department"), + flt(allocation.new_leaves_allocated - leave_application.total_leave_days), + ] + ] self.assertEqual(report[1], expected_data) diff --git a/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py b/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py index 00a4a7c29f5..f13fabf06e6 100644 --- a/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py +++ b/erpnext/hr/report/employees_working_on_a_holiday/employees_working_on_a_holiday.py @@ -21,16 +21,21 @@ def get_columns(): _("Name") + ":Data:200", _("Date") + ":Date:100", _("Status") + ":Data:70", - _("Holiday") + ":Data:200" + _("Holiday") + ":Data:200", ] + def get_employees(filters): - holiday_filter = [["holiday_date", ">=", filters.from_date], ["holiday_date", "<=", filters.to_date]] + holiday_filter = [ + ["holiday_date", ">=", filters.from_date], + ["holiday_date", "<=", filters.to_date], + ] if filters.holiday_list: holiday_filter.append(["parent", "=", filters.holiday_list]) - holidays = frappe.get_all("Holiday", fields=["holiday_date", "description"], - filters=holiday_filter) + holidays = frappe.get_all( + "Holiday", fields=["holiday_date", "description"], filters=holiday_filter + ) holiday_names = {} holidays_list = [] @@ -39,18 +44,23 @@ def get_employees(filters): holidays_list.append(holiday.holiday_date) holiday_names[holiday.holiday_date] = holiday.description - if(holidays_list): + if holidays_list: cond = " attendance_date in %(holidays_list)s" if filters.holiday_list: - cond += """ and (employee in (select employee from tabEmployee where holiday_list = %(holidays)s))""" + cond += ( + """ and (employee in (select employee from tabEmployee where holiday_list = %(holidays)s))""" + ) - employee_list = frappe.db.sql("""select + employee_list = frappe.db.sql( + """select employee, employee_name, attendance_date, status from tabAttendance - where %s"""% cond.format(', '.join(["%s"] * len(holidays_list))), - {'holidays_list':holidays_list, - 'holidays':filters.holiday_list}, as_list=True) + where %s""" + % cond.format(", ".join(["%s"] * len(holidays_list))), + {"holidays_list": holidays_list, "holidays": filters.holiday_list}, + as_list=True, + ) for employee_data in employee_list: employee_data.append(holiday_names[employee_data[2]]) diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py index 4e043379404..e3cb36e0daf 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -15,21 +15,15 @@ status_map = { "Weekly Off": "WO", "On Leave": "L", "Present": "P", - "Work From Home": "WFH" - } + "Work From Home": "WFH", +} + +day_abbr = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] -day_abbr = [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" -] def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if filters.hide_year_field == 1: filters.year = 2020 @@ -44,14 +38,19 @@ def execute(filters=None): emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company) holiday_list = [] for parameter in group_by_parameters: - h_list = [emp_map[parameter][d]["holiday_list"] for d in emp_map[parameter] if emp_map[parameter][d]["holiday_list"]] + h_list = [ + emp_map[parameter][d]["holiday_list"] + for d in emp_map[parameter] + if emp_map[parameter][d]["holiday_list"] + ] holiday_list += h_list else: emp_map = get_employee_details(filters.group_by, filters.company) holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]] - - default_holiday_list = frappe.get_cached_value('Company', filters.get("company"), "default_holiday_list") + default_holiday_list = frappe.get_cached_value( + "Company", filters.get("company"), "default_holiday_list" + ) holiday_list.append(default_holiday_list) holiday_list = list(set(holiday_list)) holiday_map = get_holiday(holiday_list, filters["month"]) @@ -70,20 +69,33 @@ def execute(filters=None): for parameter in group_by_parameters: emp_map_set = set([key for key in emp_map[parameter].keys()]) att_map_set = set([key for key in att_map.keys()]) - if (att_map_set & emp_map_set): - parameter_row = [""+ parameter + ""] + ['' for day in range(filters["total_days_in_month"] + 2)] + if att_map_set & emp_map_set: + parameter_row = ["" + parameter + ""] + [ + "" for day in range(filters["total_days_in_month"] + 2) + ] data.append(parameter_row) - record, emp_att_data = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) + record, emp_att_data = add_data( + emp_map[parameter], + att_map, + filters, + holiday_map, + conditions, + default_holiday_list, + leave_list=leave_list, + ) emp_att_map.update(emp_att_data) data += record else: - record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list) + record, emp_att_map = add_data( + emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list + ) data += record chart_data = get_chart_data(emp_att_map, days) return columns, data, None, chart_data + def get_chart_data(emp_att_map, days): labels = [] datasets = [ @@ -110,24 +122,20 @@ def get_chart_data(emp_att_map, days): if emp_att_map[emp][idx] == "L": total_leave_on_day += 1 - datasets[0]["values"].append(total_absent_on_day) datasets[1]["values"].append(total_present_on_day) datasets[2]["values"].append(total_leave_on_day) - - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } - } + chart = {"data": {"labels": labels, "datasets": datasets}} chart["type"] = "line" return chart -def add_data(employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None): + +def add_data( + employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None +): record = [] emp_att_map = {} @@ -141,7 +149,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho row += [" "] row += [emp, emp_det.employee_name] - total_p = total_a = total_l = total_h = total_um= 0.0 + total_p = total_a = total_l = total_h = total_um = 0.0 emp_status_map = [] for day in range(filters["total_days_in_month"]): status = None @@ -152,7 +160,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho if emp_holiday_list in holiday_map: for idx, ele in enumerate(holiday_map[emp_holiday_list]): - if day+1 == holiday_map[emp_holiday_list][idx][0]: + if day + 1 == holiday_map[emp_holiday_list][idx][0]: if holiday_map[emp_holiday_list][idx][1]: status = "Weekly Off" else: @@ -162,7 +170,7 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho abbr = status_map.get(status, "") emp_status_map.append(abbr) - if filters.summarized_view: + if filters.summarized_view: if status == "Present" or status == "Work From Home": total_p += 1 elif status == "Absent": @@ -189,12 +197,21 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho filters.update({"employee": emp}) if filters.summarized_view: - leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ - where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) + leave_details = frappe.db.sql( + """select leave_type, status, count(*) as count from `tabAttendance`\ + where leave_type is not NULL %s group by leave_type, status""" + % conditions, + filters, + as_dict=1, + ) - time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \ + time_default_counts = frappe.db.sql( + """select (select count(*) from `tabAttendance` where \ late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \ - early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters) + early_exit = 1 %s) as early_exit_count""" + % (conditions, conditions), + filters, + ) leaves = {} for d in leave_details: @@ -211,38 +228,48 @@ def add_data(employee_map, att_map, filters, holiday_map, conditions, default_ho else: row.append("0.0") - row.extend([time_default_counts[0][0],time_default_counts[0][1]]) + row.extend([time_default_counts[0][0], time_default_counts[0][1]]) emp_att_map[emp] = emp_status_map record.append(row) return record, emp_att_map + def get_columns(filters): columns = [] if filters.group_by: - columns = [_(filters.group_by)+ ":Link/Branch:120"] + columns = [_(filters.group_by) + ":Link/Branch:120"] - columns += [ - _("Employee") + ":Link/Employee:120", _("Employee Name") + ":Data/:120" - ] + columns += [_("Employee") + ":Link/Employee:120", _("Employee Name") + ":Data/:120"] days = [] for day in range(filters["total_days_in_month"]): - date = str(filters.year) + "-" + str(filters.month)+ "-" + str(day+1) + date = str(filters.year) + "-" + str(filters.month) + "-" + str(day + 1) day_name = day_abbr[getdate(date).weekday()] - days.append(cstr(day+1)+ " " +day_name +"::65") + days.append(cstr(day + 1) + " " + day_name + "::65") if not filters.summarized_view: columns += days if filters.summarized_view: - columns += [_("Total Present") + ":Float:120", _("Total Leaves") + ":Float:120", _("Total Absent") + ":Float:120", _("Total Holidays") + ":Float:120", _("Unmarked Days")+ ":Float:120"] + columns += [ + _("Total Present") + ":Float:120", + _("Total Leaves") + ":Float:120", + _("Total Absent") + ":Float:120", + _("Total Holidays") + ":Float:120", + _("Unmarked Days") + ":Float:120", + ] return columns, days + def get_attendance_list(conditions, filters): - attendance_list = frappe.db.sql("""select employee, day(attendance_date) as day_of_month, - status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" % - conditions, filters, as_dict=1) + attendance_list = frappe.db.sql( + """select employee, day(attendance_date) as day_of_month, + status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" + % conditions, + filters, + as_dict=1, + ) if not attendance_list: msgprint(_("No attendance record found"), alert=True, indicator="orange") @@ -254,6 +281,7 @@ def get_attendance_list(conditions, filters): return att_map + def get_conditions(filters): if not (filters.get("month") and filters.get("year")): msgprint(_("Please select month and year"), raise_exception=1) @@ -262,29 +290,35 @@ def get_conditions(filters): conditions = " and month(attendance_date) = %(month)s and year(attendance_date) = %(year)s" - if filters.get("company"): conditions += " and company = %(company)s" - if filters.get("employee"): conditions += " and employee = %(employee)s" + if filters.get("company"): + conditions += " and company = %(company)s" + if filters.get("employee"): + conditions += " and employee = %(employee)s" return conditions, filters + def get_employee_details(group_by, company): emp_map = {} query = """select name, employee_name, designation, department, branch, company, - holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape(company) + holiday_list from `tabEmployee` where company = %s """ % frappe.db.escape( + company + ) if group_by: group_by = group_by.lower() query += " order by " + group_by + " ASC" - employee_details = frappe.db.sql(query , as_dict=1) + employee_details = frappe.db.sql(query, as_dict=1) group_by_parameters = [] if group_by: - group_by_parameters = list(set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, ""))) + group_by_parameters = list( + set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, "")) + ) for parameter in group_by_parameters: - emp_map[parameter] = {} - + emp_map[parameter] = {} for d in employee_details: if group_by and len(group_by_parameters): @@ -299,18 +333,28 @@ def get_employee_details(group_by, company): else: return emp_map, group_by_parameters + def get_holiday(holiday_list, month): holiday_map = frappe._dict() for d in holiday_list: if d: - holiday_map.setdefault(d, frappe.db.sql('''select day(holiday_date), weekly_off from `tabHoliday` - where parent=%s and month(holiday_date)=%s''', (d, month))) + holiday_map.setdefault( + d, + frappe.db.sql( + """select day(holiday_date), weekly_off from `tabHoliday` + where parent=%s and month(holiday_date)=%s""", + (d, month), + ), + ) return holiday_map + @frappe.whitelist() def get_attendance_years(): - year_list = frappe.db.sql_list("""select distinct YEAR(attendance_date) from tabAttendance ORDER BY YEAR(attendance_date) DESC""") + year_list = frappe.db.sql_list( + """select distinct YEAR(attendance_date) from tabAttendance ORDER BY YEAR(attendance_date) DESC""" + ) if not year_list: year_list = [getdate().year] diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py index 952af8117e2..91da08eee50 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -11,31 +11,33 @@ from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import class TestMonthlyAttendanceSheet(FrappeTestCase): def setUp(self): self.employee = make_employee("test_employee@example.com") - frappe.db.delete('Attendance', {'employee': self.employee}) + frappe.db.delete("Attendance", {"employee": self.employee}) def test_monthly_attendance_sheet_report(self): now = now_datetime() previous_month = now.month - 1 previous_month_first = now.replace(day=1).replace(month=previous_month).date() - company = frappe.db.get_value('Employee', self.employee, 'company') + company = frappe.db.get_value("Employee", self.employee, "company") # mark different attendance status on first 3 days of previous month - mark_attendance(self.employee, previous_month_first, 'Absent') - mark_attendance(self.employee, previous_month_first + relativedelta(days=1), 'Present') - mark_attendance(self.employee, previous_month_first + relativedelta(days=2), 'On Leave') + mark_attendance(self.employee, previous_month_first, "Absent") + mark_attendance(self.employee, previous_month_first + relativedelta(days=1), "Present") + mark_attendance(self.employee, previous_month_first + relativedelta(days=2), "On Leave") - filters = frappe._dict({ - 'month': previous_month, - 'year': now.year, - 'company': company, - }) + filters = frappe._dict( + { + "month": previous_month, + "year": now.year, + "company": company, + } + ) report = execute(filters=filters) employees = report[1][0] - datasets = report[3]['data']['datasets'] - absent = datasets[0]['values'] - present = datasets[1]['values'] - leaves = datasets[2]['values'] + datasets = report[3]["data"]["datasets"] + absent = datasets[0]["values"] + present = datasets[1]["values"] + leaves = datasets[2]["values"] # ensure correct attendance is reflect on the report self.assertIn(self.employee, employees) diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py index 6383a9bbac9..b6caf400dd0 100644 --- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py +++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py @@ -8,7 +8,8 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} filters = frappe._dict(filters) columns = get_columns() @@ -25,67 +26,53 @@ def get_columns(): "fieldtype": "Link", "fieldname": "staffing_plan", "options": "Staffing Plan", - "width": 150 + "width": 150, }, { "label": _("Job Opening"), "fieldtype": "Link", "fieldname": "job_opening", "options": "Job Opening", - "width": 105 + "width": 105, }, { "label": _("Job Applicant"), "fieldtype": "Link", "fieldname": "job_applicant", "options": "Job Applicant", - "width": 150 - }, - { - "label": _("Applicant name"), - "fieldtype": "data", - "fieldname": "applicant_name", - "width": 130 + "width": 150, }, + {"label": _("Applicant name"), "fieldtype": "data", "fieldname": "applicant_name", "width": 130}, { "label": _("Application Status"), "fieldtype": "Data", "fieldname": "application_status", - "width": 150 + "width": 150, }, { "label": _("Job Offer"), "fieldtype": "Link", "fieldname": "job_offer", "options": "job Offer", - "width": 150 - }, - { - "label": _("Designation"), - "fieldtype": "Data", - "fieldname": "designation", - "width": 100 - }, - { - "label": _("Offer Date"), - "fieldtype": "date", - "fieldname": "offer_date", - "width": 100 + "width": 150, }, + {"label": _("Designation"), "fieldtype": "Data", "fieldname": "designation", "width": 100}, + {"label": _("Offer Date"), "fieldtype": "date", "fieldname": "offer_date", "width": 100}, { "label": _("Job Offer status"), "fieldtype": "Data", "fieldname": "job_offer_status", - "width": 150 - } + "width": 150, + }, ] + def get_data(filters): data = [] staffing_plan_details = get_staffing_plan(filters) - staffing_plan_list = list(set([details["name"] for details in staffing_plan_details])) - sp_jo_map , jo_list = get_job_opening(staffing_plan_list) - jo_ja_map , ja_list = get_job_applicant(jo_list) + staffing_plan_list = list(set([details["name"] for details in staffing_plan_details])) + sp_jo_map, jo_list = get_job_opening(staffing_plan_list) + jo_ja_map, ja_list = get_job_applicant(jo_list) ja_joff_map = get_job_offer(ja_list) for sp in sp_jo_map.keys(): @@ -100,37 +87,40 @@ def get_parent_row(sp_jo_map, sp, jo_ja_map, ja_joff_map): if sp in sp_jo_map.keys(): for jo in sp_jo_map[sp]: row = { - "staffing_plan" : sp, - "job_opening" : jo["name"], + "staffing_plan": sp, + "job_opening": jo["name"], } data.append(row) - child_row = get_child_row( jo["name"], jo_ja_map, ja_joff_map) + child_row = get_child_row(jo["name"], jo_ja_map, ja_joff_map) data += child_row return data + def get_child_row(jo, jo_ja_map, ja_joff_map): data = [] if jo in jo_ja_map.keys(): for ja in jo_ja_map[jo]: row = { - "indent":1, + "indent": 1, "job_applicant": ja.name, "applicant_name": ja.applicant_name, "application_status": ja.status, } if ja.name in ja_joff_map.keys(): - jo_detail =ja_joff_map[ja.name][0] + jo_detail = ja_joff_map[ja.name][0] row["job_offer"] = jo_detail.name row["job_offer_status"] = jo_detail.status - row["offer_date"]= jo_detail.offer_date.strftime("%d-%m-%Y") + row["offer_date"] = jo_detail.offer_date.strftime("%d-%m-%Y") row["designation"] = jo_detail.designation data.append(row) return data + def get_staffing_plan(filters): - staffing_plan = frappe.db.sql(""" + staffing_plan = frappe.db.sql( + """ select sp.name, sp.department, spd.designation, spd.vacancies, spd.current_count, spd.parent, sp.to_date from @@ -139,13 +129,20 @@ def get_staffing_plan(filters): spd.parent = sp.name And sp.to_date > '{0}' - """.format(filters.on_date), as_dict = 1) + """.format( + filters.on_date + ), + as_dict=1, + ) return staffing_plan + def get_job_opening(sp_list): - job_openings = frappe.get_all("Job Opening", filters = [["staffing_plan", "IN", sp_list]], fields =["name", "staffing_plan"]) + job_openings = frappe.get_all( + "Job Opening", filters=[["staffing_plan", "IN", sp_list]], fields=["name", "staffing_plan"] + ) sp_jo_map = {} jo_list = [] @@ -160,12 +157,17 @@ def get_job_opening(sp_list): return sp_jo_map, jo_list + def get_job_applicant(jo_list): jo_ja_map = {} - ja_list =[] + ja_list = [] - applicants = frappe.get_all("Job Applicant", filters = [["job_title", "IN", jo_list]], fields =["name", "job_title","applicant_name", 'status']) + applicants = frappe.get_all( + "Job Applicant", + filters=[["job_title", "IN", jo_list]], + fields=["name", "job_title", "applicant_name", "status"], + ) for applicant in applicants: if applicant.job_title not in jo_ja_map.keys(): @@ -175,12 +177,17 @@ def get_job_applicant(jo_list): ja_list.append(applicant.name) - return jo_ja_map , ja_list + return jo_ja_map, ja_list + def get_job_offer(ja_list): ja_joff_map = {} - offers = frappe.get_all("Job Offer", filters = [["job_applicant", "IN", ja_list]], fields =["name", "job_applicant", "status", 'offer_date', 'designation']) + offers = frappe.get_all( + "Job Offer", + filters=[["job_applicant", "IN", ja_list]], + fields=["name", "job_applicant", "status", "offer_date", "designation"], + ) for offer in offers: if offer.job_applicant not in ja_joff_map.keys(): diff --git a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py index 8672e68cf4b..da6dace72b5 100644 --- a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py +++ b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py @@ -17,12 +17,14 @@ from erpnext.hr.report.vehicle_expenses.vehicle_expenses import execute class TestVehicleExpenses(unittest.TestCase): @classmethod def setUpClass(self): - frappe.db.sql('delete from `tabVehicle Log`') + frappe.db.sql("delete from `tabVehicle Log`") - employee_id = frappe.db.sql('''select name from `tabEmployee` where name="testdriver@example.com"''') + employee_id = frappe.db.sql( + '''select name from `tabEmployee` where name="testdriver@example.com"''' + ) self.employee_id = employee_id[0][0] if employee_id else None if not self.employee_id: - self.employee_id = make_employee('testdriver@example.com', company='_Test Company') + self.employee_id = make_employee("testdriver@example.com", company="_Test Company") self.license_plate = get_vehicle(self.employee_id) @@ -31,36 +33,35 @@ class TestVehicleExpenses(unittest.TestCase): expense_claim = make_expense_claim(vehicle_log.name) # Based on Fiscal Year - filters = { - 'filter_based_on': 'Fiscal Year', - 'fiscal_year': get_fiscal_year(getdate())[0] - } + filters = {"filter_based_on": "Fiscal Year", "fiscal_year": get_fiscal_year(getdate())[0]} report = execute(filters) - expected_data = [{ - 'vehicle': self.license_plate, - 'make': 'Maruti', - 'model': 'PCM', - 'location': 'Mumbai', - 'log_name': vehicle_log.name, - 'odometer': 5010, - 'date': getdate(), - 'fuel_qty': 50.0, - 'fuel_price': 500.0, - 'fuel_expense': 25000.0, - 'service_expense': 2000.0, - 'employee': self.employee_id - }] + expected_data = [ + { + "vehicle": self.license_plate, + "make": "Maruti", + "model": "PCM", + "location": "Mumbai", + "log_name": vehicle_log.name, + "odometer": 5010, + "date": getdate(), + "fuel_qty": 50.0, + "fuel_price": 500.0, + "fuel_expense": 25000.0, + "service_expense": 2000.0, + "employee": self.employee_id, + } + ] self.assertEqual(report[1], expected_data) # Based on Date Range fiscal_year = get_fiscal_year(getdate(), as_dict=True) filters = { - 'filter_based_on': 'Date Range', - 'from_date': fiscal_year.year_start_date, - 'to_date': fiscal_year.year_end_date + "filter_based_on": "Date Range", + "from_date": fiscal_year.year_start_date, + "to_date": fiscal_year.year_end_date, } report = execute(filters) @@ -68,9 +69,9 @@ class TestVehicleExpenses(unittest.TestCase): # clean up vehicle_log.cancel() - frappe.delete_doc('Expense Claim', expense_claim.name) - frappe.delete_doc('Vehicle Log', vehicle_log.name) + frappe.delete_doc("Expense Claim", expense_claim.name) + frappe.delete_doc("Vehicle Log", vehicle_log.name) def tearDown(self): - frappe.delete_doc('Vehicle', self.license_plate, force=1) - frappe.delete_doc('Employee', self.employee_id, force=1) + frappe.delete_doc("Vehicle", self.license_plate, force=1) + frappe.delete_doc("Employee", self.employee_id, force=1) diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py index 17d1e9d46a0..fc5510ddad8 100644 --- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py +++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py @@ -18,83 +18,44 @@ def execute(filters=None): return columns, data, None, chart + def get_columns(): return [ { - 'fieldname': 'vehicle', - 'fieldtype': 'Link', - 'label': _('Vehicle'), - 'options': 'Vehicle', - 'width': 150 + "fieldname": "vehicle", + "fieldtype": "Link", + "label": _("Vehicle"), + "options": "Vehicle", + "width": 150, + }, + {"fieldname": "make", "fieldtype": "Data", "label": _("Make"), "width": 100}, + {"fieldname": "model", "fieldtype": "Data", "label": _("Model"), "width": 80}, + {"fieldname": "location", "fieldtype": "Data", "label": _("Location"), "width": 100}, + { + "fieldname": "log_name", + "fieldtype": "Link", + "label": _("Vehicle Log"), + "options": "Vehicle Log", + "width": 100, + }, + {"fieldname": "odometer", "fieldtype": "Int", "label": _("Odometer Value"), "width": 120}, + {"fieldname": "date", "fieldtype": "Date", "label": _("Date"), "width": 100}, + {"fieldname": "fuel_qty", "fieldtype": "Float", "label": _("Fuel Qty"), "width": 80}, + {"fieldname": "fuel_price", "fieldtype": "Float", "label": _("Fuel Price"), "width": 100}, + {"fieldname": "fuel_expense", "fieldtype": "Currency", "label": _("Fuel Expense"), "width": 150}, + { + "fieldname": "service_expense", + "fieldtype": "Currency", + "label": _("Service Expense"), + "width": 150, }, { - 'fieldname': 'make', - 'fieldtype': 'Data', - 'label': _('Make'), - 'width': 100 + "fieldname": "employee", + "fieldtype": "Link", + "label": _("Employee"), + "options": "Employee", + "width": 150, }, - { - 'fieldname': 'model', - 'fieldtype': 'Data', - 'label': _('Model'), - 'width': 80 - }, - { - 'fieldname': 'location', - 'fieldtype': 'Data', - 'label': _('Location'), - 'width': 100 - }, - { - 'fieldname': 'log_name', - 'fieldtype': 'Link', - 'label': _('Vehicle Log'), - 'options': 'Vehicle Log', - 'width': 100 - }, - { - 'fieldname': 'odometer', - 'fieldtype': 'Int', - 'label': _('Odometer Value'), - 'width': 120 - }, - { - 'fieldname': 'date', - 'fieldtype': 'Date', - 'label': _('Date'), - 'width': 100 - }, - { - 'fieldname': 'fuel_qty', - 'fieldtype': 'Float', - 'label': _('Fuel Qty'), - 'width': 80 - }, - { - 'fieldname': 'fuel_price', - 'fieldtype': 'Float', - 'label': _('Fuel Price'), - 'width': 100 - }, - { - 'fieldname': 'fuel_expense', - 'fieldtype': 'Currency', - 'label': _('Fuel Expense'), - 'width': 150 - }, - { - 'fieldname': 'service_expense', - 'fieldtype': 'Currency', - 'label': _('Service Expense'), - 'width': 150 - }, - { - 'fieldname': 'employee', - 'fieldtype': 'Link', - 'label': _('Employee'), - 'options': 'Employee', - 'width': 150 - } ] @@ -102,7 +63,8 @@ def get_vehicle_log_data(filters): start_date, end_date = get_period_dates(filters) conditions, values = get_conditions(filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT vhcl.license_plate as vehicle, vhcl.make, vhcl.model, vhcl.location, log.name as log_name, log.odometer, @@ -116,58 +78,70 @@ def get_vehicle_log_data(filters): and log.docstatus = 1 and date between %(start_date)s and %(end_date)s {0} - ORDER BY date""".format(conditions), values, as_dict=1) + ORDER BY date""".format( + conditions + ), + values, + as_dict=1, + ) for row in data: - row['service_expense'] = get_service_expense(row.log_name) + row["service_expense"] = get_service_expense(row.log_name) return data def get_conditions(filters): - conditions = '' + conditions = "" start_date, end_date = get_period_dates(filters) - values = { - 'start_date': start_date, - 'end_date': end_date - } + values = {"start_date": start_date, "end_date": end_date} if filters.employee: - conditions += ' and log.employee = %(employee)s' - values['employee'] = filters.employee + conditions += " and log.employee = %(employee)s" + values["employee"] = filters.employee if filters.vehicle: - conditions += ' and vhcl.license_plate = %(vehicle)s' - values['vehicle'] = filters.vehicle + conditions += " and vhcl.license_plate = %(vehicle)s" + values["vehicle"] = filters.vehicle return conditions, values def get_period_dates(filters): - if filters.filter_based_on == 'Fiscal Year' and filters.fiscal_year: - fy = frappe.db.get_value('Fiscal Year', filters.fiscal_year, - ['year_start_date', 'year_end_date'], as_dict=True) + if filters.filter_based_on == "Fiscal Year" and filters.fiscal_year: + fy = frappe.db.get_value( + "Fiscal Year", filters.fiscal_year, ["year_start_date", "year_end_date"], as_dict=True + ) return fy.year_start_date, fy.year_end_date else: return filters.from_date, filters.to_date def get_service_expense(logname): - expense_amount = frappe.db.sql(""" + expense_amount = frappe.db.sql( + """ SELECT sum(expense_amount) FROM `tabVehicle Log` log, `tabVehicle Service` service WHERE service.parent=log.name and log.name=%s - """, logname) + """, + logname, + ) return flt(expense_amount[0][0]) if expense_amount else 0.0 def get_chart_data(data, filters): - period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, - filters.from_date, filters.to_date, filters.filter_based_on, 'Monthly') + period_list = get_period_list( + filters.fiscal_year, + filters.fiscal_year, + filters.from_date, + filters.to_date, + filters.filter_based_on, + "Monthly", + ) fuel_data, service_data = [], [] @@ -184,29 +158,20 @@ def get_chart_data(data, filters): service_data.append([period.key, total_service_exp]) labels = [period.label for period in period_list] - fuel_exp_data= [row[1] for row in fuel_data] - service_exp_data= [row[1] for row in service_data] + fuel_exp_data = [row[1] for row in fuel_data] + service_exp_data = [row[1] for row in service_data] datasets = [] if fuel_exp_data: - datasets.append({ - 'name': _('Fuel Expenses'), - 'values': fuel_exp_data - }) + datasets.append({"name": _("Fuel Expenses"), "values": fuel_exp_data}) if service_exp_data: - datasets.append({ - 'name': _('Service Expenses'), - 'values': service_exp_data - }) + datasets.append({"name": _("Service Expenses"), "values": service_exp_data}) chart = { - 'data': { - 'labels': labels, - 'datasets': datasets - }, - 'type': 'line', - 'fieldtype': 'Currency' + "data": {"labels": labels, "datasets": datasets}, + "type": "line", + "fieldtype": "Currency", } return chart diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 46bcadcf536..2524872ea2b 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -26,20 +26,22 @@ from erpnext.hr.doctype.employee.employee import ( ) -class DuplicateDeclarationError(frappe.ValidationError): pass +class DuplicateDeclarationError(frappe.ValidationError): + pass class EmployeeBoardingController(Document): - ''' - Create the project and the task for the boarding process - Assign to the concerned person and roles as per the onboarding/separation template - ''' + """ + Create the project and the task for the boarding process + Assign to the concerned person and roles as per the onboarding/separation template + """ + def validate(self): validate_active_employee(self.employee) # remove the task if linked before submitting the form if self.amended_from: for activity in self.activities: - activity.task = '' + activity.task = "" def on_submit(self): # create the project for the given employee onboarding @@ -49,13 +51,17 @@ class EmployeeBoardingController(Document): else: project_name += self.employee - project = frappe.get_doc({ + project = frappe.get_doc( + { "doctype": "Project", "project_name": project_name, - "expected_start_date": self.date_of_joining if self.doctype == "Employee Onboarding" else self.resignation_letter_date, + "expected_start_date": self.date_of_joining + if self.doctype == "Employee Onboarding" + else self.resignation_letter_date, "department": self.department, - "company": self.company - }).insert(ignore_permissions=True, ignore_mandatory=True) + "company": self.company, + } + ).insert(ignore_permissions=True, ignore_mandatory=True) self.db_set("project", project.name) self.db_set("boarding_status", "Pending") @@ -68,20 +74,23 @@ class EmployeeBoardingController(Document): if activity.task: continue - task = frappe.get_doc({ - "doctype": "Task", - "project": self.project, - "subject": activity.activity_name + " : " + self.employee_name, - "description": activity.description, - "department": self.department, - "company": self.company, - "task_weight": activity.task_weight - }).insert(ignore_permissions=True) + task = frappe.get_doc( + { + "doctype": "Task", + "project": self.project, + "subject": activity.activity_name + " : " + self.employee_name, + "description": activity.description, + "department": self.department, + "company": self.company, + "task_weight": activity.task_weight, + } + ).insert(ignore_permissions=True) activity.db_set("task", task.name) users = [activity.user] if activity.user else [] if activity.role: - user_list = frappe.db.sql_list(''' + user_list = frappe.db.sql_list( + """ SELECT DISTINCT(has_role.parent) FROM @@ -92,7 +101,9 @@ class EmployeeBoardingController(Document): has_role.parenttype = 'User' AND user.enabled = 1 AND has_role.role = %s - ''', activity.role) + """, + activity.role, + ) users = unique(users + user_list) if "Administrator" in users: @@ -105,11 +116,11 @@ class EmployeeBoardingController(Document): def assign_task_to_users(self, task, users): for user in users: args = { - 'assign_to': [user], - 'doctype': task.doctype, - 'name': task.name, - 'description': task.description or task.subject, - 'notify': self.notify_users_by_email + "assign_to": [user], + "doctype": task.doctype, + "name": task.name, + "description": task.description or task.subject, + "notify": self.notify_users_by_email, } assign_to.add(args) @@ -118,41 +129,56 @@ class EmployeeBoardingController(Document): for task in frappe.get_all("Task", filters={"project": self.project}): frappe.delete_doc("Task", task.name, force=1) frappe.delete_doc("Project", self.project, force=1) - self.db_set('project', '') + self.db_set("project", "") for activity in self.activities: activity.db_set("task", "") @frappe.whitelist() def get_onboarding_details(parent, parenttype): - return frappe.get_all("Employee Boarding Activity", - fields=["activity_name", "role", "user", "required_for_employee_creation", "description", "task_weight"], + return frappe.get_all( + "Employee Boarding Activity", + fields=[ + "activity_name", + "role", + "user", + "required_for_employee_creation", + "description", + "task_weight", + ], filters={"parent": parent, "parenttype": parenttype}, - order_by= "idx") + order_by="idx", + ) + @frappe.whitelist() def get_boarding_status(project): - status = 'Pending' + status = "Pending" if project: - doc = frappe.get_doc('Project', project) + doc = frappe.get_doc("Project", project) if flt(doc.percent_complete) > 0.0 and flt(doc.percent_complete) < 100.0: - status = 'In Process' + status = "In Process" elif flt(doc.percent_complete) == 100.0: - status = 'Completed' + status = "Completed" return status + def set_employee_name(doc): if doc.employee and not doc.employee_name: doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name") + def update_employee_work_history(employee, details, date=None, cancel=False): if not employee.internal_work_history and not cancel: - employee.append("internal_work_history", { - "branch": employee.branch, - "designation": employee.designation, - "department": employee.department, - "from_date": employee.date_of_joining - }) + employee.append( + "internal_work_history", + { + "branch": employee.branch, + "designation": employee.designation, + "department": employee.department, + "from_date": employee.date_of_joining, + }, + ) internal_work_history = {} for item in details: @@ -163,7 +189,7 @@ def update_employee_work_history(employee, details, date=None, cancel=False): new_data = item.new if not cancel else item.current if fieldtype == "Date" and new_data: new_data = getdate(new_data) - elif fieldtype =="Datetime" and new_data: + elif fieldtype == "Datetime" and new_data: new_data = get_datetime(new_data) setattr(employee, item.fieldname, new_data) if item.fieldname in ["department", "designation", "branch"]: @@ -178,6 +204,7 @@ def update_employee_work_history(employee, details, date=None, cancel=False): return employee + def delete_employee_work_history(details, employee, date): filters = {} for d in details: @@ -201,12 +228,25 @@ def delete_employee_work_history(details, employee, date): def get_employee_fields_label(): fields = [] for df in frappe.get_meta("Employee").get("fields"): - if df.fieldname in ["salutation", "user_id", "employee_number", "employment_type", - "holiday_list", "branch", "department", "designation", "grade", - "notice_number_of_days", "reports_to", "leave_policy", "company_email"]: - fields.append({"value": df.fieldname, "label": df.label}) + if df.fieldname in [ + "salutation", + "user_id", + "employee_number", + "employment_type", + "holiday_list", + "branch", + "department", + "designation", + "grade", + "notice_number_of_days", + "reports_to", + "leave_policy", + "company_email", + ]: + fields.append({"value": df.fieldname, "label": df.label}) return fields + @frappe.whitelist() def get_employee_field_property(employee, fieldname): if employee and fieldname: @@ -217,17 +257,15 @@ def get_employee_field_property(employee, fieldname): value = formatdate(value) elif field.fieldtype == "Datetime": value = format_datetime(value) - return { - "value" : value, - "datatype" : field.fieldtype, - "label" : field.label, - "options" : options - } + return {"value": value, "datatype": field.fieldtype, "label": field.label, "options": options} else: return False + def validate_dates(doc, from_date, to_date): - date_of_joining, relieving_date = frappe.db.get_value("Employee", doc.employee, ["date_of_joining", "relieving_date"]) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", doc.employee, ["date_of_joining", "relieving_date"] + ) if getdate(from_date) > getdate(to_date): frappe.throw(_("To date can not be less than from date")) elif getdate(from_date) > getdate(nowdate()): @@ -237,7 +275,8 @@ def validate_dates(doc, from_date, to_date): elif relieving_date and getdate(to_date) > getdate(relieving_date): frappe.throw(_("To date can not greater than employee's relieving date")) -def validate_overlap(doc, from_date, to_date, company = None): + +def validate_overlap(doc, from_date, to_date, company=None): query = """ select name from `tab{0}` @@ -247,15 +286,19 @@ def validate_overlap(doc, from_date, to_date, company = None): if not doc.name: # hack! if name is null, it could cause problems with != - doc.name = "New "+doc.doctype + doc.name = "New " + doc.doctype - overlap_doc = frappe.db.sql(query.format(doc.doctype),{ + overlap_doc = frappe.db.sql( + query.format(doc.doctype), + { "employee": doc.get("employee"), "from_date": from_date, "to_date": to_date, "name": doc.name, - "company": company - }, as_dict = 1) + "company": company, + }, + as_dict=1, + ) if overlap_doc: if doc.get("employee"): @@ -264,6 +307,7 @@ def validate_overlap(doc, from_date, to_date, company = None): exists_for = company throw_overlap_error(doc, exists_for, overlap_doc[0].name, from_date, to_date) + def get_doc_condition(doctype): if doctype == "Compensatory Leave Request": return "and employee = %(employee)s and docstatus < 2 \ @@ -275,23 +319,36 @@ def get_doc_condition(doctype): or to_date between %(from_date)s and %(to_date)s \ or (from_date < %(from_date)s and to_date > %(to_date)s))" + def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date): - msg = _("A {0} exists between {1} and {2} (").format(doc.doctype, - formatdate(from_date), formatdate(to_date)) \ - + """ {1}""".format(doc.doctype, overlap_doc) \ + msg = ( + _("A {0} exists between {1} and {2} (").format( + doc.doctype, formatdate(from_date), formatdate(to_date) + ) + + """ {1}""".format(doc.doctype, overlap_doc) + _(") for {0}").format(exists_for) + ) frappe.throw(msg) + def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee): - existing_record = frappe.db.exists(doctype, { - "payroll_period": payroll_period, - "employee": employee, - 'docstatus': ['<', 2], - 'name': ['!=', docname] - }) + existing_record = frappe.db.exists( + doctype, + { + "payroll_period": payroll_period, + "employee": employee, + "docstatus": ["<", 2], + "name": ["!=", docname], + }, + ) if existing_record: - frappe.throw(_("{0} already exists for employee {1} and period {2}") - .format(doctype, employee, payroll_period), DuplicateDeclarationError) + frappe.throw( + _("{0} already exists for employee {1} and period {2}").format( + doctype, employee, payroll_period + ), + DuplicateDeclarationError, + ) + def validate_tax_declaration(declarations): subcategories = [] @@ -300,61 +357,79 @@ def validate_tax_declaration(declarations): frappe.throw(_("More than one selection for {0} not allowed").format(d.exemption_sub_category)) subcategories.append(d.exemption_sub_category) + def get_total_exemption_amount(declarations): exemptions = frappe._dict() for d in declarations: exemptions.setdefault(d.exemption_category, frappe._dict()) category_max_amount = exemptions.get(d.exemption_category).max_amount if not category_max_amount: - category_max_amount = frappe.db.get_value("Employee Tax Exemption Category", d.exemption_category, "max_amount") + category_max_amount = frappe.db.get_value( + "Employee Tax Exemption Category", d.exemption_category, "max_amount" + ) exemptions.get(d.exemption_category).max_amount = category_max_amount - sub_category_exemption_amount = d.max_amount \ - if (d.max_amount and flt(d.amount) > flt(d.max_amount)) else d.amount + sub_category_exemption_amount = ( + d.max_amount if (d.max_amount and flt(d.amount) > flt(d.max_amount)) else d.amount + ) exemptions.get(d.exemption_category).setdefault("total_exemption_amount", 0.0) exemptions.get(d.exemption_category).total_exemption_amount += flt(sub_category_exemption_amount) - if category_max_amount and exemptions.get(d.exemption_category).total_exemption_amount > category_max_amount: + if ( + category_max_amount + and exemptions.get(d.exemption_category).total_exemption_amount > category_max_amount + ): exemptions.get(d.exemption_category).total_exemption_amount = category_max_amount total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()]) return total_exemption_amount + @frappe.whitelist() def get_leave_period(from_date, to_date, company): - leave_period = frappe.db.sql(""" + leave_period = frappe.db.sql( + """ select name, from_date, to_date from `tabLeave Period` where company=%(company)s and is_active=1 and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, { - "from_date": from_date, - "to_date": to_date, - "company": company - }, as_dict=1) + """, + {"from_date": from_date, "to_date": to_date, "company": company}, + as_dict=1, + ) if leave_period: return leave_period + def generate_leave_encashment(): - ''' Generates a draft leave encashment on allocation expiry ''' + """Generates a draft leave encashment on allocation expiry""" from erpnext.hr.doctype.leave_encashment.leave_encashment import create_leave_encashment - if frappe.db.get_single_value('HR Settings', 'auto_leave_encashment'): - leave_type = frappe.get_all('Leave Type', filters={'allow_encashment': 1}, fields=['name']) - leave_type=[l['name'] for l in leave_type] + if frappe.db.get_single_value("HR Settings", "auto_leave_encashment"): + leave_type = frappe.get_all("Leave Type", filters={"allow_encashment": 1}, fields=["name"]) + leave_type = [l["name"] for l in leave_type] - leave_allocation = frappe.get_all("Leave Allocation", filters={ - 'to_date': add_days(today(), -1), - 'leave_type': ('in', leave_type) - }, fields=['employee', 'leave_period', 'leave_type', 'to_date', 'total_leaves_allocated', 'new_leaves_allocated']) + leave_allocation = frappe.get_all( + "Leave Allocation", + filters={"to_date": add_days(today(), -1), "leave_type": ("in", leave_type)}, + fields=[ + "employee", + "leave_period", + "leave_type", + "to_date", + "total_leaves_allocated", + "new_leaves_allocated", + ], + ) create_leave_encashment(leave_allocation=leave_allocation) + def allocate_earned_leaves(ignore_duplicates=False): - '''Allocate earned leaves to Employees''' + """Allocate earned leaves to Employees""" e_leave_types = get_earned_leaves() today = getdate() @@ -367,37 +442,52 @@ def allocate_earned_leaves(ignore_duplicates=False): if not allocation.leave_policy_assignment and not allocation.leave_policy: continue - leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value( - "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"]) + leave_policy = ( + allocation.leave_policy + if allocation.leave_policy + else frappe.db.get_value( + "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"] + ) + ) - annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={ - 'parent': leave_policy, - 'leave_type': e_leave_type.name - }, fieldname=['annual_allocation']) + annual_allocation = frappe.db.get_value( + "Leave Policy Detail", + filters={"parent": leave_policy, "leave_type": e_leave_type.name}, + fieldname=["annual_allocation"], + ) - from_date=allocation.from_date + from_date = allocation.from_date if e_leave_type.based_on_date_of_joining: - from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") + from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining): - update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates) + if check_effective_date( + from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining + ): + update_previous_leave_allocation( + allocation, annual_allocation, e_leave_type, ignore_duplicates + ) -def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type, ignore_duplicates=False): - earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding) - allocation = frappe.get_doc('Leave Allocation', allocation.name) - new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) +def update_previous_leave_allocation( + allocation, annual_allocation, e_leave_type, ignore_duplicates=False +): + earned_leaves = get_monthly_earned_leave( + annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding + ) - if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: - new_allocation = e_leave_type.max_leaves_allowed + allocation = frappe.get_doc("Leave Allocation", allocation.name) + new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) - if new_allocation != allocation.total_leaves_allocated: - today_date = today() + if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: + new_allocation = e_leave_type.max_leaves_allowed - if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + if new_allocation != allocation.total_leaves_allocated: + today_date = today() + + if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) def get_monthly_earned_leave(annual_leaves, frequency, rounding): @@ -425,8 +515,9 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): date_of_joining = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") assignment = frappe.get_doc("Leave Policy Assignment", allocation.leave_policy_assignment) - leaves_for_passed_months = assignment.get_leaves_for_passed_months(allocation.leave_type, - annual_allocation, leave_type_details, date_of_joining) + leaves_for_passed_months = assignment.get_leaves_for_passed_months( + allocation.leave_type, annual_allocation, leave_type_details, date_of_joining + ) # exclude carry-forwarded leaves while checking for leave allocation for passed months num_allocations = allocation.total_leaves_allocated @@ -439,26 +530,39 @@ def is_earned_leave_already_allocated(allocation, annual_allocation): def get_leave_allocations(date, leave_type): - return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy + return frappe.db.sql( + """select name, employee, from_date, to_date, leave_policy_assignment, leave_policy from `tabLeave Allocation` where %s between from_date and to_date and docstatus=1 and leave_type=%s""", - (date, leave_type), as_dict=1) + (date, leave_type), + as_dict=1, + ) def get_earned_leaves(): - return frappe.get_all("Leave Type", - fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"], - filters={'is_earned_leave' : 1}) + return frappe.get_all( + "Leave Type", + fields=[ + "name", + "max_leaves_allowed", + "earned_leave_frequency", + "rounding", + "based_on_date_of_joining", + ], + filters={"is_earned_leave": 1}, + ) + def create_additional_leave_ledger_entry(allocation, leaves, date): - ''' Create leave ledger entry for leave types ''' + """Create leave ledger entry for leave types""" allocation.new_leaves_allocated = leaves allocation.from_date = date allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() + def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining): import calendar @@ -467,10 +571,12 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining from_date = get_datetime(from_date) to_date = get_datetime(to_date) rd = relativedelta.relativedelta(to_date, from_date) - #last day of month - last_day = calendar.monthrange(to_date.year, to_date.month)[1] + # last day of month + last_day = calendar.monthrange(to_date.year, to_date.month)[1] - if (from_date.day == to_date.day and based_on_date_of_joining) or (not based_on_date_of_joining and to_date.day == last_day): + if (from_date.day == to_date.day and based_on_date_of_joining) or ( + not based_on_date_of_joining and to_date.day == last_day + ): if frequency == "Monthly": return True elif frequency == "Quarterly" and rd.months % 3: @@ -487,16 +593,21 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining def get_salary_assignment(employee, date): - assignment = frappe.db.sql(""" + assignment = frappe.db.sql( + """ select * from `tabSalary Structure Assignment` where employee=%(employee)s and docstatus = 1 - and %(on_date)s >= from_date order by from_date desc limit 1""", { - 'employee': employee, - 'on_date': date, - }, as_dict=1) + and %(on_date)s >= from_date order by from_date desc limit 1""", + { + "employee": employee, + "on_date": date, + }, + as_dict=1, + ) return assignment[0] if assignment else None + def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): total_given_benefit_amount = 0 query = """ @@ -514,17 +625,22 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): if component: query += "and sd.salary_component = %(component)s" - sum_of_given_benefit = frappe.db.sql(query, { - 'employee': employee, - 'start_date': payroll_period.start_date, - 'end_date': payroll_period.end_date, - 'component': component - }, as_dict=True) + sum_of_given_benefit = frappe.db.sql( + query, + { + "employee": employee, + "start_date": payroll_period.start_date, + "end_date": payroll_period.end_date, + "component": component, + }, + as_dict=True, + ) if sum_of_given_benefit and flt(sum_of_given_benefit[0].total_amount) > 0: total_given_benefit_amount = sum_of_given_benefit[0].total_amount return total_given_benefit_amount + def get_holiday_dates_for_employee(employee, start_date, end_date): """return a list of holiday dates for the given employee between start_date and end_date""" # return only date @@ -533,50 +649,48 @@ def get_holiday_dates_for_employee(employee, start_date, end_date): return [cstr(h.holiday_date) for h in holidays] -def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False): +def get_holidays_for_employee( + employee, start_date, end_date, raise_exception=True, only_non_weekly=False +): """Get Holidays for a given employee - `employee` (str) - `start_date` (str or datetime) - `end_date` (str or datetime) - `raise_exception` (bool) - `only_non_weekly` (bool) + `employee` (str) + `start_date` (str or datetime) + `end_date` (str or datetime) + `raise_exception` (bool) + `only_non_weekly` (bool) - return: list of dicts with `holiday_date` and `description` + return: list of dicts with `holiday_date` and `description` """ holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception) if not holiday_list: return [] - filters = { - 'parent': holiday_list, - 'holiday_date': ('between', [start_date, end_date]) - } + filters = {"parent": holiday_list, "holiday_date": ("between", [start_date, end_date])} if only_non_weekly: - filters['weekly_off'] = False + filters["weekly_off"] = False - holidays = frappe.get_all( - 'Holiday', - fields=['description', 'holiday_date'], - filters=filters - ) + holidays = frappe.get_all("Holiday", fields=["description", "holiday_date"], filters=filters) return holidays + @erpnext.allow_regional def calculate_annual_eligible_hra_exemption(doc): # Don't delete this method, used for localization # Indian HRA Exemption Calculation return {} + @erpnext.allow_regional def calculate_hra_exemption_for_period(doc): # Don't delete this method, used for localization # Indian HRA Exemption Calculation return {} + def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, component=False): total_claimed_amount = 0 query = """ @@ -591,24 +705,29 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co if component: query += "and earning_component = %(component)s" - sum_of_claimed_amount = frappe.db.sql(query, { - 'employee': employee, - 'start_date': payroll_period.start_date, - 'end_date': payroll_period.end_date, - 'component': component - }, as_dict=True) + sum_of_claimed_amount = frappe.db.sql( + query, + { + "employee": employee, + "start_date": payroll_period.start_date, + "end_date": payroll_period.end_date, + "component": component, + }, + as_dict=True, + ) if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: total_claimed_amount = sum_of_claimed_amount[0].total_amount return total_claimed_amount + def share_doc_with_approver(doc, user): # if approver does not have permissions, share if not frappe.has_permission(doc=doc, ptype="submit", user=user): - frappe.share.add(doc.doctype, doc.name, user, submit=1, - flags={"ignore_share_permission": True}) + frappe.share.add(doc.doctype, doc.name, user, submit=1, flags={"ignore_share_permission": True}) - frappe.msgprint(_("Shared with the user {0} with {1} access").format( - user, frappe.bold("submit"), alert=True)) + frappe.msgprint( + _("Shared with the user {0} with {1} access").format(user, frappe.bold("submit"), alert=True) + ) # remove shared doc if approver changes doc_before_save = doc.get_doc_before_save() @@ -616,14 +735,19 @@ def share_doc_with_approver(doc, user): approvers = { "Leave Application": "leave_approver", "Expense Claim": "expense_approver", - "Shift Request": "approver" + "Shift Request": "approver", } approver = approvers.get(doc.doctype) if doc_before_save.get(approver) != doc.get(approver): frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) + def validate_active_employee(employee): if frappe.db.get_value("Employee", employee, "status") == "Inactive": - frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format( - get_link_to_form("Employee", employee)), InactiveEmployeeStatusError) + frappe.throw( + _("Transactions cannot be created for an Inactive Employee {0}.").format( + get_link_to_form("Employee", employee) + ), + InactiveEmployeeStatusError, + ) diff --git a/erpnext/hr/web_form/job_application/job_application.py b/erpnext/hr/web_form/job_application/job_application.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/hr/web_form/job_application/job_application.py +++ b/erpnext/hr/web_form/job_application/job_application.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/hub_node/__init__.py b/erpnext/hub_node/__init__.py index 2bfbfd1088e..4ff2e2c9dd0 100644 --- a/erpnext/hub_node/__init__.py +++ b/erpnext/hub_node/__init__.py @@ -7,12 +7,13 @@ import frappe @frappe.whitelist() def enable_hub(): - hub_settings = frappe.get_doc('Marketplace Settings') + hub_settings = frappe.get_doc("Marketplace Settings") hub_settings.register() frappe.db.commit() return hub_settings + @frappe.whitelist() def sync(): - hub_settings = frappe.get_doc('Marketplace Settings') + hub_settings = frappe.get_doc("Marketplace Settings") hub_settings.sync() diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py index 1bf7b8c3b52..410c508de06 100644 --- a/erpnext/hub_node/api.py +++ b/erpnext/hub_node/api.py @@ -1,4 +1,3 @@ - import json import frappe @@ -14,24 +13,24 @@ current_user = frappe.session.user def register_marketplace(company, company_description): validate_registerer() - settings = frappe.get_single('Marketplace Settings') + settings = frappe.get_single("Marketplace Settings") message = settings.register_seller(company, company_description) - if message.get('hub_seller_name'): + if message.get("hub_seller_name"): settings.registered = 1 - settings.hub_seller_name = message.get('hub_seller_name') + settings.hub_seller_name = message.get("hub_seller_name") settings.save() settings.add_hub_user(frappe.session.user) - return { 'ok': 1 } + return {"ok": 1} @frappe.whitelist() def register_users(user_list): user_list = json.loads(user_list) - settings = frappe.get_single('Marketplace Settings') + settings = frappe.get_single("Marketplace Settings") for user in user_list: settings.add_hub_user(user) @@ -40,14 +39,16 @@ def register_users(user_list): def validate_registerer(): - if current_user == 'Administrator': - frappe.throw(_('Please login as another user to register on Marketplace')) + if current_user == "Administrator": + frappe.throw(_("Please login as another user to register on Marketplace")) - valid_roles = ['System Manager', 'Item Manager'] + valid_roles = ["System Manager", "Item Manager"] if not frappe.utils.is_subset(valid_roles, frappe.get_roles()): - frappe.throw(_('Only users with {0} role can register on Marketplace').format(', '.join(valid_roles)), - frappe.PermissionError) + frappe.throw( + _("Only users with {0} role can register on Marketplace").format(", ".join(valid_roles)), + frappe.PermissionError, + ) @frappe.whitelist() @@ -57,9 +58,7 @@ def call_hub_method(method, params=None): if isinstance(params, string_types): params = json.loads(params) - params.update({ - 'cmd': 'hub.hub.api.' + method - }) + params.update({"cmd": "hub.hub.api." + method}) response = connection.post_request(params) return response @@ -67,81 +66,81 @@ def call_hub_method(method, params=None): def map_fields(items): field_mappings = get_field_mappings() - table_fields = [d.fieldname for d in frappe.get_meta('Item').get_table_fields()] + table_fields = [d.fieldname for d in frappe.get_meta("Item").get_table_fields()] - hub_seller_name = frappe.db.get_value('Marketplace Settings', 'Marketplace Settings', 'hub_seller_name') + hub_seller_name = frappe.db.get_value( + "Marketplace Settings", "Marketplace Settings", "hub_seller_name" + ) for item in items: for fieldname in table_fields: item.pop(fieldname, None) for mapping in field_mappings: - local_fieldname = mapping.get('local_fieldname') - remote_fieldname = mapping.get('remote_fieldname') + local_fieldname = mapping.get("local_fieldname") + remote_fieldname = mapping.get("remote_fieldname") value = item.get(local_fieldname) item.pop(local_fieldname, None) item[remote_fieldname] = value - item['doctype'] = 'Hub Item' - item['hub_seller'] = hub_seller_name - item.pop('attachments', None) + item["doctype"] = "Hub Item" + item["hub_seller"] = hub_seller_name + item.pop("attachments", None) return items @frappe.whitelist() -def get_valid_items(search_value=''): +def get_valid_items(search_value=""): items = frappe.get_list( - 'Item', + "Item", fields=["*"], - filters={ - 'disabled': 0, - 'item_name': ['like', '%' + search_value + '%'], - 'publish_in_hub': 0 - }, - order_by="modified desc" + filters={"disabled": 0, "item_name": ["like", "%" + search_value + "%"], "publish_in_hub": 0}, + order_by="modified desc", ) valid_items = filter(lambda x: x.image and x.description, items) def prepare_item(item): item.source_type = "local" - item.attachments = get_attachments('Item', item.item_code) + item.attachments = get_attachments("Item", item.item_code) return item valid_items = map(prepare_item, valid_items) return valid_items + @frappe.whitelist() def update_item(ref_doc, data): data = json.loads(data) - data.update(dict(doctype='Hub Item', name=ref_doc)) + data.update(dict(doctype="Hub Item", name=ref_doc)) try: connection = get_hub_connection() connection.update(data) except Exception as e: - frappe.log_error(message=e, title='Hub Sync Error') + frappe.log_error(message=e, title="Hub Sync Error") + @frappe.whitelist() def publish_selected_items(items_to_publish): items_to_publish = json.loads(items_to_publish) items_to_update = [] if not len(items_to_publish): - frappe.throw(_('No items to publish')) + frappe.throw(_("No items to publish")) for item in items_to_publish: - item_code = item.get('item_code') - frappe.db.set_value('Item', item_code, 'publish_in_hub', 1) + item_code = item.get("item_code") + frappe.db.set_value("Item", item_code, "publish_in_hub", 1) hub_dict = { - 'doctype': 'Hub Tracked Item', - 'item_code': item_code, - 'published': 1, - 'hub_category': item.get('hub_category'), - 'image_list': item.get('image_list') + "doctype": "Hub Tracked Item", + "item_code": item_code, + "published": 1, + "hub_category": item.get("hub_category"), + "image_list": item.get("image_list"), } frappe.get_doc(hub_dict).insert(ignore_if_duplicate=True) @@ -157,65 +156,67 @@ def publish_selected_items(items_to_publish): item_sync_postprocess() except Exception as e: - frappe.log_error(message=e, title='Hub Sync Error') + frappe.log_error(message=e, title="Hub Sync Error") + @frappe.whitelist() def unpublish_item(item_code, hub_item_name): - ''' Remove item listing from the marketplace ''' + """Remove item listing from the marketplace""" - response = call_hub_method('unpublish_item', { - 'hub_item_name': hub_item_name - }) + response = call_hub_method("unpublish_item", {"hub_item_name": hub_item_name}) if response: - frappe.db.set_value('Item', item_code, 'publish_in_hub', 0) - frappe.delete_doc('Hub Tracked Item', item_code) + frappe.db.set_value("Item", item_code, "publish_in_hub", 0) + frappe.delete_doc("Hub Tracked Item", item_code) else: - frappe.throw(_('Unable to update remote activity')) + frappe.throw(_("Unable to update remote activity")) + @frappe.whitelist() def get_unregistered_users(): - settings = frappe.get_single('Marketplace Settings') - registered_users = [user.user for user in settings.users] + ['Administrator', 'Guest'] - all_users = [user.name for user in frappe.db.get_all('User', filters={'enabled': 1})] + settings = frappe.get_single("Marketplace Settings") + registered_users = [user.user for user in settings.users] + ["Administrator", "Guest"] + all_users = [user.name for user in frappe.db.get_all("User", filters={"enabled": 1})] unregistered_users = [user for user in all_users if user not in registered_users] return unregistered_users def item_sync_preprocess(intended_item_publish_count): - response = call_hub_method('pre_items_publish', { - 'intended_item_publish_count': intended_item_publish_count - }) + response = call_hub_method( + "pre_items_publish", {"intended_item_publish_count": intended_item_publish_count} + ) if response: frappe.db.set_value("Marketplace Settings", "Marketplace Settings", "sync_in_progress", 1) return response else: - frappe.throw(_('Unable to update remote activity')) + frappe.throw(_("Unable to update remote activity")) def item_sync_postprocess(): - response = call_hub_method('post_items_publish', {}) + response = call_hub_method("post_items_publish", {}) if response: - frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'last_sync_datetime', frappe.utils.now()) + frappe.db.set_value( + "Marketplace Settings", "Marketplace Settings", "last_sync_datetime", frappe.utils.now() + ) else: - frappe.throw(_('Unable to update remote activity')) + frappe.throw(_("Unable to update remote activity")) - frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'sync_in_progress', 0) + frappe.db.set_value("Marketplace Settings", "Marketplace Settings", "sync_in_progress", 0) def convert_relative_image_urls_to_absolute(items): from six.moves.urllib.parse import urljoin for item in items: - file_path = item['image'] + file_path = item["image"] - if file_path.startswith('/files/'): - item['image'] = urljoin(frappe.utils.get_url(), file_path) + if file_path.startswith("/files/"): + item["image"] = urljoin(frappe.utils.get_url(), file_path) def get_hub_connection(): - settings = frappe.get_single('Marketplace Settings') + settings = frappe.get_single("Marketplace Settings") marketplace_url = settings.marketplace_url hub_user = settings.get_hub_user(frappe.session.user) diff --git a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py index af2ff37797e..e8b68cbc0e7 100644 --- a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py +++ b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.py @@ -10,82 +10,82 @@ from frappe.utils import cint class MarketplaceSettings(Document): - def register_seller(self, company, company_description): - country, currency, company_logo = frappe.db.get_value('Company', company, - ['country', 'default_currency', 'company_logo']) + country, currency, company_logo = frappe.db.get_value( + "Company", company, ["country", "default_currency", "company_logo"] + ) company_details = { - 'company': company, - 'country': country, - 'currency': currency, - 'company_description': company_description, - 'company_logo': company_logo, - 'site_name': frappe.utils.get_url() + "company": company, + "country": country, + "currency": currency, + "company_description": company_description, + "company_logo": company_logo, + "site_name": frappe.utils.get_url(), } hub_connection = self.get_connection() - response = hub_connection.post_request({ - 'cmd': 'hub.hub.api.add_hub_seller', - 'company_details': json.dumps(company_details) - }) + response = hub_connection.post_request( + {"cmd": "hub.hub.api.add_hub_seller", "company_details": json.dumps(company_details)} + ) return response - def add_hub_user(self, user_email): - '''Create a Hub User and User record on hub server + """Create a Hub User and User record on hub server and if successfull append it to Hub User table - ''' + """ if not self.registered: return hub_connection = self.get_connection() - first_name, last_name = frappe.db.get_value('User', user_email, ['first_name', 'last_name']) + first_name, last_name = frappe.db.get_value("User", user_email, ["first_name", "last_name"]) - hub_user = hub_connection.post_request({ - 'cmd': 'hub.hub.api.add_hub_user', - 'user_email': user_email, - 'first_name': first_name, - 'last_name': last_name, - 'hub_seller': self.hub_seller_name - }) + hub_user = hub_connection.post_request( + { + "cmd": "hub.hub.api.add_hub_user", + "user_email": user_email, + "first_name": first_name, + "last_name": last_name, + "hub_seller": self.hub_seller_name, + } + ) - self.append('users', { - 'user': hub_user.get('user_email'), - 'hub_user_name': hub_user.get('hub_user_name'), - 'password': hub_user.get('password') - }) + self.append( + "users", + { + "user": hub_user.get("user_email"), + "hub_user_name": hub_user.get("hub_user_name"), + "password": hub_user.get("password"), + }, + ) self.save() def get_hub_user(self, user): - '''Return the Hub User doc from the `users` table if password is set''' + """Return the Hub User doc from the `users` table if password is set""" - filtered_users = list(filter( - lambda x: x.user == user and x.password, - self.users - )) + filtered_users = list(filter(lambda x: x.user == user and x.password, self.users)) if filtered_users: return filtered_users[0] - def get_connection(self): return FrappeClient(self.marketplace_url) - def unregister(self): """Disable the User on hubmarket.org""" + @frappe.whitelist() def is_marketplace_enabled(): - if not hasattr(frappe.local, 'is_marketplace_enabled'): - frappe.local.is_marketplace_enabled = cint(frappe.db.get_single_value('Marketplace Settings', - 'disable_marketplace')) + if not hasattr(frappe.local, "is_marketplace_enabled"): + frappe.local.is_marketplace_enabled = cint( + frappe.db.get_single_value("Marketplace Settings", "disable_marketplace") + ) return frappe.local.is_marketplace_enabled diff --git a/erpnext/hub_node/legacy.py b/erpnext/hub_node/legacy.py index b19167bfefe..8d95c0706c2 100644 --- a/erpnext/hub_node/legacy.py +++ b/erpnext/hub_node/legacy.py @@ -1,4 +1,3 @@ - import json import frappe @@ -11,9 +10,10 @@ from frappe.utils.nestedset import get_root_of def get_list(doctype, start, limit, fields, filters, order_by): pass + def get_hub_connection(): - if frappe.db.exists('Data Migration Connector', 'Hub Connector'): - hub_connector = frappe.get_doc('Data Migration Connector', 'Hub Connector') + if frappe.db.exists("Data Migration Connector", "Hub Connector"): + hub_connector = frappe.get_doc("Data Migration Connector", "Hub Connector") hub_connection = hub_connector.get_connection() return hub_connection.connection @@ -21,10 +21,11 @@ def get_hub_connection(): hub_connection = FrappeClient(frappe.conf.hub_url) return hub_connection + def make_opportunity(buyer_name, email_id): buyer_name = "HUB-" + buyer_name - if not frappe.db.exists('Lead', {'email_id': email_id}): + if not frappe.db.exists("Lead", {"email_id": email_id}): lead = frappe.new_doc("Lead") lead.lead_name = buyer_name lead.email_id = email_id @@ -32,9 +33,10 @@ def make_opportunity(buyer_name, email_id): o = frappe.new_doc("Opportunity") o.opportunity_from = "Lead" - o.lead = frappe.get_all("Lead", filters={"email_id": email_id}, fields = ["name"])[0]["name"] + o.lead = frappe.get_all("Lead", filters={"email_id": email_id}, fields=["name"])[0]["name"] o.save(ignore_permissions=True) + @frappe.whitelist() def make_rfq_and_send_opportunity(item, supplier): supplier = make_supplier(supplier) @@ -43,105 +45,110 @@ def make_rfq_and_send_opportunity(item, supplier): rfq = make_rfq(item, supplier, contact) status = send_opportunity(contact) - return { - 'rfq': rfq, - 'hub_document_created': status - } + return {"rfq": rfq, "hub_document_created": status} + def make_supplier(supplier): # make supplier if not already exists supplier = frappe._dict(json.loads(supplier)) - if not frappe.db.exists('Supplier', {'supplier_name': supplier.supplier_name}): - supplier_doc = frappe.get_doc({ - 'doctype': 'Supplier', - 'supplier_name': supplier.supplier_name, - 'supplier_group': supplier.supplier_group, - 'supplier_email': supplier.supplier_email - }).insert() + if not frappe.db.exists("Supplier", {"supplier_name": supplier.supplier_name}): + supplier_doc = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": supplier.supplier_name, + "supplier_group": supplier.supplier_group, + "supplier_email": supplier.supplier_email, + } + ).insert() else: - supplier_doc = frappe.get_doc('Supplier', supplier.supplier_name) + supplier_doc = frappe.get_doc("Supplier", supplier.supplier_name) return supplier_doc + def make_contact(supplier): - contact_name = get_default_contact('Supplier', supplier.supplier_name) + contact_name = get_default_contact("Supplier", supplier.supplier_name) # make contact if not already exists if not contact_name: - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': supplier.supplier_name, - 'is_primary_contact': 1, - 'links': [ - {'link_doctype': 'Supplier', 'link_name': supplier.supplier_name} - ] - }) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": supplier.supplier_name, + "is_primary_contact": 1, + "links": [{"link_doctype": "Supplier", "link_name": supplier.supplier_name}], + } + ) contact.add_email(supplier.supplier_email, is_primary=True) contact.insert() else: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) return contact + def make_item(item): # make item if not already exists item = frappe._dict(json.loads(item)) - if not frappe.db.exists('Item', {'item_code': item.item_code}): - item_doc = frappe.get_doc({ - 'doctype': 'Item', - 'item_code': item.item_code, - 'item_group': item.item_group, - 'is_item_from_hub': 1 - }).insert() + if not frappe.db.exists("Item", {"item_code": item.item_code}): + item_doc = frappe.get_doc( + { + "doctype": "Item", + "item_code": item.item_code, + "item_group": item.item_group, + "is_item_from_hub": 1, + } + ).insert() else: - item_doc = frappe.get_doc('Item', item.item_code) + item_doc = frappe.get_doc("Item", item.item_code) return item_doc + def make_rfq(item, supplier, contact): # make rfq - rfq = frappe.get_doc({ - 'doctype': 'Request for Quotation', - 'transaction_date': nowdate(), - 'status': 'Draft', - 'company': frappe.db.get_single_value('Marketplace Settings', 'company'), - 'message_for_supplier': 'Please supply the specified items at the best possible rates', - 'suppliers': [ - { 'supplier': supplier.name, 'contact': contact.name } - ], - 'items': [ - { - 'item_code': item.item_code, - 'qty': 1, - 'schedule_date': nowdate(), - 'warehouse': item.default_warehouse or get_root_of("Warehouse"), - 'description': item.description, - 'uom': item.stock_uom - } - ] - }).insert() + rfq = frappe.get_doc( + { + "doctype": "Request for Quotation", + "transaction_date": nowdate(), + "status": "Draft", + "company": frappe.db.get_single_value("Marketplace Settings", "company"), + "message_for_supplier": "Please supply the specified items at the best possible rates", + "suppliers": [{"supplier": supplier.name, "contact": contact.name}], + "items": [ + { + "item_code": item.item_code, + "qty": 1, + "schedule_date": nowdate(), + "warehouse": item.default_warehouse or get_root_of("Warehouse"), + "description": item.description, + "uom": item.stock_uom, + } + ], + } + ).insert() rfq.save() rfq.submit() return rfq + def send_opportunity(contact): # Make Hub Message on Hub with lead data doc = { - 'doctype': 'Lead', - 'lead_name': frappe.db.get_single_value('Marketplace Settings', 'company'), - 'email_id': frappe.db.get_single_value('Marketplace Settings', 'user') + "doctype": "Lead", + "lead_name": frappe.db.get_single_value("Marketplace Settings", "company"), + "email_id": frappe.db.get_single_value("Marketplace Settings", "user"), } - args = frappe._dict(dict( - doctype='Hub Message', - reference_doctype='Lead', - data=json.dumps(doc), - user=contact.email_id - )) + args = frappe._dict( + dict( + doctype="Hub Message", reference_doctype="Lead", data=json.dumps(doc), user=contact.email_id + ) + ) connection = get_hub_connection() - response = connection.insert('Hub Message', args) + response = connection.insert("Hub Message", args) return response.ok diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py index 9512c8fa195..03eed801fcb 100644 --- a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py +++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py @@ -13,10 +13,19 @@ from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applic @frappe.whitelist() @cache_source -def get_data(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, heatmap_year = None): +def get_data( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): if chart_name: - chart = frappe.get_doc('Dashboard Chart', chart_name) + chart = frappe.get_doc("Dashboard Chart", chart_name) else: chart = frappe._dict(frappe.parse_json(chart)) @@ -30,28 +39,44 @@ def get_data(chart_name = None, chart = None, no_cache = None, filters = None, f labels = [] values = [] - if filters.get('company'): + if filters.get("company"): conditions = "AND company = %(company)s" loan_security_details = get_loan_security_details() - unpledges = frappe._dict(frappe.db.sql(""" + unpledges = frappe._dict( + frappe.db.sql( + """ SELECT u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY u.loan_security - """.format(conditions=conditions), filters, as_list=1)) + """.format( + conditions=conditions + ), + filters, + as_list=1, + ) + ) - pledges = frappe._dict(frappe.db.sql(""" + pledges = frappe._dict( + frappe.db.sql( + """ SELECT p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY p.loan_security - """.format(conditions=conditions), filters, as_list=1)) + """.format( + conditions=conditions + ), + filters, + as_list=1, + ) + ) for security, qty in iteritems(pledges): current_pledges.setdefault(security, qty) @@ -61,19 +86,15 @@ def get_data(chart_name = None, chart = None, no_cache = None, filters = None, f count = 0 for security, qty in iteritems(sorted_pledges): - values.append(qty * loan_security_details.get(security, {}).get('latest_price', 0)) + values.append(qty * loan_security_details.get(security, {}).get("latest_price", 0)) labels.append(security) - count +=1 + count += 1 ## Just need top 10 securities if count == 10: break return { - 'labels': labels, - 'datasets': [{ - 'name': 'Top 10 Securities', - 'chartType': 'bar', - 'values': values - }] + "labels": labels, + "datasets": [{"name": "Top 10 Securities", "chartType": "bar", "values": values}], } diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index f3914d51286..03ec4014eec 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -21,7 +21,7 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled class Loan(AccountsController): def validate(self): - if self.applicant_type == 'Employee' and self.repay_from_salary: + if self.applicant_type == "Employee" and self.repay_from_salary: validate_employee_currency_with_company_currency(self.applicant, self.company) self.set_loan_amount() self.validate_loan_amount() @@ -31,27 +31,40 @@ class Loan(AccountsController): self.validate_repay_from_salary() if self.is_term_loan: - validate_repayment_method(self.repayment_method, self.loan_amount, self.monthly_repayment_amount, - self.repayment_periods, self.is_term_loan) + validate_repayment_method( + self.repayment_method, + self.loan_amount, + self.monthly_repayment_amount, + self.repayment_periods, + self.is_term_loan, + ) self.make_repayment_schedule() self.set_repayment_period() self.calculate_totals() def validate_accounts(self): - for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: - company = frappe.get_value("Account", self.get(fieldname), 'company') + for fieldname in [ + "payment_account", + "loan_account", + "interest_income_account", + "penalty_income_account", + ]: + company = frappe.get_value("Account", self.get(fieldname), "company") if company != self.company: - frappe.throw(_("Account {0} does not belongs to company {1}").format(frappe.bold(self.get(fieldname)), - frappe.bold(self.company))) + frappe.throw( + _("Account {0} does not belongs to company {1}").format( + frappe.bold(self.get(fieldname)), frappe.bold(self.company) + ) + ) def on_submit(self): self.link_loan_security_pledge() def on_cancel(self): self.unlink_loan_security_pledge() - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] def set_missing_fields(self): if not self.company: @@ -64,15 +77,25 @@ class Loan(AccountsController): self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest") if self.repayment_method == "Repay Over Number of Periods": - self.monthly_repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods) + self.monthly_repayment_amount = get_monthly_repayment_amount( + self.loan_amount, self.rate_of_interest, self.repayment_periods + ) def check_sanctioned_amount_limit(self): - sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit( + self.applicant_type, self.applicant, self.company + ) if sanctioned_amount_limit: total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) - if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): - frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt( + sanctioned_amount_limit + ): + frappe.throw( + _("Sanctioned Amount limit crossed for {0} {1}").format( + self.applicant_type, frappe.bold(self.applicant) + ) + ) def validate_repay_from_salary(self): if not self.is_term_loan and self.repay_from_salary: @@ -85,8 +108,8 @@ class Loan(AccountsController): self.repayment_schedule = [] payment_date = self.repayment_start_date balance_amount = self.loan_amount - while(balance_amount > 0): - interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12*100)) + while balance_amount > 0: + interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100)) principal_amount = self.monthly_repayment_amount - interest_amount balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) if balance_amount < 0: @@ -94,13 +117,16 @@ class Loan(AccountsController): balance_amount = 0.0 total_payment = principal_amount + interest_amount - self.append("repayment_schedule", { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount - }) + self.append( + "repayment_schedule", + { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_amount, + }, + ) next_payment_date = add_single_month(payment_date) payment_date = next_payment_date @@ -118,14 +144,13 @@ class Loan(AccountsController): if self.is_term_loan: for data in self.repayment_schedule: self.total_payment += data.total_payment - self.total_interest_payable +=data.interest_amount + self.total_interest_payable += data.interest_amount else: self.total_payment = self.loan_amount def set_loan_amount(self): if self.loan_application and not self.loan_amount: - self.loan_amount = frappe.db.get_value('Loan Application', self.loan_application, 'loan_amount') - + self.loan_amount = frappe.db.get_value("Loan Application", self.loan_application, "loan_amount") def validate_loan_amount(self): if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: @@ -137,30 +162,36 @@ class Loan(AccountsController): def link_loan_security_pledge(self): if self.is_secured_loan and self.loan_application: - maximum_loan_value = frappe.db.get_value('Loan Security Pledge', - { - 'loan_application': self.loan_application, - 'status': 'Requested' - }, - 'sum(maximum_loan_value)' + maximum_loan_value = frappe.db.get_value( + "Loan Security Pledge", + {"loan_application": self.loan_application, "status": "Requested"}, + "sum(maximum_loan_value)", ) if maximum_loan_value: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLoan Security Pledge` SET loan = %s, pledge_time = %s, status = 'Pledged' WHERE status = 'Requested' and loan_application = %s - """, (self.name, now_datetime(), self.loan_application)) + """, + (self.name, now_datetime(), self.loan_application), + ) - self.db_set('maximum_loan_amount', maximum_loan_value) + self.db_set("maximum_loan_amount", maximum_loan_value) def unlink_loan_security_pledge(self): - pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name}) + pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name}) pledge_list = [d.name for d in pledges] if pledge_list: - frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET + frappe.db.sql( + """UPDATE `tabLoan Security Pledge` SET loan = '', status = 'Unpledged' - where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec + where name in (%s) """ + % (", ".join(["%s"] * len(pledge_list))), + tuple(pledge_list), + ) # nosec + def update_total_amount_paid(doc): total_amount_paid = 0 @@ -169,24 +200,51 @@ def update_total_amount_paid(doc): total_amount_paid += data.total_payment frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) + def get_total_loan_amount(applicant_type, applicant, company): pending_amount = 0 - loan_details = frappe.db.get_all("Loan", - filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1, - "status": ("!=", "Closed")}, - fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid", - "written_off_amount"]) + loan_details = frappe.db.get_all( + "Loan", + filters={ + "applicant_type": applicant_type, + "company": company, + "applicant": applicant, + "docstatus": 1, + "status": ("!=", "Closed"), + }, + fields=[ + "status", + "total_payment", + "disbursed_amount", + "total_interest_payable", + "total_principal_paid", + "written_off_amount", + ], + ) - interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type, - "company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)")) + interest_amount = flt( + frappe.db.get_value( + "Loan Interest Accrual", + {"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1}, + "sum(interest_amount - paid_interest_amount)", + ) + ) for loan in loan_details: if loan.status in ("Disbursed", "Loan Closure Requested"): - pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + pending_amount += ( + flt(loan.total_payment) + - flt(loan.total_interest_payable) + - flt(loan.total_principal_paid) + - flt(loan.written_off_amount) + ) elif loan.status == "Partially Disbursed": - pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) - flt(loan.written_off_amount) + pending_amount += ( + flt(loan.disbursed_amount) + - flt(loan.total_interest_payable) + - flt(loan.total_principal_paid) + - flt(loan.written_off_amount) + ) elif loan.status == "Sanctioned": pending_amount += flt(loan.total_payment) @@ -194,12 +252,18 @@ def get_total_loan_amount(applicant_type, applicant, company): return pending_amount -def get_sanctioned_amount_limit(applicant_type, applicant, company): - return frappe.db.get_value('Sanctioned Loan Amount', - {'applicant_type': applicant_type, 'company': company, 'applicant': applicant}, - 'sanctioned_amount_limit') -def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan): +def get_sanctioned_amount_limit(applicant_type, applicant, company): + return frappe.db.get_value( + "Sanctioned Loan Amount", + {"applicant_type": applicant_type, "company": company, "applicant": applicant}, + "sanctioned_amount_limit", + ) + + +def validate_repayment_method( + repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan +): if is_term_loan and not repayment_method: frappe.throw(_("Repayment Method is mandatory for term loans")) @@ -213,27 +277,34 @@ def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_a if monthly_repayment_amount > loan_amount: frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount")) + def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods): if rate_of_interest: - monthly_interest_rate = flt(rate_of_interest) / (12 *100) - monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate * - (1 + monthly_interest_rate)**repayment_periods) \ - / ((1 + monthly_interest_rate)**repayment_periods - 1)) + monthly_interest_rate = flt(rate_of_interest) / (12 * 100) + monthly_repayment_amount = math.ceil( + (loan_amount * monthly_interest_rate * (1 + monthly_interest_rate) ** repayment_periods) + / ((1 + monthly_interest_rate) ** repayment_periods - 1) + ) else: monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) return monthly_repayment_amount + @frappe.whitelist() def request_loan_closure(loan, posting_date=None): if not posting_date: posting_date = getdate() amounts = calculate_amounts(loan, posting_date) - pending_amount = amounts['pending_principal_amount'] + amounts['unaccrued_interest'] + \ - amounts['interest_amount'] + amounts['penalty_amount'] + pending_amount = ( + amounts["pending_principal_amount"] + + amounts["unaccrued_interest"] + + amounts["interest_amount"] + + amounts["penalty_amount"] + ) - loan_type = frappe.get_value('Loan', loan, 'loan_type') - write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount') + loan_type = frappe.get_value("Loan", loan, "loan_type") + write_off_limit = frappe.get_value("Loan Type", loan_type, "write_off_amount") if pending_amount and abs(pending_amount) < write_off_limit: # Auto create loan write off and update status as loan closure requested @@ -242,7 +313,8 @@ def request_loan_closure(loan, posting_date=None): elif pending_amount > 0: frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount)) - frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested') + frappe.db.set_value("Loan", loan, "status", "Loan Closure Requested") + @frappe.whitelist() def get_loan_application(loan_application): @@ -250,10 +322,12 @@ def get_loan_application(loan_application): if loan: return loan.as_dict() + def close_loan(loan, total_amount_paid): frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid) frappe.db.set_value("Loan", loan, "status", "Closed") + @frappe.whitelist() def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0): disbursement_entry = frappe.new_doc("Loan Disbursement") @@ -270,6 +344,7 @@ def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amo else: return disbursement_entry + @frappe.whitelist() def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0): repayment_entry = frappe.new_doc("Loan Repayment") @@ -285,27 +360,28 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as else: return repayment_entry + @frappe.whitelist() def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0): if not company: - company = frappe.get_value('Loan', loan, 'company') + company = frappe.get_value("Loan", loan, "company") if not posting_date: posting_date = getdate() amounts = calculate_amounts(loan, posting_date) - pending_amount = amounts['pending_principal_amount'] + pending_amount = amounts["pending_principal_amount"] if amount and (amount > pending_amount): - frappe.throw(_('Write Off amount cannot be greater than pending loan amount')) + frappe.throw(_("Write Off amount cannot be greater than pending loan amount")) if not amount: amount = pending_amount # get default write off account from company master - write_off_account = frappe.get_value('Company', company, 'write_off_account') + write_off_account = frappe.get_value("Company", company, "write_off_account") - write_off = frappe.new_doc('Loan Write Off') + write_off = frappe.new_doc("Loan Write Off") write_off.loan = loan write_off.posting_date = posting_date write_off.write_off_account = write_off_account @@ -317,26 +393,35 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict else: return write_off + @frappe.whitelist() -def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0): +def unpledge_security( + loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0 +): # if no security_map is passed it will be considered as full unpledge if security_map and isinstance(security_map, string_types): security_map = json.loads(security_map) if loan: pledge_qty_map = security_map or get_pledged_security_qty(loan) - loan_doc = frappe.get_doc('Loan', loan) - unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, - loan_doc.applicant_type, loan_doc.applicant) + loan_doc = frappe.get_doc("Loan", loan) + unpledge_request = create_loan_security_unpledge( + pledge_qty_map, loan_doc.name, loan_doc.company, loan_doc.applicant_type, loan_doc.applicant + ) # will unpledge qty based on loan security pledge elif loan_security_pledge: security_map = {} - pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge) + pledge_doc = frappe.get_doc("Loan Security Pledge", loan_security_pledge) for security in pledge_doc.securities: security_map.setdefault(security.loan_security, security.qty) - unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan, - pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant) + unpledge_request = create_loan_security_unpledge( + security_map, + pledge_doc.loan, + pledge_doc.company, + pledge_doc.applicant_type, + pledge_doc.applicant, + ) if save: unpledge_request.save() @@ -346,16 +431,17 @@ def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, a if approve: if unpledge_request.docstatus == 1: - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() else: - frappe.throw(_('Only submittted unpledge requests can be approved')) + frappe.throw(_("Only submittted unpledge requests can be approved")) if as_dict: return unpledge_request else: return unpledge_request + def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant): unpledge_request = frappe.new_doc("Loan Security Unpledge") unpledge_request.applicant_type = applicant_type @@ -365,17 +451,16 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a for security, qty in unpledge_map.items(): if qty: - unpledge_request.append('securities', { - "loan_security": security, - "qty": qty - }) + unpledge_request.append("securities", {"loan_security": security, "qty": qty}) return unpledge_request + def validate_employee_currency_with_company_currency(applicant, company): from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import ( get_employee_currency, ) + if not applicant: frappe.throw(_("Please select Applicant")) if not company: @@ -383,18 +468,20 @@ def validate_employee_currency_with_company_currency(applicant, company): employee_currency = get_employee_currency(applicant) company_currency = erpnext.get_company_currency(company) if employee_currency != company_currency: - frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}") - .format(applicant, employee_currency)) + frappe.throw( + _( + "Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}" + ).format(applicant, employee_currency) + ) + @frappe.whitelist() def get_shortfall_applicants(): - loans = frappe.get_all('Loan Security Shortfall', {'status': 'Pending'}, pluck='loan') - applicants = set(frappe.get_all('Loan', {'name': ('in', loans)}, pluck='name')) + loans = frappe.get_all("Loan Security Shortfall", {"status": "Pending"}, pluck="loan") + applicants = set(frappe.get_all("Loan", {"name": ("in", loans)}, pluck="name")) + + return {"value": len(applicants), "fieldtype": "Int"} - return { - "value": len(applicants), - "fieldtype": "Int" - } def add_single_month(date): if getdate(date) == get_last_day(date): @@ -402,29 +489,46 @@ def add_single_month(date): else: return add_months(date, 1) + @frappe.whitelist() def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0): - loan_details = frappe.db.get_value('Loan', loan, ['applicant_type', 'applicant', - 'loan_account', 'payment_account', 'posting_date', 'company', 'name', - 'total_payment', 'total_principal_paid'], as_dict=1) + loan_details = frappe.db.get_value( + "Loan", + loan, + [ + "applicant_type", + "applicant", + "loan_account", + "payment_account", + "posting_date", + "company", + "name", + "total_payment", + "total_principal_paid", + ], + as_dict=1, + ) - loan_details.doctype = 'Loan' + loan_details.doctype = "Loan" loan_details[loan_details.applicant_type.lower()] = loan_details.applicant if not amount: amount = flt(loan_details.total_principal_paid - loan_details.total_payment) if amount < 0: - frappe.throw(_('No excess amount pending for refund')) + frappe.throw(_("No excess amount pending for refund")) - refund_jv = get_payment_entry(loan_details, { - "party_type": loan_details.applicant_type, - "party_account": loan_details.loan_account, - "amount_field_party": 'debit_in_account_currency', - "amount_field_bank": 'credit_in_account_currency', - "amount": amount, - "bank_account": loan_details.payment_account - }) + refund_jv = get_payment_entry( + loan_details, + { + "party_type": loan_details.applicant_type, + "party_account": loan_details.loan_account, + "amount_field_party": "debit_in_account_currency", + "amount_field_bank": "credit_in_account_currency", + "amount": amount, + "bank_account": loan_details.payment_account, + }, + ) if reference_number: refund_jv.cheque_no = reference_number @@ -435,4 +539,4 @@ def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, s if submit: refund_jv.submit() - return refund_jv \ No newline at end of file + return refund_jv diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py index 0374eda4991..971d5450eaa 100644 --- a/erpnext/loan_management/doctype/loan/loan_dashboard.py +++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py @@ -1,18 +1,19 @@ - - def get_data(): return { - 'fieldname': 'loan', - 'non_standard_fieldnames': { - 'Loan Disbursement': 'against_loan', - 'Loan Repayment': 'against_loan', + "fieldname": "loan", + "non_standard_fieldnames": { + "Loan Disbursement": "against_loan", + "Loan Repayment": "against_loan", }, - 'transactions': [ + "transactions": [ + {"items": ["Loan Security Pledge", "Loan Security Shortfall", "Loan Disbursement"]}, { - 'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement'] + "items": [ + "Loan Repayment", + "Loan Interest Accrual", + "Loan Write Off", + "Loan Security Unpledge", + ] }, - { - 'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge'] - } - ] + ], } diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5ebb2e1bdce..e2b0870c322 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -39,31 +39,69 @@ from erpnext.selling.doctype.customer.test_customer import get_customer_dict class TestLoan(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Personal Loan", 500000, 8.4, + create_loan_type( + "Personal Loan", + 500000, + 8.4, is_term_loan=1, - mode_of_payment='Cash', - disbursement_account='Disbursement Account - _TC', - payment_account='Payment Account - _TC', - loan_account='Loan Account - _TC', - interest_income_account='Interest Income Account - _TC', - penalty_income_account='Penalty Income Account - _TC') + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + ) - create_loan_type("Stock Loan", 2000000, 13.5, 25, 1, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Stock Loan", + 2000000, + 13.5, + 25, + 1, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Demand Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) create_loan_security_type() create_loan_security() - create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) - create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price( + "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) + create_loan_security_price( + "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) self.applicant1 = make_employee("robert_loan@loan.com") - make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company") + make_salary_structure( + "Test Salary Structure Loan", + "Monthly", + employee=self.applicant1, + currency="INR", + company="_Test Company", + ) if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) if not frappe.db.exists("Customer", "_Test Loan Customer 1"): frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True) @@ -74,7 +112,7 @@ class TestLoan(unittest.TestCase): create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) def test_loan(self): - loan = frappe.get_doc("Loan", {"applicant":self.applicant1}) + loan = frappe.get_doc("Loan", {"applicant": self.applicant1}) self.assertEqual(loan.monthly_repayment_amount, 15052) self.assertEqual(flt(loan.total_interest_payable, 0), 21034) self.assertEqual(flt(loan.total_payment, 0), 301034) @@ -83,7 +121,11 @@ class TestLoan(unittest.TestCase): self.assertEqual(len(schedule), 20) - for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227080], [19, 14941, 105, 0], [17, 14740, 312, 29785]]: + for idx, principal_amount, interest_amount, balance_loan_amount in [ + [3, 13369, 1683, 227080], + [19, 14941, 105, 0], + [17, 14740, 312, 29785], + ]: self.assertEqual(flt(schedule[idx].principal_amount, 0), principal_amount) self.assertEqual(flt(schedule[idx].interest_amount, 0), interest_amount) self.assertEqual(flt(schedule[idx].balance_loan_amount, 0), balance_loan_amount) @@ -98,30 +140,35 @@ class TestLoan(unittest.TestCase): def test_loan_with_security(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00, - }] + pledge = [ + { + "loan_security": "Test Security 1", + "qty": 4000.00, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledge, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", - 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) self.assertEqual(loan.loan_amount, 1000000) def test_loan_disbursement(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Stock Loan', pledge, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) self.assertEqual(loan.loan_amount, 1000000) loan.submit() @@ -130,14 +177,16 @@ class TestLoan(unittest.TestCase): loan_disbursement_entry2 = make_loan_disbursement_entry(loan.name, 500000) loan = frappe.get_doc("Loan", loan.name) - gl_entries1 = frappe.db.get_all("GL Entry", + gl_entries1 = frappe.db.get_all( + "GL Entry", fields=["name"], - filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry1.name} + filters={"voucher_type": "Loan Disbursement", "voucher_no": loan_disbursement_entry1.name}, ) - gl_entries2 = frappe.db.get_all("GL Entry", + gl_entries2 = frappe.db.get_all( + "GL Entry", fields=["name"], - filters = {'voucher_type': 'Loan Disbursement', 'voucher_no': loan_disbursement_entry2.name} + filters={"voucher_type": "Loan Disbursement", "voucher_no": loan_disbursement_entry2.name}, ) self.assertEqual(loan.status, "Disbursed") @@ -151,73 +200,93 @@ class TestLoan(unittest.TestCase): frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'") frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'") - if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer", - "applicant": "_Test Loan Customer 1", "company": "_Test Company"}): - frappe.get_doc({ - "doctype": "Sanctioned Loan Amount", + if not frappe.db.get_value( + "Sanctioned Loan Amount", + filters={ "applicant_type": "Customer", "applicant": "_Test Loan Customer 1", - "sanctioned_amount_limit": 1500000, - "company": "_Test Company" - }).insert(ignore_permissions=True) + "company": "_Test Company", + }, + ): + frappe.get_doc( + { + "doctype": "Sanctioned Loan Amount", + "applicant_type": "Customer", + "applicant": "_Test Loan Customer 1", + "sanctioned_amount_limit": 1500000, + "company": "_Test Company", + } + ).insert(ignore_permissions=True) # Make First Loan - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant3, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant3, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() # Make second loan greater than the sanctioned amount - loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge, - do_not_save=True) + loan_application = create_loan_application( + "_Test Company", self.applicant3, "Demand Loan", pledge, do_not_save=True + ) self.assertRaises(frappe.ValidationError, loan_application.save) def test_regular_loan_repayment(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days) - / (days_in_year(get_datetime(first_date).year) * 100), 2) + accrued_interest_amount = flt( + (loan.loan_amount * loan.rate_of_interest * no_of_days) + / (days_in_year(get_datetime(first_date).year) * 100), + 2, + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 10), 111119 + ) repayment_entry.save() repayment_entry.submit() penalty_amount = (accrued_interest_amount * 5 * 25) / 100 - self.assertEqual(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0)) + self.assertEqual(flt(repayment_entry.penalty_amount, 0), flt(penalty_amount, 0)) - amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount']) + amounts = frappe.db.get_all( + "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount"] + ) loan.load_from_db() - total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount'] - self.assertEqual(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable) - self.assertEqual(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid - - penalty_amount - total_interest_paid, 0)) + total_interest_paid = amounts[0]["paid_interest_amount"] + amounts[1]["paid_interest_amount"] + self.assertEqual(amounts[1]["paid_interest_amount"], repayment_entry.interest_payable) + self.assertEqual( + flt(loan.total_principal_paid, 0), + flt(repayment_entry.amount_paid - penalty_amount - total_interest_paid, 0), + ) # Check Repayment Entry cancel repayment_entry.load_from_db() @@ -228,21 +297,22 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.total_principal_paid, 0) def test_loan_closure(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 @@ -251,20 +321,27 @@ class TestLoan(unittest.TestCase): # 5 days as well though in grace period no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount), + ) repayment_entry.submit() - amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + amount = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"] + ) - self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0)) self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0) request_loan_closure(loan.name) @@ -272,78 +349,101 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, "Loan Closure Requested") def test_loan_repayment_for_term_loan(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 4000.00 - }, - { - "loan_security": "Test Security 1", - "qty": 2000.00 - }] + pledges = [ + {"loan_security": "Test Security 2", "qty": 4000.00}, + {"loan_security": "Test Security 1", "qty": 2000.00}, + ] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Stock Loan', pledges, - "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application, - posting_date=add_months(nowdate(), -1)) + loan = create_loan_with_security( + self.applicant2, + "Stock Loan", + "Repay Over Number of Periods", + 12, + loan_application, + posting_date=add_months(nowdate(), -1), + ) loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + make_loan_disbursement_entry( + loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1) + ) process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75 + ) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amounts = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount", "paid_principal_amount"] + ) self.assertEqual(amounts[0], 11250.00) self.assertEqual(amounts[1], 78303.00) def test_repayment_schedule_update(self): - loan = create_loan(self.applicant2, "Personal Loan", 200000, "Repay Over Number of Periods", 4, - applicant_type='Customer', repayment_start_date='2021-04-30', posting_date='2021-04-01') + loan = create_loan( + self.applicant2, + "Personal Loan", + 200000, + "Repay Over Number of Periods", + 4, + applicant_type="Customer", + repayment_start_date="2021-04-30", + posting_date="2021-04-01", + ) loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date='2021-04-01') + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date="2021-04-01") - process_loan_interest_accrual_for_term_loans(posting_date='2021-05-01') - process_loan_interest_accrual_for_term_loans(posting_date='2021-06-01') + process_loan_interest_accrual_for_term_loans(posting_date="2021-05-01") + process_loan_interest_accrual_for_term_loans(posting_date="2021-06-01") - repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2021-06-05', 120000) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, "2021-06-05", 120000) repayment_entry.submit() loan.load_from_db() - self.assertEqual(flt(loan.get('repayment_schedule')[3].principal_amount, 2), 41369.83) - self.assertEqual(flt(loan.get('repayment_schedule')[3].interest_amount, 2), 289.59) - self.assertEqual(flt(loan.get('repayment_schedule')[3].total_payment, 2), 41659.41) - self.assertEqual(flt(loan.get('repayment_schedule')[3].balance_loan_amount, 2), 0) + self.assertEqual(flt(loan.get("repayment_schedule")[3].principal_amount, 2), 41369.83) + self.assertEqual(flt(loan.get("repayment_schedule")[3].interest_amount, 2), 289.59) + self.assertEqual(flt(loan.get("repayment_schedule")[3].total_payment, 2), 41659.41) + self.assertEqual(flt(loan.get("repayment_schedule")[3].balance_loan_amount, 2), 0) def test_security_shortfall(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 8000.00, - "haircut": 50, - }] + pledges = [ + { + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) loan.submit() make_loan_disbursement_entry(loan.name, loan.loan_amount) - frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 - where loan_security='Test Security 2'""") + frappe.db.sql( + """UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""" + ) create_process_loan_security_shortfall() loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) @@ -353,8 +453,10 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan_security_shortfall.security_value, 800000.00) self.assertEqual(loan_security_shortfall.shortfall_amount, 600000.00) - frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 - where loan_security='Test Security 2'""") + frappe.db.sql( + """ UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""" + ) create_process_loan_security_shortfall() loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) @@ -362,33 +464,40 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan_security_shortfall.shortfall_amount, 0) def test_loan_security_unpledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount), + ) repayment_entry.submit() request_loan_closure(loan.name) @@ -397,98 +506,108 @@ class TestLoan(unittest.TestCase): unpledge_request = unpledge_security(loan=loan.name, save=1) unpledge_request.submit() - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() loan.load_from_db() pledged_qty = get_pledged_security_qty(loan.name) - self.assertEqual(loan.status, 'Closed') + self.assertEqual(loan.status, "Closed") self.assertEqual(sum(pledged_qty.values()), 0) amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(amounts['pending_principal_amount'], 0) - self.assertEqual(amounts['payable_principal_amount'], 0.0) - self.assertEqual(amounts['interest_amount'], 0) + self.assertEqual(amounts["pending_principal_amount"], 0) + self.assertEqual(amounts["payable_principal_amount"], 0.0) + self.assertEqual(amounts["interest_amount"], 0) def test_partial_loan_security_unpledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 2000.00 - }, - { - "loan_security": "Test Security 2", - "qty": 4000.00 - }] + pledge = [ + {"loan_security": "Test Security 1", "qty": 2000.00}, + {"loan_security": "Test Security 2", "qty": 4000.00}, + ] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 5), 600000 + ) repayment_entry.submit() - unpledge_map = {'Test Security 2': 2000} + unpledge_map = {"Test Security 2": 2000} - unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_request = unpledge_security(loan=loan.name, security_map=unpledge_map, save=1) unpledge_request.submit() - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() unpledge_request.submit() unpledge_request.load_from_db() self.assertEqual(unpledge_request.docstatus, 1) def test_sanctioned_loan_security_unpledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - unpledge_map = {'Test Security 1': 4000} - unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1) + unpledge_map = {"Test Security 1": 4000} + unpledge_request = unpledge_security(loan=loan.name, security_map=unpledge_map, save=1) unpledge_request.submit() - unpledge_request.status = 'Approved' + unpledge_request.status = "Approved" unpledge_request.save() unpledge_request.submit() def test_disbursal_check_with_shortfall(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 8000.00, - "haircut": 50, - }] + pledges = [ + { + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) loan.submit() - #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + # Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge make_loan_disbursement_entry(loan.name, 700000) - frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 - where loan_security='Test Security 2'""") + frappe.db.sql( + """UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""" + ) create_process_loan_security_shortfall() loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) @@ -496,422 +615,505 @@ class TestLoan(unittest.TestCase): self.assertEqual(get_disbursal_amount(loan.name), 0) - frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 - where loan_security='Test Security 2'""") + frappe.db.sql( + """ UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""" + ) def test_disbursal_check_without_shortfall(self): - pledges = [{ - "loan_security": "Test Security 2", - "qty": 8000.00, - "haircut": 50, - }] + pledges = [ + { + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + } + ] - loan_application = create_loan_application('_Test Company', self.applicant2, - 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 + ) create_pledge(loan_application) - loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan = create_loan_with_security( + self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application + ) loan.submit() - #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + # Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge make_loan_disbursement_entry(loan.name, 700000) self.assertEqual(get_disbursal_amount(loan.name), 300000) def test_pending_loan_amount_after_closure_request(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount), + ) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amounts = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["paid_interest_amount", "paid_principal_amount"] + ) request_loan_closure(loan.name) loan.load_from_db() self.assertEqual(loan.status, "Loan Closure Requested") amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(amounts['pending_principal_amount'], 0.0) + self.assertEqual(amounts["pending_principal_amount"], 0.0) def test_partial_unaccrued_interest_payment(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5.5 # get partial unaccrued interest amount - paid_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + paid_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - paid_amount) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 5), paid_amount + ) repayment_entry.submit() repayment_entry.load_from_db() - partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \ - / (days_in_year(get_datetime(first_date).year) * 100) + partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) - interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2) + interest_amount = flt(amounts["interest_amount"] + partial_accrued_interest_amount, 2) self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) def test_penalty(self): loan, amounts = create_loan_scenario_for_penalty(self) # 30 days - grace period penalty_days = 30 - 4 - penalty_applicable_amount = flt(amounts['interest_amount']/2) + penalty_applicable_amount = flt(amounts["interest_amount"] / 2) penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2) - process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30') + process = process_loan_interest_accrual_for_demand_loans(posting_date="2019-11-30") - calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', - {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') + calculated_penalty_amount = frappe.db.get_value( + "Loan Interest Accrual", + {"process_loan_interest_accrual": process, "loan": loan.name}, + "penalty_amount", + ) self.assertEqual(loan.loan_amount, 1000000) self.assertEqual(calculated_penalty_amount, penalty_amount) def test_penalty_repayment(self): loan, dummy = create_loan_scenario_for_penalty(self) - amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00') + amounts = calculate_amounts(loan.name, "2019-11-30 00:00:00") first_penalty = 10000 - second_penalty = amounts['penalty_amount'] - 10000 + second_penalty = amounts["penalty_amount"] - 10000 - repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:00', 10000) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, "2019-11-30 00:00:00", 10000 + ) repayment_entry.submit() - amounts = calculate_amounts(loan.name, '2019-11-30 00:00:01') - self.assertEqual(amounts['penalty_amount'], second_penalty) + amounts = calculate_amounts(loan.name, "2019-11-30 00:00:01") + self.assertEqual(amounts["penalty_amount"], second_penalty) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:01', second_penalty) + repayment_entry = create_repayment_entry( + loan.name, self.applicant2, "2019-11-30 00:00:01", second_penalty + ) repayment_entry.submit() - amounts = calculate_amounts(loan.name, '2019-11-30 00:00:02') - self.assertEqual(amounts['penalty_amount'], 0) + amounts = calculate_amounts(loan.name, "2019-11-30 00:00:02") + self.assertEqual(amounts["penalty_amount"], 0) def test_loan_write_off_limit(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) # repay 50 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount + accrued_interest_amount - 50)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount - 50), + ) repayment_entry.submit() - amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + amount = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"] + ) - self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0)) self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(flt(amounts['pending_principal_amount'], 0), 50) + self.assertEqual(flt(amounts["pending_principal_amount"], 0), 50) request_loan_closure(loan.name) loan.load_from_db() self.assertEqual(loan.status, "Loan Closure Requested") def test_loan_repayment_against_partially_disbursed_loan(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" - make_loan_disbursement_entry(loan.name, loan.loan_amount/2, disbursement_date=first_date) + make_loan_disbursement_entry(loan.name, loan.loan_amount / 2, disbursement_date=first_date) loan.load_from_db() self.assertEqual(loan.status, "Partially Disbursed") - create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount/3)) + create_repayment_entry( + loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount / 3) + ) def test_loan_amount_write_off(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant2, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 no_of_days += 5 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) # repay 100 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - flt(loan.loan_amount + accrued_interest_amount - 100)) + repayment_entry = create_repayment_entry( + loan.name, + self.applicant2, + add_days(last_date, 5), + flt(loan.loan_amount + accrued_interest_amount - 100), + ) repayment_entry.submit() - amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + amount = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name}, ["sum(paid_interest_amount)"] + ) - self.assertEqual(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEqual(flt(amount, 0), flt(accrued_interest_amount, 0)) self.assertEqual(flt(repayment_entry.penalty_amount, 5), 0) amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(flt(amounts['pending_principal_amount'], 0), 100) + self.assertEqual(flt(amounts["pending_principal_amount"], 0), 100) - we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount']) + we = make_loan_write_off(loan.name, amount=amounts["pending_principal_amount"]) we.submit() amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - self.assertEqual(flt(amounts['pending_principal_amount'], 0), 0) + self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0) + def create_loan_scenario_for_penalty(doc): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', doc.applicant2, 'Demand Loan', pledge) + loan_application = create_loan_application("_Test Company", doc.applicant2, "Demand Loan", pledge) create_pledge(loan_application) - loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + doc.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 1)) - paid_amount = amounts['interest_amount']/2 + paid_amount = amounts["interest_amount"] / 2 - repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5), - paid_amount) + repayment_entry = create_repayment_entry( + loan.name, doc.applicant2, add_days(last_date, 5), paid_amount + ) repayment_entry.submit() return loan, amounts + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): - frappe.get_doc({ - "doctype": "Account", - "account_name": "Loans and Advances (Assets)", - "company": "_Test Company", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Current Assets - _TC", - "account_type": "Bank", - "is_group": 1 - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "account_name": "Loans and Advances (Assets)", + "company": "_Test Company", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 1, + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Loan Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Loan Account", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Loans and Advances (Assets) - _TC", - "account_type": "Bank", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Loan Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Loans and Advances (Assets) - _TC", + "account_type": "Bank", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Payment Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Payment Account", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Bank Accounts - _TC", - "account_type": "Bank", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Payment Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Disbursement Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Disbursement Account", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Bank Accounts - _TC", - "account_type": "Bank", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Disbursement Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Interest Income Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "root_type": "Income", - "account_name": "Interest Income Account", - "report_type": "Profit and Loss", - "currency": "INR", - "parent_account": "Direct Income - _TC", - "account_type": "Income Account", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "root_type": "Income", + "account_name": "Interest Income Account", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Account", "Penalty Income Account - _TC"): - frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Penalty Income Account", - "root_type": "Income", - "report_type": "Profit and Loss", - "currency": "INR", - "parent_account": "Direct Income - _TC", - "account_type": "Income Account", - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "Penalty Income Account", + "root_type": "Income", + "report_type": "Profit and Loss", + "currency": "INR", + "parent_account": "Direct Income - _TC", + "account_type": "Income Account", + } + ).insert(ignore_permissions=True) -def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_interest_rate=None, is_term_loan=None, grace_period_in_days=None, - mode_of_payment=None, disbursement_account=None, payment_account=None, loan_account=None, interest_income_account=None, penalty_income_account=None, - repayment_method=None, repayment_periods=None): + +def create_loan_type( + loan_name, + maximum_loan_amount, + rate_of_interest, + penalty_interest_rate=None, + is_term_loan=None, + grace_period_in_days=None, + mode_of_payment=None, + disbursement_account=None, + payment_account=None, + loan_account=None, + interest_income_account=None, + penalty_income_account=None, + repayment_method=None, + repayment_periods=None, +): if not frappe.db.exists("Loan Type", loan_name): - loan_type = frappe.get_doc({ - "doctype": "Loan Type", - "company": "_Test Company", - "loan_name": loan_name, - "is_term_loan": is_term_loan, - "maximum_loan_amount": maximum_loan_amount, - "rate_of_interest": rate_of_interest, - "penalty_interest_rate": penalty_interest_rate, - "grace_period_in_days": grace_period_in_days, - "mode_of_payment": mode_of_payment, - "disbursement_account": disbursement_account, - "payment_account": payment_account, - "loan_account": loan_account, - "interest_income_account": interest_income_account, - "penalty_income_account": penalty_income_account, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "write_off_amount": 100 - }).insert() + loan_type = frappe.get_doc( + { + "doctype": "Loan Type", + "company": "_Test Company", + "loan_name": loan_name, + "is_term_loan": is_term_loan, + "maximum_loan_amount": maximum_loan_amount, + "rate_of_interest": rate_of_interest, + "penalty_interest_rate": penalty_interest_rate, + "grace_period_in_days": grace_period_in_days, + "mode_of_payment": mode_of_payment, + "disbursement_account": disbursement_account, + "payment_account": payment_account, + "loan_account": loan_account, + "interest_income_account": interest_income_account, + "penalty_income_account": penalty_income_account, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "write_off_amount": 100, + } + ).insert() loan_type.submit() + def create_loan_security_type(): if not frappe.db.exists("Loan Security Type", "Stock"): - frappe.get_doc({ - "doctype": "Loan Security Type", - "loan_security_type": "Stock", - "unit_of_measure": "Nos", - "haircut": 50.00, - "loan_to_value_ratio": 50 - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Loan Security Type", + "loan_security_type": "Stock", + "unit_of_measure": "Nos", + "haircut": 50.00, + "loan_to_value_ratio": 50, + } + ).insert(ignore_permissions=True) + def create_loan_security(): if not frappe.db.exists("Loan Security", "Test Security 1"): - frappe.get_doc({ - "doctype": "Loan Security", - "loan_security_type": "Stock", - "loan_security_code": "532779", - "loan_security_name": "Test Security 1", - "unit_of_measure": "Nos", - "haircut": 50.00, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "532779", + "loan_security_name": "Test Security 1", + "unit_of_measure": "Nos", + "haircut": 50.00, + } + ).insert(ignore_permissions=True) if not frappe.db.exists("Loan Security", "Test Security 2"): - frappe.get_doc({ - "doctype": "Loan Security", - "loan_security_type": "Stock", - "loan_security_code": "531335", - "loan_security_name": "Test Security 2", - "unit_of_measure": "Nos", - "haircut": 50.00, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Loan Security", + "loan_security_type": "Stock", + "loan_security_code": "531335", + "loan_security_name": "Test Security 2", + "unit_of_measure": "Nos", + "haircut": 50.00, + } + ).insert(ignore_permissions=True) + def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None): lsp = frappe.new_doc("Loan Security Pledge") - lsp.applicant_type = 'Customer' + lsp.applicant_type = "Customer" lsp.applicant = applicant lsp.company = "_Test Company" lsp.loan_application = loan_application @@ -920,64 +1122,82 @@ def create_loan_security_pledge(applicant, pledges, loan_application=None, loan= lsp.loan = loan for pledge in pledges: - lsp.append('securities', { - "loan_security": pledge['loan_security'], - "qty": pledge['qty'] - }) + lsp.append("securities", {"loan_security": pledge["loan_security"], "qty": pledge["qty"]}) lsp.save() lsp.submit() return lsp + def make_loan_disbursement_entry(loan, amount, disbursement_date=None): - loan_disbursement_entry = frappe.get_doc({ - "doctype": "Loan Disbursement", - "against_loan": loan, - "disbursement_date": disbursement_date, - "company": "_Test Company", - "disbursed_amount": amount, - "cost_center": 'Main - _TC' - }).insert(ignore_permissions=True) + loan_disbursement_entry = frappe.get_doc( + { + "doctype": "Loan Disbursement", + "against_loan": loan, + "disbursement_date": disbursement_date, + "company": "_Test Company", + "disbursed_amount": amount, + "cost_center": "Main - _TC", + } + ).insert(ignore_permissions=True) loan_disbursement_entry.save() loan_disbursement_entry.submit() return loan_disbursement_entry + def create_loan_security_price(loan_security, loan_security_price, uom, from_date, to_date): - if not frappe.db.get_value("Loan Security Price",{"loan_security": loan_security, - "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, 'name'): + if not frappe.db.get_value( + "Loan Security Price", + {"loan_security": loan_security, "valid_from": ("<=", from_date), "valid_upto": (">=", to_date)}, + "name", + ): + + lsp = frappe.get_doc( + { + "doctype": "Loan Security Price", + "loan_security": loan_security, + "loan_security_price": loan_security_price, + "uom": uom, + "valid_from": from_date, + "valid_upto": to_date, + } + ).insert(ignore_permissions=True) - lsp = frappe.get_doc({ - "doctype": "Loan Security Price", - "loan_security": loan_security, - "loan_security_price": loan_security_price, - "uom": uom, - "valid_from":from_date, - "valid_upto": to_date - }).insert(ignore_permissions=True) def create_repayment_entry(loan, applicant, posting_date, paid_amount): - lr = frappe.get_doc({ - "doctype": "Loan Repayment", - "against_loan": loan, - "company": "_Test Company", - "posting_date": posting_date or nowdate(), - "applicant": applicant, - "amount_paid": paid_amount, - "loan_type": "Stock Loan" - }).insert(ignore_permissions=True) + lr = frappe.get_doc( + { + "doctype": "Loan Repayment", + "against_loan": loan, + "company": "_Test Company", + "posting_date": posting_date or nowdate(), + "applicant": applicant, + "amount_paid": paid_amount, + "loan_type": "Stock Loan", + } + ).insert(ignore_permissions=True) return lr -def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, - repayment_periods=None, posting_date=None, do_not_save=False): - loan_application = frappe.new_doc('Loan Application') - loan_application.applicant_type = 'Customer' + +def create_loan_application( + company, + applicant, + loan_type, + proposed_pledges, + repayment_method=None, + repayment_periods=None, + posting_date=None, + do_not_save=False, +): + loan_application = frappe.new_doc("Loan Application") + loan_application.applicant_type = "Customer" loan_application.company = company loan_application.applicant = applicant loan_application.loan_type = loan_type @@ -989,7 +1209,7 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep loan_application.repayment_periods = repayment_periods for pledge in proposed_pledges: - loan_application.append('proposed_pledges', pledge) + loan_application.append("proposed_pledges", pledge) if do_not_save: return loan_application @@ -997,75 +1217,99 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep loan_application.save() loan_application.submit() - loan_application.status = 'Approved' + loan_application.status = "Approved" loan_application.save() return loan_application.name -def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods, - applicant_type=None, repayment_start_date=None, posting_date=None): +def create_loan( + applicant, + loan_type, + loan_amount, + repayment_method, + repayment_periods, + applicant_type=None, + repayment_start_date=None, + posting_date=None, +): - loan = frappe.get_doc({ - "doctype": "Loan", - "applicant_type": applicant_type or "Employee", - "company": "_Test Company", - "applicant": applicant, - "loan_type": loan_type, - "loan_amount": loan_amount, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "repayment_start_date": repayment_start_date or nowdate(), - "is_term_loan": 1, - "posting_date": posting_date or nowdate() - }) + loan = frappe.get_doc( + { + "doctype": "Loan", + "applicant_type": applicant_type or "Employee", + "company": "_Test Company", + "applicant": applicant, + "loan_type": loan_type, + "loan_amount": loan_amount, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": repayment_start_date or nowdate(), + "is_term_loan": 1, + "posting_date": posting_date or nowdate(), + } + ) loan.save() return loan -def create_loan_with_security(applicant, loan_type, repayment_method, repayment_periods, loan_application, posting_date=None, repayment_start_date=None): - loan = frappe.get_doc({ - "doctype": "Loan", - "company": "_Test Company", - "applicant_type": "Customer", - "posting_date": posting_date or nowdate(), - "loan_application": loan_application, - "applicant": applicant, - "loan_type": loan_type, - "is_term_loan": 1, - "is_secured_loan": 1, - "repayment_method": repayment_method, - "repayment_periods": repayment_periods, - "repayment_start_date": repayment_start_date or nowdate(), - "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - "payment_account": 'Payment Account - _TC', - "loan_account": 'Loan Account - _TC', - "interest_income_account": 'Interest Income Account - _TC', - "penalty_income_account": 'Penalty Income Account - _TC', - }) + +def create_loan_with_security( + applicant, + loan_type, + repayment_method, + repayment_periods, + loan_application, + posting_date=None, + repayment_start_date=None, +): + loan = frappe.get_doc( + { + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "loan_application": loan_application, + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 1, + "is_secured_loan": 1, + "repayment_method": repayment_method, + "repayment_periods": repayment_periods, + "repayment_start_date": repayment_start_date or nowdate(), + "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"), + "payment_account": "Payment Account - _TC", + "loan_account": "Loan Account - _TC", + "interest_income_account": "Interest Income Account - _TC", + "penalty_income_account": "Penalty Income Account - _TC", + } + ) loan.save() return loan + def create_demand_loan(applicant, loan_type, loan_application, posting_date=None): - loan = frappe.get_doc({ - "doctype": "Loan", - "company": "_Test Company", - "applicant_type": "Customer", - "posting_date": posting_date or nowdate(), - 'loan_application': loan_application, - "applicant": applicant, - "loan_type": loan_type, - "is_term_loan": 0, - "is_secured_loan": 1, - "mode_of_payment": frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name'), - "payment_account": 'Payment Account - _TC', - "loan_account": 'Loan Account - _TC', - "interest_income_account": 'Interest Income Account - _TC', - "penalty_income_account": 'Penalty Income Account - _TC', - }) + loan = frappe.get_doc( + { + "doctype": "Loan", + "company": "_Test Company", + "applicant_type": "Customer", + "posting_date": posting_date or nowdate(), + "loan_application": loan_application, + "applicant": applicant, + "loan_type": loan_type, + "is_term_loan": 0, + "is_secured_loan": 1, + "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"), + "payment_account": "Payment Account - _TC", + "loan_account": "Loan Account - _TC", + "interest_income_account": "Interest Income Account - _TC", + "penalty_income_account": "Penalty Income Account - _TC", + } + ) loan.save() diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index 2abd8ecbe48..41d8c2a9e28 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -30,8 +30,13 @@ class LoanApplication(Document): self.validate_loan_amount() if self.is_term_loan: - validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, - self.repayment_periods, self.is_term_loan) + validate_repayment_method( + self.repayment_method, + self.loan_amount, + self.repayment_amount, + self.repayment_periods, + self.is_term_loan, + ) self.validate_loan_type() @@ -47,21 +52,35 @@ class LoanApplication(Document): if not self.loan_amount: frappe.throw(_("Loan Amount is mandatory")) - maximum_loan_limit = frappe.db.get_value('Loan Type', self.loan_type, 'maximum_loan_amount') + maximum_loan_limit = frappe.db.get_value("Loan Type", self.loan_type, "maximum_loan_amount") if maximum_loan_limit and self.loan_amount > maximum_loan_limit: - frappe.throw(_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)) + frappe.throw( + _("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit) + ) if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: - frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) + frappe.throw( + _("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format( + self.maximum_loan_amount + ) + ) def check_sanctioned_amount_limit(self): - sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) + sanctioned_amount_limit = get_sanctioned_amount_limit( + self.applicant_type, self.applicant, self.company + ) if sanctioned_amount_limit: total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company) - if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): - frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) + if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt( + sanctioned_amount_limit + ): + frappe.throw( + _("Sanctioned Amount limit crossed for {0} {1}").format( + self.applicant_type, frappe.bold(self.applicant) + ) + ) def set_pledge_amount(self): for proposed_pledge in self.proposed_pledges: @@ -72,26 +91,31 @@ class LoanApplication(Document): proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security) if not proposed_pledge.qty: - proposed_pledge.qty = cint(proposed_pledge.amount/proposed_pledge.loan_security_price) + proposed_pledge.qty = cint(proposed_pledge.amount / proposed_pledge.loan_security_price) proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price - proposed_pledge.post_haircut_amount = cint(proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut/100)) + proposed_pledge.post_haircut_amount = cint( + proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut / 100) + ) def get_repayment_details(self): if self.is_term_loan: if self.repayment_method == "Repay Over Number of Periods": - self.repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods) + self.repayment_amount = get_monthly_repayment_amount( + self.loan_amount, self.rate_of_interest, self.repayment_periods + ) if self.repayment_method == "Repay Fixed Amount per Period": - monthly_interest_rate = flt(self.rate_of_interest) / (12 *100) + monthly_interest_rate = flt(self.rate_of_interest) / (12 * 100) if monthly_interest_rate: - min_repayment_amount = self.loan_amount*monthly_interest_rate + min_repayment_amount = self.loan_amount * monthly_interest_rate if self.repayment_amount - min_repayment_amount <= 0: - frappe.throw(_("Repayment Amount must be greater than " \ - + str(flt(min_repayment_amount, 2)))) - self.repayment_periods = math.ceil((math.log(self.repayment_amount) - - math.log(self.repayment_amount - min_repayment_amount)) /(math.log(1 + monthly_interest_rate))) + frappe.throw(_("Repayment Amount must be greater than " + str(flt(min_repayment_amount, 2)))) + self.repayment_periods = math.ceil( + (math.log(self.repayment_amount) - math.log(self.repayment_amount - min_repayment_amount)) + / (math.log(1 + monthly_interest_rate)) + ) else: self.repayment_periods = self.loan_amount / self.repayment_amount @@ -104,8 +128,8 @@ class LoanApplication(Document): self.total_payable_amount = 0 self.total_payable_interest = 0 - while(balance_amount > 0): - interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100)) + while balance_amount > 0: + interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12 * 100)) balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount) self.total_payable_interest += interest_amount @@ -124,12 +148,21 @@ class LoanApplication(Document): if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: self.loan_amount = self.maximum_loan_amount + @frappe.whitelist() def create_loan(source_name, target_doc=None, submit=0): def update_accounts(source_doc, target_doc, source_parent): - account_details = frappe.get_all("Loan Type", - fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"], - filters = {'name': source_doc.loan_type})[0] + account_details = frappe.get_all( + "Loan Type", + fields=[ + "mode_of_payment", + "payment_account", + "loan_account", + "interest_income_account", + "penalty_income_account", + ], + filters={"name": source_doc.loan_type}, + )[0] if source_doc.is_secured_loan: target_doc.maximum_loan_amount = 0 @@ -141,22 +174,25 @@ def create_loan(source_name, target_doc=None, submit=0): target_doc.penalty_income_account = account_details.penalty_income_account target_doc.loan_application = source_name - - doclist = get_mapped_doc("Loan Application", source_name, { - "Loan Application": { - "doctype": "Loan", - "validation": { - "docstatus": ["=", 1] - }, - "postprocess": update_accounts - } - }, target_doc) + doclist = get_mapped_doc( + "Loan Application", + source_name, + { + "Loan Application": { + "doctype": "Loan", + "validation": {"docstatus": ["=", 1]}, + "postprocess": update_accounts, + } + }, + target_doc, + ) if submit: doclist.submit() return doclist + @frappe.whitelist() def create_pledge(loan_application, loan=None): loan_application_doc = frappe.get_doc("Loan Application", loan_application) @@ -172,12 +208,15 @@ def create_pledge(loan_application, loan=None): for pledge in loan_application_doc.proposed_pledges: - lsp.append('securities', { - "loan_security": pledge.loan_security, - "qty": pledge.qty, - "loan_security_price": pledge.loan_security_price, - "haircut": pledge.haircut - }) + lsp.append( + "securities", + { + "loan_security": pledge.loan_security, + "qty": pledge.qty, + "loan_security_price": pledge.loan_security_price, + "haircut": pledge.haircut, + }, + ) lsp.save() lsp.submit() @@ -187,15 +226,14 @@ def create_pledge(loan_application, loan=None): return lsp.name -#This is a sandbox method to get the proposed pledges + +# This is a sandbox method to get the proposed pledges @frappe.whitelist() def get_proposed_pledge(securities): if isinstance(securities, string_types): securities = json.loads(securities) - proposed_pledges = { - 'securities': [] - } + proposed_pledges = {"securities": []} maximum_loan_amount = 0 for security in securities: @@ -206,15 +244,15 @@ def get_proposed_pledge(securities): security.loan_security_price = get_loan_security_price(security.loan_security) if not security.qty: - security.qty = cint(security.amount/security.loan_security_price) + security.qty = cint(security.amount / security.loan_security_price) security.amount = security.qty * security.loan_security_price - security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut/100)) + security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut / 100)) maximum_loan_amount += security.post_haircut_amount - proposed_pledges['securities'].append(security) + proposed_pledges["securities"].append(security) - proposed_pledges['maximum_loan_amount'] = maximum_loan_amount + proposed_pledges["maximum_loan_amount"] = maximum_loan_amount return proposed_pledges diff --git a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py index 01ef9f9d78a..1d90e9bb114 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'loan_application', - 'transactions': [ - { - 'items': ['Loan', 'Loan Security Pledge'] - }, - ], - } + return { + "fieldname": "loan_application", + "transactions": [ + {"items": ["Loan", "Loan Security Pledge"]}, + ], + } diff --git a/erpnext/loan_management/doctype/loan_application/test_loan_application.py b/erpnext/loan_management/doctype/loan_application/test_loan_application.py index 640709c095f..2a4bb882a8e 100644 --- a/erpnext/loan_management/doctype/loan_application/test_loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/test_loan_application.py @@ -15,27 +15,45 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( class TestLoanApplication(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Home Loan", 500000, 9.2, 0, 1, 0, 'Cash', 'Disbursement Account - _TC', 'Payment Account - _TC', 'Loan Account - _TC', - 'Interest Income Account - _TC', 'Penalty Income Account - _TC', 'Repay Over Number of Periods', 18) + create_loan_type( + "Home Loan", + 500000, + 9.2, + 0, + 1, + 0, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + "Repay Over Number of Periods", + 18, + ) self.applicant = make_employee("kate_loan@loan.com", "_Test Company") - make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant, currency='INR') + make_salary_structure( + "Test Salary Structure Loan", "Monthly", employee=self.applicant, currency="INR" + ) self.create_loan_application() def create_loan_application(self): loan_application = frappe.new_doc("Loan Application") - loan_application.update({ - "applicant": self.applicant, - "loan_type": "Home Loan", - "rate_of_interest": 9.2, - "loan_amount": 250000, - "repayment_method": "Repay Over Number of Periods", - "repayment_periods": 18, - "company": "_Test Company" - }) + loan_application.update( + { + "applicant": self.applicant, + "loan_type": "Home Loan", + "rate_of_interest": 9.2, + "loan_amount": 250000, + "repayment_method": "Repay Over Number of Periods", + "repayment_periods": 18, + "company": "_Test Company", + } + ) loan_application.insert() def test_loan_totals(self): - loan_application = frappe.get_doc("Loan Application", {"applicant":self.applicant}) + loan_application = frappe.get_doc("Loan Application", {"applicant": self.applicant}) self.assertEqual(loan_application.total_payable_interest, 18599) self.assertEqual(loan_application.total_payable_amount, 268599) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 54a03b92b5e..10174e531a1 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -18,7 +18,6 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ class LoanDisbursement(AccountsController): - def validate(self): self.set_missing_values() self.validate_disbursal_amount() @@ -30,7 +29,7 @@ class LoanDisbursement(AccountsController): def on_cancel(self): self.set_status_and_amounts(cancel=1) self.make_gl_entries(cancel=1) - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] def set_missing_values(self): if not self.disbursement_date: @@ -49,21 +48,36 @@ class LoanDisbursement(AccountsController): frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) def set_status_and_amounts(self, cancel=0): - loan_details = frappe.get_all("Loan", - fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable", - "status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0] + loan_details = frappe.get_all( + "Loan", + fields=[ + "loan_amount", + "disbursed_amount", + "total_payment", + "total_principal_paid", + "total_interest_payable", + "status", + "is_term_loan", + "is_secured_loan", + ], + filters={"name": self.against_loan}, + )[0] if cancel: disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details) else: disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details) - frappe.db.set_value("Loan", self.against_loan, { - "disbursement_date": self.disbursement_date, - "disbursed_amount": disbursed_amount, - "status": status, - "total_payment": total_payment - }) + frappe.db.set_value( + "Loan", + self.against_loan, + { + "disbursement_date": self.disbursement_date, + "disbursed_amount": disbursed_amount, + "status": status, + "total_payment": total_payment, + }, + ) def get_values_on_cancel(self, loan_details): disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount @@ -91,8 +105,11 @@ class LoanDisbursement(AccountsController): total_payment = loan_details.total_payment if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan: - process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), - loan=self.against_loan, accrual_type="Disbursement") + process_loan_interest_accrual_for_demand_loans( + posting_date=add_days(self.disbursement_date, -1), + loan=self.against_loan, + accrual_type="Disbursement", + ) if disbursed_amount > loan_details.loan_amount: topup_amount = disbursed_amount - loan_details.loan_amount @@ -116,72 +133,96 @@ class LoanDisbursement(AccountsController): gle_map = [] gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "against": self.disbursement_account, - "debit": self.disbursed_amount, - "debit_in_account_currency": self.disbursed_amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": self.disbursement_date - }) + self.get_gl_dict( + { + "account": self.loan_account, + "against": self.disbursement_account, + "debit": self.disbursed_amount, + "debit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": self.disbursement_date, + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": self.disbursement_account, - "against": self.loan_account, - "credit": self.disbursed_amount, - "credit_in_account_currency": self.disbursed_amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": self.disbursement_date - }) + self.get_gl_dict( + { + "account": self.disbursement_account, + "against": self.loan_account, + "credit": self.disbursed_amount, + "credit_in_account_currency": self.disbursed_amount, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": self.disbursement_date, + } + ) ) if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + def get_total_pledged_security_value(loan): update_time = get_datetime() - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", - fields=["loan_security", "loan_security_price"], - filters = { - "valid_from": ("<=", update_time), - "valid_upto": (">=", update_time) - }, as_list=1)) + loan_security_price_map = frappe._dict( + frappe.get_all( + "Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)}, + as_list=1, + ) + ) - hair_cut_map = frappe._dict(frappe.get_all('Loan Security', - fields=["name", "haircut"], as_list=1)) + hair_cut_map = frappe._dict( + frappe.get_all("Loan Security", fields=["name", "haircut"], as_list=1) + ) security_value = 0.0 pledged_securities = get_pledged_security_qty(loan) for security, qty in pledged_securities.items(): after_haircut_percentage = 100 - hair_cut_map.get(security) - security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100 + security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage) / 100 return security_value + @frappe.whitelist() def get_disbursal_amount(loan, on_current_security_price=0): from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( get_pending_principal_amount, ) - loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment", - "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan", - "maximum_loan_amount", "written_off_amount"], as_dict=1) + loan_details = frappe.get_value( + "Loan", + loan, + [ + "loan_amount", + "disbursed_amount", + "total_payment", + "total_principal_paid", + "total_interest_payable", + "status", + "is_term_loan", + "is_secured_loan", + "maximum_loan_amount", + "written_off_amount", + ], + as_dict=1, + ) - if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, - 'status': 'Pending'}): + if loan_details.is_secured_loan and frappe.get_all( + "Loan Security Shortfall", filters={"loan": loan, "status": "Pending"} + ): return 0 pending_principal_amount = get_pending_principal_amount(loan_details) @@ -198,10 +239,14 @@ def get_disbursal_amount(loan, on_current_security_price=0): disbursal_amount = flt(security_value) - flt(pending_principal_amount) - if loan_details.is_term_loan and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount: + if ( + loan_details.is_term_loan + and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount + ): disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount return disbursal_amount + def get_maximum_amount_as_per_pledged_security(loan): - return flt(frappe.db.get_value('Loan Security Pledge', {'loan': loan}, 'sum(maximum_loan_value)')) + return flt(frappe.db.get_value("Loan Security Pledge", {"loan": loan}, "sum(maximum_loan_value)")) diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 10be750b449..4daa2edb28a 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -40,34 +40,50 @@ from erpnext.selling.doctype.customer.test_customer import get_customer_dict class TestLoanDisbursement(unittest.TestCase): - def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Demand Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) create_loan_security_type() create_loan_security() - create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) - create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price( + "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) + create_loan_security_price( + "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) - self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") def test_loan_topup(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) + ) loan.submit() @@ -76,18 +92,22 @@ class TestLoanDisbursement(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime().year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime().year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1)) # Should not be able to create loan disbursement entry before repayment - self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name, - 500000, first_date) + self.assertRaises( + frappe.ValidationError, make_loan_disbursement_entry, loan.name, 500000, first_date + ) - repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89) + repayment_entry = create_repayment_entry( + loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89 + ) repayment_entry.submit() loan.reload() @@ -96,49 +116,48 @@ class TestLoanDisbursement(unittest.TestCase): make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) # check for disbursement accrual - loan_interest_accrual = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name, - 'accrual_type': 'Disbursement'}) + loan_interest_accrual = frappe.db.get_value( + "Loan Interest Accrual", {"loan": loan.name, "accrual_type": "Disbursement"} + ) self.assertTrue(loan_interest_accrual) def test_loan_topup_with_additional_pledge(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, posting_date='2019-10-01') + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date="2019-10-01" + ) loan.submit() self.assertEqual(loan.loan_amount, 1000000) - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" # Disbursed 10,00,000 amount make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + process_loan_interest_accrual_for_demand_loans(posting_date=last_date) amounts = calculate_amounts(loan.name, add_days(last_date, 1)) - previous_interest = amounts['interest_amount'] + previous_interest = amounts["interest_amount"] - pledge1 = [{ - "loan_security": "Test Security 1", - "qty": 2000.00 - }] + pledge1 = [{"loan_security": "Test Security 1", "qty": 2000.00}] create_loan_security_pledge(self.applicant, pledge1, loan=loan.name) # Topup 500000 make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1)) - process_loan_interest_accrual_for_demand_loans(posting_date = add_days(last_date, 15)) + process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 15)) amounts = calculate_amounts(loan.name, add_days(last_date, 15)) - per_day_interest = get_per_day_interest(1500000, 13.5, '2019-10-30') + per_day_interest = get_per_day_interest(1500000, 13.5, "2019-10-30") interest = per_day_interest * 15 - self.assertEqual(amounts['pending_principal_amount'], 1500000) - self.assertEqual(amounts['interest_amount'], flt(interest + previous_interest, 2)) + self.assertEqual(amounts["pending_principal_amount"], 1500000) + self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2)) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 1c800a06da0..0c4b051fba2 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -33,45 +33,51 @@ class LoanInterestAccrual(AccountsController): self.update_is_accrued() self.make_gl_entries(cancel=1) - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] def update_is_accrued(self): - frappe.db.set_value('Repayment Schedule', self.repayment_schedule_name, 'is_accrued', 0) + frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0) def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] if self.interest_amount: gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": self.interest_income_account, - "debit": self.interest_amount, - "debit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) + self.get_gl_dict( + { + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.interest_income_account, + "debit": self.interest_amount, + "debit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( + self.last_accrual_date, self.posting_date, self.loan + ), + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date, + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": self.interest_income_account, - "against": self.loan_account, - "credit": self.interest_amount, - "credit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( - self.last_accrual_date, self.posting_date, self.loan), - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) + self.get_gl_dict( + { + "account": self.interest_income_account, + "against": self.loan_account, + "credit": self.interest_amount, + "credit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( + self.last_accrual_date, self.posting_date, self.loan + ), + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date, + } + ) ) if gle_map: @@ -81,7 +87,9 @@ class LoanInterestAccrual(AccountsController): # For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and # rate of interest is 13.5 then first loan interest accural will be on '01-10-2019' # which means interest will be accrued for 30 days which should be equal to 11095.89 -def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type): +def calculate_accrual_amount_for_demand_loans( + loan, posting_date, process_loan_interest, accrual_type +): from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( calculate_amounts, get_pending_principal_amount, @@ -95,51 +103,76 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i pending_principal_amount = get_pending_principal_amount(loan) - interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) + interest_per_day = get_per_day_interest( + pending_principal_amount, loan.rate_of_interest, posting_date + ) payable_interest = interest_per_day * no_of_days - pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure') + pending_amounts = calculate_amounts(loan.name, posting_date, payment_type="Loan Closure") - args = frappe._dict({ - 'loan': loan.name, - 'applicant_type': loan.applicant_type, - 'applicant': loan.applicant, - 'interest_income_account': loan.interest_income_account, - 'loan_account': loan.loan_account, - 'pending_principal_amount': pending_principal_amount, - 'interest_amount': payable_interest, - 'total_pending_interest_amount': pending_amounts['interest_amount'], - 'penalty_amount': pending_amounts['penalty_amount'], - 'process_loan_interest': process_loan_interest, - 'posting_date': posting_date, - 'accrual_type': accrual_type - }) + args = frappe._dict( + { + "loan": loan.name, + "applicant_type": loan.applicant_type, + "applicant": loan.applicant, + "interest_income_account": loan.interest_income_account, + "loan_account": loan.loan_account, + "pending_principal_amount": pending_principal_amount, + "interest_amount": payable_interest, + "total_pending_interest_amount": pending_amounts["interest_amount"], + "penalty_amount": pending_amounts["penalty_amount"], + "process_loan_interest": process_loan_interest, + "posting_date": posting_date, + "accrual_type": accrual_type, + } + ) if flt(payable_interest, precision) > 0.0: make_loan_interest_accrual_entry(args) -def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"): - query_filters = { - "status": ('in', ['Disbursed', 'Partially Disbursed']), - "docstatus": 1 - } + +def make_accrual_interest_entry_for_demand_loans( + posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular" +): + query_filters = {"status": ("in", ["Disbursed", "Partially Disbursed"]), "docstatus": 1} if loan_type: - query_filters.update({ - "loan_type": loan_type - }) + query_filters.update({"loan_type": loan_type}) if not open_loans: - open_loans = frappe.get_all("Loan", - fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "loan_amount", - "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", - "rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"], - filters=query_filters) + open_loans = frappe.get_all( + "Loan", + fields=[ + "name", + "total_payment", + "total_amount_paid", + "loan_account", + "interest_income_account", + "loan_amount", + "is_term_loan", + "status", + "disbursement_date", + "disbursed_amount", + "applicant_type", + "applicant", + "rate_of_interest", + "total_interest_payable", + "written_off_amount", + "total_principal_paid", + "repayment_start_date", + ], + filters=query_filters, + ) for loan in open_loans: - calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type) + calculate_accrual_amount_for_demand_loans( + loan, posting_date, process_loan_interest, accrual_type + ) -def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"): + +def make_accrual_interest_entry_for_term_loans( + posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular" +): curr_date = posting_date or add_days(nowdate(), 1) term_loans = get_term_loans(curr_date, term_loan, loan_type) @@ -148,37 +181,44 @@ def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_intere for loan in term_loans: accrued_entries.append(loan.payment_entry) - args = frappe._dict({ - 'loan': loan.name, - 'applicant_type': loan.applicant_type, - 'applicant': loan.applicant, - 'interest_income_account': loan.interest_income_account, - 'loan_account': loan.loan_account, - 'interest_amount': loan.interest_amount, - 'payable_principal': loan.principal_amount, - 'process_loan_interest': process_loan_interest, - 'repayment_schedule_name': loan.payment_entry, - 'posting_date': posting_date, - 'accrual_type': accrual_type - }) + args = frappe._dict( + { + "loan": loan.name, + "applicant_type": loan.applicant_type, + "applicant": loan.applicant, + "interest_income_account": loan.interest_income_account, + "loan_account": loan.loan_account, + "interest_amount": loan.interest_amount, + "payable_principal": loan.principal_amount, + "process_loan_interest": process_loan_interest, + "repayment_schedule_name": loan.payment_entry, + "posting_date": posting_date, + "accrual_type": accrual_type, + } + ) make_loan_interest_accrual_entry(args) if accrued_entries: - frappe.db.sql("""UPDATE `tabRepayment Schedule` - SET is_accrued = 1 where name in (%s)""" #nosec - % ", ".join(['%s']*len(accrued_entries)), tuple(accrued_entries)) + frappe.db.sql( + """UPDATE `tabRepayment Schedule` + SET is_accrued = 1 where name in (%s)""" # nosec + % ", ".join(["%s"] * len(accrued_entries)), + tuple(accrued_entries), + ) + def get_term_loans(date, term_loan=None, loan_type=None): - condition = '' + condition = "" if term_loan: - condition +=' AND l.name = %s' % frappe.db.escape(term_loan) + condition += " AND l.name = %s" % frappe.db.escape(term_loan) if loan_type: - condition += ' AND l.loan_type = %s' % frappe.db.escape(loan_type) + condition += " AND l.loan_type = %s" % frappe.db.escape(loan_type) - term_loans = frappe.db.sql("""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, + term_loans = frappe.db.sql( + """SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account, l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant, l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry, rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount @@ -189,10 +229,16 @@ def get_term_loans(date, term_loan=None, loan_type=None): AND rs.payment_date <= %s AND rs.is_accrued=0 {0} AND l.status = 'Disbursed' - ORDER BY rs.payment_date""".format(condition), (getdate(date)), as_dict=1) + ORDER BY rs.payment_date""".format( + condition + ), + (getdate(date)), + as_dict=1, + ) return term_loans + def make_loan_interest_accrual_entry(args): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -204,7 +250,9 @@ def make_loan_interest_accrual_entry(args): loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision) - loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision) + loan_interest_accrual.total_pending_interest_amount = flt( + args.total_pending_interest_amount, precision + ) loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision) loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest @@ -223,15 +271,20 @@ def get_no_of_days_for_interest_accural(loan, posting_date): return no_of_days + def get_last_accrual_date(loan): - last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` - WHERE loan = %s and docstatus = 1""", (loan)) + last_posting_date = frappe.db.sql( + """ SELECT MAX(posting_date) from `tabLoan Interest Accrual` + WHERE loan = %s and docstatus = 1""", + (loan), + ) if last_posting_date[0][0]: # interest for last interest accrual date is already booked, so add 1 day return add_days(last_posting_date[0][0], 1) else: - return frappe.db.get_value('Loan', loan, 'disbursement_date') + return frappe.db.get_value("Loan", loan, "disbursement_date") + def days_in_year(year): days = 365 @@ -241,8 +294,11 @@ def days_in_year(year): return days + def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): if not posting_date: posting_date = getdate() - return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)) + return flt( + (principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) + ) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index e8c77506fcb..fd59393b827 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -30,78 +30,98 @@ class TestLoanInterestAccrual(unittest.TestCase): def setUp(self): create_loan_accounts() - create_loan_type("Demand Loan", 2000000, 13.5, 25, 0, 5, 'Cash', 'Disbursement Account - _TC', - 'Payment Account - _TC', 'Loan Account - _TC', 'Interest Income Account - _TC', 'Penalty Income Account - _TC') + create_loan_type( + "Demand Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "Disbursement Account - _TC", + "Payment Account - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) create_loan_security_type() create_loan_security() - create_loan_security_price("Test Security 1", 500, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) + create_loan_security_price( + "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) - self.applicant = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') + self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") def test_loan_interest_accural(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, - posting_date=get_first_day(nowdate())) + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) self.assertEqual(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0)) def test_accumulated_amounts(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] + pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] - loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge) + loan_application = create_loan_application( + "_Test Company", self.applicant, "Demand Loan", pledge + ) create_pledge(loan_application) - loan = create_demand_loan(self.applicant, "Demand Loan", loan_application, - posting_date=get_first_day(nowdate())) + loan = create_demand_loan( + self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) + ) loan.submit() - first_date = '2019-10-01' - last_date = '2019-10-30' + first_date = "2019-10-01" + last_date = "2019-10-30" no_of_days = date_diff(last_date, first_date) + 1 - accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date=last_date) - loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) + loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) self.assertEqual(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0)) - next_start_date = '2019-10-31' - next_end_date = '2019-11-29' + next_start_date = "2019-10-31" + next_end_date = "2019-11-29" no_of_days = date_diff(next_end_date, next_start_date) + 1 process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date) - new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ - / (days_in_year(get_datetime(first_date).year) * 100) + new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) / ( + days_in_year(get_datetime(first_date).year) * 100 + ) total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0) - loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name, - 'process_loan_interest_accrual': process}) - self.assertEqual(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount) + loan_interest_accrual = frappe.get_doc( + "Loan Interest Accrual", {"loan": loan.name, "process_loan_interest_accrual": process} + ) + self.assertEqual( + flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount + ) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 0610da14d4c..ce50dd3b38d 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -23,7 +23,6 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ class LoanRepayment(AccountsController): - def validate(self): amounts = calculate_amounts(self.against_loan, self.posting_date) self.set_missing_values(amounts) @@ -43,7 +42,7 @@ class LoanRepayment(AccountsController): self.check_future_accruals() self.update_repayment_schedule(cancel=1) self.mark_as_unpaid() - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] self.make_gl_entries(cancel=1) def set_missing_values(self, amounts): @@ -56,32 +55,38 @@ class LoanRepayment(AccountsController): self.cost_center = erpnext.get_default_cost_center(self.company) if not self.interest_payable: - self.interest_payable = flt(amounts['interest_amount'], precision) + self.interest_payable = flt(amounts["interest_amount"], precision) if not self.penalty_amount: - self.penalty_amount = flt(amounts['penalty_amount'], precision) + self.penalty_amount = flt(amounts["penalty_amount"], precision) if not self.pending_principal_amount: - self.pending_principal_amount = flt(amounts['pending_principal_amount'], precision) + self.pending_principal_amount = flt(amounts["pending_principal_amount"], precision) if not self.payable_principal_amount and self.is_term_loan: - self.payable_principal_amount = flt(amounts['payable_principal_amount'], precision) + self.payable_principal_amount = flt(amounts["payable_principal_amount"], precision) if not self.payable_amount: - self.payable_amount = flt(amounts['payable_amount'], precision) + self.payable_amount = flt(amounts["payable_amount"], precision) - shortfall_amount = flt(frappe.db.get_value('Loan Security Shortfall', {'loan': self.against_loan, 'status': 'Pending'}, - 'shortfall_amount')) + shortfall_amount = flt( + frappe.db.get_value( + "Loan Security Shortfall", {"loan": self.against_loan, "status": "Pending"}, "shortfall_amount" + ) + ) if shortfall_amount: self.shortfall_amount = shortfall_amount - if amounts.get('due_date'): - self.due_date = amounts.get('due_date') + if amounts.get("due_date"): + self.due_date = amounts.get("due_date") def check_future_entries(self): - future_repayment_date = frappe.db.get_value("Loan Repayment", {"posting_date": (">", self.posting_date), - "docstatus": 1, "against_loan": self.against_loan}, 'posting_date') + future_repayment_date = frappe.db.get_value( + "Loan Repayment", + {"posting_date": (">", self.posting_date), "docstatus": 1, "against_loan": self.against_loan}, + "posting_date", + ) if future_repayment_date: frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date))) @@ -100,106 +105,167 @@ class LoanRepayment(AccountsController): last_accrual_date = get_last_accrual_date(self.against_loan) # get posting date upto which interest has to be accrued - per_day_interest = get_per_day_interest(self.pending_principal_amount, - self.rate_of_interest, self.posting_date) + per_day_interest = get_per_day_interest( + self.pending_principal_amount, self.rate_of_interest, self.posting_date + ) - no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, - precision)/per_day_interest, 0) - 1 + no_of_days = ( + flt(flt(self.total_interest_paid - self.interest_payable, precision) / per_day_interest, 0) + - 1 + ) posting_date = add_days(last_accrual_date, no_of_days) # book excess interest paid - process = process_loan_interest_accrual_for_demand_loans(posting_date=posting_date, - loan=self.against_loan, accrual_type="Repayment") + process = process_loan_interest_accrual_for_demand_loans( + posting_date=posting_date, loan=self.against_loan, accrual_type="Repayment" + ) # get loan interest accrual to update paid amount - lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': - process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + lia = frappe.db.get_value( + "Loan Interest Accrual", + {"process_loan_interest_accrual": process}, + ["name", "interest_amount", "payable_principal_amount"], + as_dict=1, + ) if lia: - self.append('repayment_details', { - 'loan_interest_accrual': lia.name, - 'paid_interest_amount': flt(self.total_interest_paid - self.interest_payable, precision), - 'paid_principal_amount': 0.0, - 'accrual_type': 'Repayment' - }) + self.append( + "repayment_details", + { + "loan_interest_accrual": lia.name, + "paid_interest_amount": flt(self.total_interest_paid - self.interest_payable, precision), + "paid_principal_amount": 0.0, + "accrual_type": "Repayment", + }, + ) def update_paid_amount(self): - loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', - 'written_off_amount'], as_dict=1) + loan = frappe.get_value( + "Loan", + self.against_loan, + [ + "total_amount_paid", + "total_principal_paid", + "status", + "is_secured_loan", + "total_payment", + "loan_amount", + "disbursed_amount", + "total_interest_payable", + "written_off_amount", + ], + as_dict=1, + ) - loan.update({ - 'total_amount_paid': loan.total_amount_paid + self.amount_paid, - 'total_principal_paid': loan.total_principal_paid + self.principal_amount_paid - }) + loan.update( + { + "total_amount_paid": loan.total_amount_paid + self.amount_paid, + "total_principal_paid": loan.total_principal_paid + self.principal_amount_paid, + } + ) pending_principal_amount = get_pending_principal_amount(loan) if not loan.is_secured_loan and pending_principal_amount <= 0: - loan.update({'status': 'Loan Closure Requested'}) + loan.update({"status": "Loan Closure Requested"}) for payment in self.repayment_details: - frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + frappe.db.sql( + """ UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` + %s, paid_interest_amount = `paid_interest_amount` + %s WHERE name = %s""", - (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual)) + ( + flt(payment.paid_principal_amount), + flt(payment.paid_interest_amount), + payment.loan_interest_accrual, + ), + ) - frappe.db.sql(""" UPDATE `tabLoan` + frappe.db.sql( + """ UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s, status = %s - WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status, - self.against_loan)) + WHERE name = %s """, + (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan), + ) update_shortfall_status(self.against_loan, self.principal_amount_paid) def mark_as_unpaid(self): - loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid', - 'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'disbursed_amount', 'total_interest_payable', - 'written_off_amount'], as_dict=1) + loan = frappe.get_value( + "Loan", + self.against_loan, + [ + "total_amount_paid", + "total_principal_paid", + "status", + "is_secured_loan", + "total_payment", + "loan_amount", + "disbursed_amount", + "total_interest_payable", + "written_off_amount", + ], + as_dict=1, + ) no_of_repayments = len(self.repayment_details) - loan.update({ - 'total_amount_paid': loan.total_amount_paid - self.amount_paid, - 'total_principal_paid': loan.total_principal_paid - self.principal_amount_paid - }) + loan.update( + { + "total_amount_paid": loan.total_amount_paid - self.amount_paid, + "total_principal_paid": loan.total_principal_paid - self.principal_amount_paid, + } + ) - if loan.status == 'Loan Closure Requested': + if loan.status == "Loan Closure Requested": if loan.disbursed_amount >= loan.loan_amount: - loan['status'] = 'Disbursed' + loan["status"] = "Disbursed" else: - loan['status'] = 'Partially Disbursed' + loan["status"] = "Partially Disbursed" for payment in self.repayment_details: - frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + frappe.db.sql( + """ UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` - %s, paid_interest_amount = `paid_interest_amount` - %s WHERE name = %s""", - (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual)) + (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual), + ) # Cancel repayment interest accrual # checking idx as a preventive measure, repayment accrual will always be the last entry - if payment.accrual_type == 'Repayment' and payment.idx == no_of_repayments: - lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual) + if payment.accrual_type == "Repayment" and payment.idx == no_of_repayments: + lia_doc = frappe.get_doc("Loan Interest Accrual", payment.loan_interest_accrual) lia_doc.cancel() - frappe.db.sql(""" UPDATE `tabLoan` + frappe.db.sql( + """ UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s, status = %s - WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan)) + WHERE name = %s """, + (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan), + ) def check_future_accruals(self): - future_accrual_date = frappe.db.get_value("Loan Interest Accrual", {"posting_date": (">", self.posting_date), - "docstatus": 1, "loan": self.against_loan}, 'posting_date') + future_accrual_date = frappe.db.get_value( + "Loan Interest Accrual", + {"posting_date": (">", self.posting_date), "docstatus": 1, "loan": self.against_loan}, + "posting_date", + ) if future_accrual_date: - frappe.throw("Cannot cancel. Interest accruals already processed till {0}".format(get_datetime(future_accrual_date))) + frappe.throw( + "Cannot cancel. Interest accruals already processed till {0}".format( + get_datetime(future_accrual_date) + ) + ) def update_repayment_schedule(self, cancel=0): if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount: regenerate_repayment_schedule(self.against_loan, cancel) def allocate_amounts(self, repayment_details): - self.set('repayment_details', []) + self.set("repayment_details", []) self.principal_amount_paid = 0 self.total_penalty_paid = 0 interest_paid = self.amount_paid @@ -232,15 +298,15 @@ class LoanRepayment(AccountsController): idx = 1 if interest_paid > 0: - for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])): + for lia, amounts in iteritems(repayment_details.get("pending_accrual_entries", [])): interest_amount = 0 - if amounts['interest_amount'] <= interest_paid: - interest_amount = amounts['interest_amount'] + if amounts["interest_amount"] <= interest_paid: + interest_amount = amounts["interest_amount"] self.total_interest_paid += interest_amount interest_paid -= interest_amount elif interest_paid: - if interest_paid >= amounts['interest_amount']: - interest_amount = amounts['interest_amount'] + if interest_paid >= amounts["interest_amount"]: + interest_amount = amounts["interest_amount"] self.total_interest_paid += interest_amount interest_paid = 0 else: @@ -249,27 +315,32 @@ class LoanRepayment(AccountsController): interest_paid = 0 if interest_amount: - self.append('repayment_details', { - 'loan_interest_accrual': lia, - 'paid_interest_amount': interest_amount, - 'paid_principal_amount': 0 - }) + self.append( + "repayment_details", + { + "loan_interest_accrual": lia, + "paid_interest_amount": interest_amount, + "paid_principal_amount": 0, + }, + ) updated_entries[lia] = idx idx += 1 return interest_paid, updated_entries - def allocate_principal_amount_for_term_loans(self, interest_paid, repayment_details, updated_entries): + def allocate_principal_amount_for_term_loans( + self, interest_paid, repayment_details, updated_entries + ): if interest_paid > 0: - for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])): + for lia, amounts in iteritems(repayment_details.get("pending_accrual_entries", [])): paid_principal = 0 - if amounts['payable_principal_amount'] <= interest_paid: - paid_principal = amounts['payable_principal_amount'] + if amounts["payable_principal_amount"] <= interest_paid: + paid_principal = amounts["payable_principal_amount"] self.principal_amount_paid += paid_principal interest_paid -= paid_principal elif interest_paid: - if interest_paid >= amounts['payable_principal_amount']: - paid_principal = amounts['payable_principal_amount'] + if interest_paid >= amounts["payable_principal_amount"]: + paid_principal = amounts["payable_principal_amount"] self.principal_amount_paid += paid_principal interest_paid = 0 else: @@ -279,30 +350,34 @@ class LoanRepayment(AccountsController): if updated_entries.get(lia): idx = updated_entries.get(lia) - self.get('repayment_details')[idx-1].paid_principal_amount += paid_principal + self.get("repayment_details")[idx - 1].paid_principal_amount += paid_principal else: - self.append('repayment_details', { - 'loan_interest_accrual': lia, - 'paid_interest_amount': 0, - 'paid_principal_amount': paid_principal - }) + self.append( + "repayment_details", + { + "loan_interest_accrual": lia, + "paid_interest_amount": 0, + "paid_principal_amount": paid_principal, + }, + ) if interest_paid > 0: self.principal_amount_paid += interest_paid def allocate_excess_payment_for_demand_loans(self, interest_paid, repayment_details): - if repayment_details['unaccrued_interest'] and interest_paid > 0: + if repayment_details["unaccrued_interest"] and interest_paid > 0: # no of days for which to accrue interest # Interest can only be accrued for an entire day and not partial - if interest_paid > repayment_details['unaccrued_interest']: - interest_paid -= repayment_details['unaccrued_interest'] - self.total_interest_paid += repayment_details['unaccrued_interest'] + if interest_paid > repayment_details["unaccrued_interest"]: + interest_paid -= repayment_details["unaccrued_interest"] + self.total_interest_paid += repayment_details["unaccrued_interest"] else: # get no of days for which interest can be paid - per_day_interest = get_per_day_interest(self.pending_principal_amount, - self.rate_of_interest, self.posting_date) + per_day_interest = get_per_day_interest( + self.pending_principal_amount, self.rate_of_interest, self.posting_date + ) - no_of_days = cint(interest_paid/per_day_interest) + no_of_days = cint(interest_paid / per_day_interest) self.total_interest_paid += no_of_days * per_day_interest interest_paid -= no_of_days * per_day_interest @@ -313,8 +388,9 @@ class LoanRepayment(AccountsController): gle_map = [] if self.shortfall_amount and self.amount_paid > self.shortfall_amount: - remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, - self.against_loan) + remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format( + self.shortfall_amount, self.against_loan + ) elif self.shortfall_amount: remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount) else: @@ -327,91 +403,113 @@ class LoanRepayment(AccountsController): if self.total_penalty_paid: gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "against": payment_account, - "debit": self.total_penalty_paid, - "debit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.loan_account, + "against": payment_account, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date), + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": self.penalty_income_account, - "against": self.loan_account, - "credit": self.total_penalty_paid, - "credit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.penalty_income_account, + "against": self.loan_account, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": payment_account, - "against": self.loan_account + ", " + self.penalty_income_account, - "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date), - "party_type": self.applicant_type if self.repay_from_salary else '', - "party": self.applicant if self.repay_from_salary else '' - }) + self.get_gl_dict( + { + "account": payment_account, + "against": self.loan_account + ", " + self.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + "party_type": self.applicant_type if self.repay_from_salary else "", + "party": self.applicant if self.repay_from_salary else "", + } + ) ) gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": payment_account, - "credit": self.amount_paid, - "credit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": payment_account, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) -def create_repayment_entry(loan, applicant, company, posting_date, loan_type, - payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None, - payroll_payable_account=None): - lr = frappe.get_doc({ - "doctype": "Loan Repayment", - "against_loan": loan, - "payment_type": payment_type, - "company": company, - "posting_date": posting_date, - "applicant": applicant, - "penalty_amount": penalty_amount, - "interest_payable": interest_payable, - "payable_principal_amount": payable_principal_amount, - "amount_paid": amount_paid, - "loan_type": loan_type, - "payroll_payable_account": payroll_payable_account - }).insert() +def create_repayment_entry( + loan, + applicant, + company, + posting_date, + loan_type, + payment_type, + interest_payable, + payable_principal_amount, + amount_paid, + penalty_amount=None, + payroll_payable_account=None, +): + + lr = frappe.get_doc( + { + "doctype": "Loan Repayment", + "against_loan": loan, + "payment_type": payment_type, + "company": company, + "posting_date": posting_date, + "applicant": applicant, + "penalty_amount": penalty_amount, + "interest_payable": interest_payable, + "payable_principal_amount": payable_principal_amount, + "amount_paid": amount_paid, + "loan_type": loan_type, + "payroll_payable_account": payroll_payable_account, + } + ).insert() return lr + def get_accrued_interest_entries(against_loan, posting_date=None): if not posting_date: posting_date = getdate() @@ -431,35 +529,43 @@ def get_accrued_interest_entries(against_loan, posting_date=None): AND docstatus = 1 ORDER BY posting_date - """, (against_loan, posting_date), as_dict=1) + """, + (against_loan, posting_date), + as_dict=1, + ) return unpaid_accrued_entries + def get_penalty_details(against_loan): - penalty_details = frappe.db.sql(""" + penalty_details = frappe.db.sql( + """ SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment` where against_loan = %s) and docstatus = 1 and against_loan = %s - """, (against_loan, against_loan)) + """, + (against_loan, against_loan), + ) if penalty_details: return penalty_details[0][0], flt(penalty_details[0][1]) else: return None, 0 + def regenerate_repayment_schedule(loan, cancel=0): from erpnext.loan_management.doctype.loan.loan import ( add_single_month, get_monthly_repayment_amount, ) - loan_doc = frappe.get_doc('Loan', loan) + loan_doc = frappe.get_doc("Loan", loan) next_accrual_date = None accrued_entries = 0 last_repayment_amount = 0 last_balance_amount = 0 - for term in reversed(loan_doc.get('repayment_schedule')): + for term in reversed(loan_doc.get("repayment_schedule")): if not term.is_accrued: next_accrual_date = term.payment_date loan_doc.remove(term) @@ -474,20 +580,23 @@ def regenerate_repayment_schedule(loan, cancel=0): balance_amount = get_pending_principal_amount(loan_doc) - if loan_doc.repayment_method == 'Repay Fixed Amount per Period': - monthly_repayment_amount = flt(balance_amount/len(loan_doc.get('repayment_schedule')) - accrued_entries) + if loan_doc.repayment_method == "Repay Fixed Amount per Period": + monthly_repayment_amount = flt( + balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries + ) else: if not cancel: - monthly_repayment_amount = get_monthly_repayment_amount(balance_amount, - loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries) + monthly_repayment_amount = get_monthly_repayment_amount( + balance_amount, loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries + ) else: monthly_repayment_amount = last_repayment_amount balance_amount = last_balance_amount payment_date = next_accrual_date - while(balance_amount > 0): - interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12*100)) + while balance_amount > 0: + interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12 * 100)) principal_amount = monthly_repayment_amount - interest_amount balance_amount = flt(balance_amount + interest_amount - monthly_repayment_amount) if balance_amount < 0: @@ -495,31 +604,45 @@ def regenerate_repayment_schedule(loan, cancel=0): balance_amount = 0.0 total_payment = principal_amount + interest_amount - loan_doc.append("repayment_schedule", { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount - }) + loan_doc.append( + "repayment_schedule", + { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_amount, + }, + ) next_payment_date = add_single_month(payment_date) payment_date = next_payment_date loan_doc.save() + def get_pending_principal_amount(loan): - if loan.status in ('Disbursed', 'Closed') or loan.disbursed_amount >= loan.loan_amount: - pending_principal_amount = flt(loan.total_payment) - flt(loan.total_principal_paid) \ - - flt(loan.total_interest_payable) - flt(loan.written_off_amount) + if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount: + pending_principal_amount = ( + flt(loan.total_payment) + - flt(loan.total_principal_paid) + - flt(loan.total_interest_payable) + - flt(loan.written_off_amount) + ) else: - pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_principal_paid) \ - - flt(loan.total_interest_payable) - flt(loan.written_off_amount) + pending_principal_amount = ( + flt(loan.disbursed_amount) + - flt(loan.total_principal_paid) + - flt(loan.total_interest_payable) + - flt(loan.written_off_amount) + ) return pending_principal_amount + # This function returns the amounts that are payable at the time of loan repayment based on posting date # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable + def get_amounts(amounts, against_loan, posting_date): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -533,8 +656,8 @@ def get_amounts(amounts, against_loan, posting_date): total_pending_interest = 0 penalty_amount = 0 payable_principal_amount = 0 - final_due_date = '' - due_date = '' + final_due_date = "" + due_date = "" for entry in accrued_interest_entries: # Loan repayment due date is one day after the loan interest is accrued @@ -550,16 +673,25 @@ def get_amounts(amounts, against_loan, posting_date): no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1 - if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': - penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days) + if ( + no_of_late_days > 0 + and (not against_loan_doc.repay_from_salary) + and entry.accrual_type == "Regular" + ): + penalty_amount += ( + entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days + ) total_pending_interest += entry.interest_amount payable_principal_amount += entry.payable_principal_amount - pending_accrual_entries.setdefault(entry.name, { - 'interest_amount': flt(entry.interest_amount, precision), - 'payable_principal_amount': flt(entry.payable_principal_amount, precision) - }) + pending_accrual_entries.setdefault( + entry.name, + { + "interest_amount": flt(entry.interest_amount, precision), + "payable_principal_amount": flt(entry.payable_principal_amount, precision), + }, + ) if due_date and not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) @@ -575,14 +707,18 @@ def get_amounts(amounts, against_loan, posting_date): if pending_days > 0: principal_amount = flt(pending_principal_amount, precision) - per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date) - unaccrued_interest += (pending_days * per_day_interest) + per_day_interest = get_per_day_interest( + principal_amount, loan_type_details.rate_of_interest, posting_date + ) + unaccrued_interest += pending_days * per_day_interest amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) amounts["interest_amount"] = flt(total_pending_interest, precision) amounts["penalty_amount"] = flt(penalty_amount + pending_penalty_amount, precision) - amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) + amounts["payable_amount"] = flt( + payable_principal_amount + total_pending_interest + penalty_amount, precision + ) amounts["pending_accrual_entries"] = pending_accrual_entries amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) @@ -591,24 +727,25 @@ def get_amounts(amounts, against_loan, posting_date): return amounts + @frappe.whitelist() -def calculate_amounts(against_loan, posting_date, payment_type=''): +def calculate_amounts(against_loan, posting_date, payment_type=""): amounts = { - 'penalty_amount': 0.0, - 'interest_amount': 0.0, - 'pending_principal_amount': 0.0, - 'payable_principal_amount': 0.0, - 'payable_amount': 0.0, - 'unaccrued_interest': 0.0, - 'due_date': '' + "penalty_amount": 0.0, + "interest_amount": 0.0, + "pending_principal_amount": 0.0, + "payable_principal_amount": 0.0, + "payable_amount": 0.0, + "unaccrued_interest": 0.0, + "due_date": "", } amounts = get_amounts(amounts, against_loan, posting_date) # update values for closure - if payment_type == 'Loan Closure': - amounts['payable_principal_amount'] = amounts['pending_principal_amount'] - amounts['interest_amount'] += amounts['unaccrued_interest'] - amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] + if payment_type == "Loan Closure": + amounts["payable_principal_amount"] = amounts["pending_principal_amount"] + amounts["interest_amount"] += amounts["unaccrued_interest"] + amounts["payable_amount"] = amounts["payable_principal_amount"] + amounts["interest_amount"] return amounts diff --git a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py index 8d5c525ac31..1d96885e155 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py +++ b/erpnext/loan_management/doctype/loan_security/loan_security_dashboard.py @@ -1,14 +1,8 @@ - - def get_data(): return { - 'fieldname': 'loan_security', - 'transactions': [ - { - 'items': ['Loan Application', 'Loan Security Price'] - }, - { - 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] - } - ] + "fieldname": "loan_security", + "transactions": [ + {"items": ["Loan Application", "Loan Security Price"]}, + {"items": ["Loan Security Pledge", "Loan Security Unpledge"]}, + ], } diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index 7d02645609b..f0d59542753 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -40,22 +40,28 @@ class LoanSecurityPledge(Document): if security.loan_security not in security_list: security_list.append(security.loan_security) else: - frappe.throw(_('Loan Security {0} added multiple times').format(frappe.bold( - security.loan_security))) + frappe.throw( + _("Loan Security {0} added multiple times").format(frappe.bold(security.loan_security)) + ) def validate_loan_security_type(self): - existing_pledge = '' + existing_pledge = "" if self.loan: - existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan, 'docstatus': 1}, ['name']) + existing_pledge = frappe.db.get_value( + "Loan Security Pledge", {"loan": self.loan, "docstatus": 1}, ["name"] + ) if existing_pledge: - loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type']) + loan_security_type = frappe.db.get_value( + "Pledge", {"parent": existing_pledge}, ["loan_security_type"] + ) else: loan_security_type = self.securities[0].loan_security_type - ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", - fields=["name", "loan_to_value_ratio"], as_list=1)) + ltv_ratio_map = frappe._dict( + frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1) + ) ltv_ratio = ltv_ratio_map.get(loan_security_type) @@ -63,7 +69,6 @@ class LoanSecurityPledge(Document): if ltv_ratio_map.get(security.loan_security_type) != ltv_ratio: frappe.throw(_("Loan Securities with different LTV ratio cannot be pledged against one loan")) - def set_pledge_amount(self): total_security_value = 0 maximum_loan_value = 0 @@ -77,10 +82,10 @@ class LoanSecurityPledge(Document): pledge.loan_security_price = get_loan_security_price(pledge.loan_security) if not pledge.qty: - pledge.qty = cint(pledge.amount/pledge.loan_security_price) + pledge.qty = cint(pledge.amount / pledge.loan_security_price) pledge.amount = pledge.qty * pledge.loan_security_price - pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut/100)) + pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut / 100)) total_security_value += pledge.amount maximum_loan_value += pledge.post_haircut_amount @@ -88,12 +93,19 @@ class LoanSecurityPledge(Document): self.total_security_value = total_security_value self.maximum_loan_value = maximum_loan_value + def update_loan(loan, maximum_value_against_pledge, cancel=0): - maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) + maximum_loan_value = frappe.db.get_value("Loan", {"name": loan}, ["maximum_loan_amount"]) if cancel: - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s - WHERE name=%s""", (maximum_loan_value - maximum_value_against_pledge, loan)) + frappe.db.sql( + """ UPDATE `tabLoan` SET maximum_loan_amount=%s + WHERE name=%s""", + (maximum_loan_value - maximum_value_against_pledge, loan), + ) else: - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 - WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan)) + frappe.db.sql( + """ UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 + WHERE name=%s""", + (maximum_loan_value + maximum_value_against_pledge, loan), + ) diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py index fca9dd6bcb9..45c4459ac3f 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py @@ -17,23 +17,37 @@ class LoanSecurityPrice(Document): if self.valid_from > self.valid_upto: frappe.throw(_("Valid From Time must be lesser than Valid Upto Time.")) - existing_loan_security = frappe.db.sql(""" SELECT name from `tabLoan Security Price` + existing_loan_security = frappe.db.sql( + """ SELECT name from `tabLoan Security Price` WHERE loan_security = %s AND name != %s AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """, - (self.loan_security, self.name, self.valid_from, self.valid_upto, self.valid_from, self.valid_upto)) + ( + self.loan_security, + self.name, + self.valid_from, + self.valid_upto, + self.valid_from, + self.valid_upto, + ), + ) if existing_loan_security: frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0])) + @frappe.whitelist() def get_loan_security_price(loan_security, valid_time=None): if not valid_time: valid_time = get_datetime() - loan_security_price = frappe.db.get_value("Loan Security Price", { - 'loan_security': loan_security, - 'valid_from': ("<=",valid_time), - 'valid_upto': (">=", valid_time) - }, 'loan_security_price') + loan_security_price = frappe.db.get_value( + "Loan Security Price", + { + "loan_security": loan_security, + "valid_from": ("<=", valid_time), + "valid_upto": (">=", valid_time), + }, + "loan_security_price", + ) if not loan_security_price: frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 20e451b81ea..b901e626b43 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -14,27 +14,42 @@ from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpled class LoanSecurityShortfall(Document): pass + def update_shortfall_status(loan, security_value, on_cancel=0): - loan_security_shortfall = frappe.db.get_value("Loan Security Shortfall", - {"loan": loan, "status": "Pending"}, ['name', 'shortfall_amount'], as_dict=1) + loan_security_shortfall = frappe.db.get_value( + "Loan Security Shortfall", + {"loan": loan, "status": "Pending"}, + ["name", "shortfall_amount"], + as_dict=1, + ) if not loan_security_shortfall: return if security_value >= loan_security_shortfall.shortfall_amount: - frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, { - "status": "Completed", - "shortfall_amount": loan_security_shortfall.shortfall_amount, - "shortfall_percentage": 0 - }) + frappe.db.set_value( + "Loan Security Shortfall", + loan_security_shortfall.name, + { + "status": "Completed", + "shortfall_amount": loan_security_shortfall.shortfall_amount, + "shortfall_percentage": 0, + }, + ) else: - frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, - "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) + frappe.db.set_value( + "Loan Security Shortfall", + loan_security_shortfall.name, + "shortfall_amount", + loan_security_shortfall.shortfall_amount - security_value, + ) @frappe.whitelist() def add_security(loan): - loan_details = frappe.db.get_value("Loan", loan, ['applicant', 'company', 'applicant_type'], as_dict=1) + loan_details = frappe.db.get_value( + "Loan", loan, ["applicant", "company", "applicant_type"], as_dict=1 + ) loan_security_pledge = frappe.new_doc("Loan Security Pledge") loan_security_pledge.loan = loan @@ -44,33 +59,51 @@ def add_security(loan): return loan_security_pledge.as_dict() + def check_for_ltv_shortfall(process_loan_security_shortfall): update_time = get_datetime() - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", - fields=["loan_security", "loan_security_price"], - filters = { - "valid_from": ("<=", update_time), - "valid_upto": (">=", update_time) - }, as_list=1)) + loan_security_price_map = frappe._dict( + frappe.get_all( + "Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)}, + as_list=1, + ) + ) - loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', - 'total_interest_payable', 'disbursed_amount', 'status'], - filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) + loans = frappe.get_all( + "Loan", + fields=[ + "name", + "loan_amount", + "total_principal_paid", + "total_payment", + "total_interest_payable", + "disbursed_amount", + "status", + ], + filters={"status": ("in", ["Disbursed", "Partially Disbursed"]), "is_secured_loan": 1}, + ) - loan_shortfall_map = frappe._dict(frappe.get_all("Loan Security Shortfall", - fields=["loan", "name"], filters={"status": "Pending"}, as_list=1)) + loan_shortfall_map = frappe._dict( + frappe.get_all( + "Loan Security Shortfall", fields=["loan", "name"], filters={"status": "Pending"}, as_list=1 + ) + ) loan_security_map = {} for loan in loans: - if loan.status == 'Disbursed': - outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + if loan.status == "Disbursed": + outstanding_amount = ( + flt(loan.total_payment) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid) + ) else: - outstanding_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + outstanding_amount = ( + flt(loan.disbursed_amount) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid) + ) pledged_securities = get_pledged_security_qty(loan.name) ltv_ratio = 0.0 @@ -81,21 +114,36 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): ltv_ratio = get_ltv_ratio(security) security_value += flt(loan_security_price_map.get(security)) * flt(qty) - current_ratio = (outstanding_amount/security_value) * 100 if security_value else 0 + current_ratio = (outstanding_amount / security_value) * 100 if security_value else 0 if current_ratio > ltv_ratio: shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100) - create_loan_security_shortfall(loan.name, outstanding_amount, security_value, shortfall_amount, - current_ratio, process_loan_security_shortfall) + create_loan_security_shortfall( + loan.name, + outstanding_amount, + security_value, + shortfall_amount, + current_ratio, + process_loan_security_shortfall, + ) elif loan_shortfall_map.get(loan.name): shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100) if shortfall_amount <= 0: shortfall = loan_shortfall_map.get(loan.name) update_pending_shortfall(shortfall) -def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, shortfall_ratio, - process_loan_security_shortfall): - existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") + +def create_loan_security_shortfall( + loan, + loan_amount, + security_value, + shortfall_amount, + shortfall_ratio, + process_loan_security_shortfall, +): + existing_shortfall = frappe.db.get_value( + "Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name" + ) if existing_shortfall: ltv_shortfall = frappe.get_doc("Loan Security Shortfall", existing_shortfall) @@ -111,16 +159,17 @@ def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_ ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall ltv_shortfall.save() + def get_ltv_ratio(loan_security): - loan_security_type = frappe.db.get_value('Loan Security', loan_security, 'loan_security_type') - ltv_ratio = frappe.db.get_value('Loan Security Type', loan_security_type, 'loan_to_value_ratio') + loan_security_type = frappe.db.get_value("Loan Security", loan_security, "loan_security_type") + ltv_ratio = frappe.db.get_value("Loan Security Type", loan_security_type, "loan_to_value_ratio") return ltv_ratio + def update_pending_shortfall(shortfall): # Get all pending loan security shortfall - frappe.db.set_value("Loan Security Shortfall", shortfall, - { - "status": "Completed", - "shortfall_amount": 0, - "shortfall_percentage": 0 - }) + frappe.db.set_value( + "Loan Security Shortfall", + shortfall, + {"status": "Completed", "shortfall_amount": 0, "shortfall_percentage": 0}, + ) diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py index daa9958808f..8fc4520ccd2 100644 --- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type_dashboard.py @@ -1,14 +1,8 @@ - - def get_data(): return { - 'fieldname': 'loan_security_type', - 'transactions': [ - { - 'items': ['Loan Security', 'Loan Security Price'] - }, - { - 'items': ['Loan Security Pledge', 'Loan Security Unpledge'] - } - ] + "fieldname": "loan_security_type", + "transactions": [ + {"items": ["Loan Security", "Loan Security Price"]}, + {"items": ["Loan Security Pledge", "Loan Security Unpledge"]}, + ], } diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index b716288fc59..731b65e9a29 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -16,7 +16,7 @@ class LoanSecurityUnpledge(Document): def on_cancel(self): self.update_loan_status(cancel=1) - self.db_set('status', 'Requested') + self.db_set("status", "Requested") def validate_duplicate_securities(self): security_list = [] @@ -24,8 +24,11 @@ class LoanSecurityUnpledge(Document): if d.loan_security not in security_list: security_list.append(d.loan_security) else: - frappe.throw(_("Row {0}: Loan Security {1} added multiple times").format( - d.idx, frappe.bold(d.loan_security))) + frappe.throw( + _("Row {0}: Loan Security {1} added multiple times").format( + d.idx, frappe.bold(d.loan_security) + ) + ) def validate_unpledge_qty(self): from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( @@ -37,18 +40,33 @@ class LoanSecurityUnpledge(Document): pledge_qty_map = get_pledged_security_qty(self.loan) - ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", - fields=["name", "loan_to_value_ratio"], as_list=1)) + ltv_ratio_map = frappe._dict( + frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1) + ) - loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", - fields=["loan_security", "loan_security_price"], - filters = { - "valid_from": ("<=", get_datetime()), - "valid_upto": (">=", get_datetime()) - }, as_list=1)) + loan_security_price_map = frappe._dict( + frappe.get_all( + "Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters={"valid_from": ("<=", get_datetime()), "valid_upto": (">=", get_datetime())}, + as_list=1, + ) + ) - loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', 'loan_amount', - 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1) + loan_details = frappe.get_value( + "Loan", + self.loan, + [ + "total_payment", + "total_principal_paid", + "loan_amount", + "total_interest_payable", + "written_off_amount", + "disbursed_amount", + "status", + ], + as_dict=1, + ) pending_principal_amount = get_pending_principal_amount(loan_details) @@ -59,8 +77,13 @@ class LoanSecurityUnpledge(Document): for security in self.securities: pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: - msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(security.idx, pledged_qty, security.uom, - frappe.bold(security.loan_security), frappe.bold(self.loan)) + msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format( + security.idx, + pledged_qty, + security.uom, + frappe.bold(security.loan_security), + frappe.bold(self.loan), + ) msg += "
    " msg += _("You are trying to unpledge more.") frappe.throw(msg, title=_("Loan Security Unpledge Error")) @@ -79,14 +102,14 @@ class LoanSecurityUnpledge(Document): if not security_value and flt(pending_principal_amount, 2) > 0: self._throw(security_value, pending_principal_amount, ltv_ratio) - if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: + if security_value and flt(pending_principal_amount / security_value) * 100 > ltv_ratio: self._throw(security_value, pending_principal_amount, ltv_ratio) def _throw(self, security_value, pending_principal_amount, ltv_ratio): msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value)) - msg += '
    ' + msg += "
    " msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2))) - msg += '
    ' + msg += "
    " msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio)) frappe.throw(msg, title=_("Loan To Value ratio breach")) @@ -96,13 +119,13 @@ class LoanSecurityUnpledge(Document): def approve(self): if self.status == "Approved" and not self.unpledge_time: self.update_loan_status() - self.db_set('unpledge_time', get_datetime()) + self.db_set("unpledge_time", get_datetime()) def update_loan_status(self, cancel=0): if cancel: - loan_status = frappe.get_value('Loan', self.loan, 'status') - if loan_status == 'Closed': - frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested') + loan_status = frappe.get_value("Loan", self.loan, "status") + if loan_status == "Closed": + frappe.db.set_value("Loan", self.loan, "status", "Loan Closure Requested") else: pledged_qty = 0 current_pledges = get_pledged_security_qty(self.loan) @@ -111,34 +134,41 @@ class LoanSecurityUnpledge(Document): pledged_qty += qty if not pledged_qty: - frappe.db.set_value('Loan', self.loan, - { - 'status': 'Closed', - 'closure_date': getdate() - }) + frappe.db.set_value("Loan", self.loan, {"status": "Closed", "closure_date": getdate()}) + @frappe.whitelist() def get_pledged_security_qty(loan): current_pledges = {} - unpledges = frappe._dict(frappe.db.sql(""" + unpledges = frappe._dict( + frappe.db.sql( + """ SELECT u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE up.loan = %s AND u.parent = up.name AND up.status = 'Approved' GROUP BY u.loan_security - """, (loan))) + """, + (loan), + ) + ) - pledges = frappe._dict(frappe.db.sql(""" + pledges = frappe._dict( + frappe.db.sql( + """ SELECT p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE lp.loan = %s AND p.parent = lp.name AND lp.status = 'Pledged' GROUP BY p.loan_security - """, (loan))) + """, + (loan), + ) + ) for security, qty in iteritems(pledges): current_pledges.setdefault(security, qty) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.py b/erpnext/loan_management/doctype/loan_type/loan_type.py index 592229cf994..51ee05b277e 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.py +++ b/erpnext/loan_management/doctype/loan_type/loan_type.py @@ -12,12 +12,20 @@ class LoanType(Document): self.validate_accounts() def validate_accounts(self): - for fieldname in ['payment_account', 'loan_account', 'interest_income_account', 'penalty_income_account']: - company = frappe.get_value("Account", self.get(fieldname), 'company') + for fieldname in [ + "payment_account", + "loan_account", + "interest_income_account", + "penalty_income_account", + ]: + company = frappe.get_value("Account", self.get(fieldname), "company") if company and company != self.company: - frappe.throw(_("Account {0} does not belong to company {1}").format(frappe.bold(self.get(fieldname)), - frappe.bold(self.company))) + frappe.throw( + _("Account {0} does not belong to company {1}").format( + frappe.bold(self.get(fieldname)), frappe.bold(self.company) + ) + ) - if self.get('loan_account') == self.get('payment_account'): - frappe.throw(_('Loan Account and Payment Account cannot be same')) + if self.get("loan_account") == self.get("payment_account"): + frappe.throw(_("Loan Account and Payment Account cannot be same")) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py index 19026da2f66..e2467c64558 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py +++ b/erpnext/loan_management/doctype/loan_type/loan_type_dashboard.py @@ -1,14 +1,5 @@ - - def get_data(): return { - 'fieldname': 'loan_type', - 'transactions': [ - { - 'items': ['Loan Repayment', 'Loan'] - }, - { - 'items': ['Loan Application'] - } - ] + "fieldname": "loan_type", + "transactions": [{"items": ["Loan Repayment", "Loan"]}, {"items": ["Loan Application"]}], } diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 35be587f87f..e19fd15fc84 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -22,11 +22,16 @@ class LoanWriteOff(AccountsController): def validate_write_off_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 - total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, - ['total_payment', 'total_principal_paid','total_interest_payable', 'written_off_amount']) + total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value( + "Loan", + self.loan, + ["total_payment", "total_principal_paid", "total_interest_payable", "written_off_amount"], + ) - pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount), - precision) + pending_principal_amount = flt( + flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount), + precision, + ) if self.write_off_amount > pending_principal_amount: frappe.throw(_("Write off amount cannot be greater than pending principal amount")) @@ -37,52 +42,55 @@ class LoanWriteOff(AccountsController): def on_cancel(self): self.update_outstanding_amount(cancel=1) - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] self.make_gl_entries(cancel=1) def update_outstanding_amount(self, cancel=0): - written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount') + written_off_amount = frappe.db.get_value("Loan", self.loan, "written_off_amount") if cancel: written_off_amount -= self.write_off_amount else: written_off_amount += self.write_off_amount - frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_amount) - + frappe.db.set_value("Loan", self.loan, "written_off_amount", written_off_amount) def make_gl_entries(self, cancel=0): gl_entries = [] loan_details = frappe.get_doc("Loan", self.loan) gl_entries.append( - self.get_gl_dict({ - "account": self.write_off_account, - "against": loan_details.loan_account, - "debit": self.write_off_amount, - "debit_in_account_currency": self.write_off_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": self.write_off_account, + "against": loan_details.loan_account, + "debit": self.write_off_amount, + "debit_in_account_currency": self.write_off_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) gl_entries.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, - "against": self.write_off_account, - "credit": self.write_off_amount, - "credit_in_account_currency": self.write_off_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) + self.get_gl_dict( + { + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, + "against": self.write_off_account, + "credit": self.write_off_amount, + "credit_in_account_currency": self.write_off_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date), + } + ) ) make_gl_entries(gl_entries, cancel=cancel, merge_entries=False) diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 4c34ccd983e..81464a36c3d 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -17,24 +17,36 @@ class ProcessLoanInterestAccrual(Document): open_loans = [] if self.loan: - loan_doc = frappe.get_doc('Loan', self.loan) + loan_doc = frappe.get_doc("Loan", self.loan) if loan_doc: open_loans.append(loan_doc) - if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': - make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) + if (not self.loan or not loan_doc.is_term_loan) and self.process_type != "Term Loans": + make_accrual_interest_entry_for_demand_loans( + self.posting_date, + self.name, + open_loans=open_loans, + loan_type=self.loan_type, + accrual_type=self.accrual_type, + ) - if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': - make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, - loan_type=self.loan_type, accrual_type=self.accrual_type) + if (not self.loan or loan_doc.is_term_loan) and self.process_type != "Demand Loans": + make_accrual_interest_entry_for_term_loans( + self.posting_date, + self.name, + term_loan=self.loan, + loan_type=self.loan_type, + accrual_type=self.accrual_type, + ) -def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"): - loan_process = frappe.new_doc('Process Loan Interest Accrual') +def process_loan_interest_accrual_for_demand_loans( + posting_date=None, loan_type=None, loan=None, accrual_type="Regular" +): + loan_process = frappe.new_doc("Process Loan Interest Accrual") loan_process.posting_date = posting_date or nowdate() loan_process.loan_type = loan_type - loan_process.process_type = 'Demand Loans' + loan_process.process_type = "Demand Loans" loan_process.loan = loan loan_process.accrual_type = accrual_type @@ -42,25 +54,26 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type= return loan_process.name + def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): if not term_loan_accrual_pending(posting_date or nowdate()): return - loan_process = frappe.new_doc('Process Loan Interest Accrual') + loan_process = frappe.new_doc("Process Loan Interest Accrual") loan_process.posting_date = posting_date or nowdate() loan_process.loan_type = loan_type - loan_process.process_type = 'Term Loans' + loan_process.process_type = "Term Loans" loan_process.loan = loan loan_process.submit() return loan_process.name + def term_loan_accrual_pending(date): - pending_accrual = frappe.db.get_value('Repayment Schedule', { - 'payment_date': ('<=', date), - 'is_accrued': 0 - }) + pending_accrual = frappe.db.get_value( + "Repayment Schedule", {"payment_date": ("<=", date), "is_accrued": 0} + ) return pending_accrual diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py index 4195960890c..ac85df761ce 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'process_loan_interest_accrual', - 'transactions': [ - { - 'items': ['Loan Interest Accrual'] - } - ] + "fieldname": "process_loan_interest_accrual", + "transactions": [{"items": ["Loan Interest Accrual"]}], } diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py index ba9fb0c449f..fffc5d4876b 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py @@ -13,16 +13,18 @@ from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_short class ProcessLoanSecurityShortfall(Document): def onload(self): - self.set_onload('update_time', get_datetime()) + self.set_onload("update_time", get_datetime()) def on_submit(self): check_for_ltv_shortfall(self.name) + def create_process_loan_security_shortfall(): if check_for_secured_loans(): process = frappe.new_doc("Process Loan Security Shortfall") process.update_time = get_datetime() process.submit() + def check_for_secured_loans(): - return frappe.db.count('Loan', {'docstatus': 1, 'is_secured_loan': 1}) + return frappe.db.count("Loan", {"docstatus": 1, "is_secured_loan": 1}) diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py index fa9d18b6fab..4d7b1630bad 100644 --- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py +++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'process_loan_security_shortfall', - 'transactions': [ - { - 'items': ['Loan Security Shortfall'] - } - ] + "fieldname": "process_loan_security_shortfall", + "transactions": [{"items": ["Loan Security Shortfall"]}], } diff --git a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py index 6063b7bad8b..e7487cb34d3 100644 --- a/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py +++ b/erpnext/loan_management/doctype/sanctioned_loan_amount/sanctioned_loan_amount.py @@ -9,9 +9,13 @@ from frappe.model.document import Document class SanctionedLoanAmount(Document): def validate(self): - sanctioned_doc = frappe.db.exists('Sanctioned Loan Amount', {'applicant': self.applicant, 'company': self.company}) + sanctioned_doc = frappe.db.exists( + "Sanctioned Loan Amount", {"applicant": self.applicant, "company": self.company} + ) if sanctioned_doc and sanctioned_doc != self.name: - frappe.throw(_("Sanctioned Loan Amount already exists for {0} against company {1}").format( - frappe.bold(self.applicant), frappe.bold(self.company) - )) + frappe.throw( + _("Sanctioned Loan Amount already exists for {0} against company {1}").format( + frappe.bold(self.applicant), frappe.bold(self.company) + ) + ) diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py index 8ebca39061c..75c4b2877b1 100644 --- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py +++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py @@ -18,84 +18,166 @@ def execute(filters=None): def get_columns(filters): columns = [ - {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, - {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, - {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, - {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, - {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + { + "label": _("Applicant Type"), + "fieldname": "applicant_type", + "options": "DocType", + "width": 100, + }, + { + "label": _("Applicant Name"), + "fieldname": "applicant_name", + "fieldtype": "Dynamic Link", + "options": "applicant_type", + "width": 150, + }, + { + "label": _("Loan Security"), + "fieldname": "loan_security", + "fieldtype": "Link", + "options": "Loan Security", + "width": 160, + }, + { + "label": _("Loan Security Code"), + "fieldname": "loan_security_code", + "fieldtype": "Data", + "width": 100, + }, + { + "label": _("Loan Security Name"), + "fieldname": "loan_security_name", + "fieldtype": "Data", + "width": 150, + }, {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, - {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + { + "label": _("Loan Security Type"), + "fieldname": "loan_security_type", + "fieldtype": "Link", + "options": "Loan Security Type", + "width": 120, + }, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, - {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100}, - {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, - {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + { + "label": _("Latest Price"), + "fieldname": "latest_price", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("Price Valid Upto"), + "fieldname": "price_valid_upto", + "fieldtype": "Datetime", + "width": 100, + }, + { + "label": _("Current Value"), + "fieldname": "current_value", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("% Of Applicant Portfolio"), + "fieldname": "portfolio_percent", + "fieldtype": "Percentage", + "width": 100, + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "options": "Currency", + "hidden": 1, + "width": 100, + }, ] return columns + def get_data(filters): data = [] loan_security_details = get_loan_security_details() - pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, - loan_security_details) + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty( + filters, loan_security_details + ) - currency = erpnext.get_company_currency(filters.get('company')) + currency = erpnext.get_company_currency(filters.get("company")) for key, qty in iteritems(pledge_values): if qty: row = {} - current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(key[1], {}).get('valid_upto') + current_value = flt(qty * loan_security_details.get(key[1], {}).get("latest_price", 0)) + valid_upto = loan_security_details.get(key[1], {}).get("valid_upto") row.update(loan_security_details.get(key[1])) - row.update({ - 'applicant_type': applicant_type_map.get(key[0]), - 'applicant_name': key[0], - 'total_qty': qty, - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \ + row.update( + { + "applicant_type": applicant_type_map.get(key[0]), + "applicant_name": key[0], + "total_qty": qty, + "current_value": current_value, + "price_valid_upto": valid_upto, + "portfolio_percent": flt(current_value * 100 / total_value_map.get(key[0]), 2) + if total_value_map.get(key[0]) else 0.0, - 'currency': currency - }) + "currency": currency, + } + ) data.append(row) return data + def get_loan_security_details(): security_detail_map = {} loan_security_price_map = {} lsp_validity_map = {} - loan_security_prices = frappe.db.sql(""" + loan_security_prices = frappe.db.sql( + """ SELECT loan_security, loan_security_price, valid_upto FROM `tabLoan Security Price` t1 WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2 WHERE t1.loan_security = t2.loan_security) - """, as_dict=1) + """, + as_dict=1, + ) for security in loan_security_prices: loan_security_price_map.setdefault(security.loan_security, security.loan_security_price) lsp_validity_map.setdefault(security.loan_security, security.valid_upto) - loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security', - 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type', - 'disabled']) + loan_security_details = frappe.get_all( + "Loan Security", + fields=[ + "name as loan_security", + "loan_security_code", + "loan_security_name", + "haircut", + "loan_security_type", + "disabled", + ], + ) for security in loan_security_details: - security.update({ - 'latest_price': flt(loan_security_price_map.get(security.loan_security)), - 'valid_upto': lsp_validity_map.get(security.loan_security) - }) + security.update( + { + "latest_price": flt(loan_security_price_map.get(security.loan_security)), + "valid_upto": lsp_validity_map.get(security.loan_security), + } + ) security_detail_map.setdefault(security.loan_security, security) return security_detail_map + def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): current_pledges = {} total_value_map = {} @@ -103,39 +185,53 @@ def get_applicant_wise_total_loan_security_qty(filters, loan_security_details): applicant_wise_unpledges = {} conditions = "" - if filters.get('company'): + if filters.get("company"): conditions = "AND company = %(company)s" - unpledges = frappe.db.sql(""" + unpledges = frappe.db.sql( + """ SELECT up.applicant, u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY up.applicant, u.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for unpledge in unpledges: applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty) - pledges = frappe.db.sql(""" + pledges = frappe.db.sql( + """ SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY lp.applicant, p.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for security in pledges: current_pledges.setdefault((security.applicant, security.loan_security), security.qty) total_value_map.setdefault(security.applicant, 0.0) applicant_type_map.setdefault(security.applicant, security.applicant_type) - current_pledges[(security.applicant, security.loan_security)] -= \ - applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0) + current_pledges[(security.applicant, security.loan_security)] -= applicant_wise_unpledges.get( + (security.applicant, security.loan_security), 0.0 + ) - total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \ - * loan_security_details.get(security.loan_security, {}).get('latest_price', 0) + total_value_map[security.applicant] += current_pledges.get( + (security.applicant, security.loan_security) + ) * loan_security_details.get(security.loan_security, {}).get("latest_price", 0) return current_pledges, total_value_map, applicant_type_map diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py index 7c512679567..9186ce61743 100644 --- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py +++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py @@ -17,41 +17,148 @@ def execute(filters=None): data = get_active_loan_details(filters) return columns, data + def get_columns(filters): columns = [ {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, - {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100}, - {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150}, - {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100}, - {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120}, - {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100}, - {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100}, - {"label": _("Loan To Value Ratio"), "fieldname": "loan_to_value", "fieldtype": "Percent", "width": 100}, - {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + { + "label": _("Applicant Type"), + "fieldname": "applicant_type", + "options": "DocType", + "width": 100, + }, + { + "label": _("Applicant Name"), + "fieldname": "applicant_name", + "fieldtype": "Dynamic Link", + "options": "applicant_type", + "width": 150, + }, + { + "label": _("Loan Type"), + "fieldname": "loan_type", + "fieldtype": "Link", + "options": "Loan Type", + "width": 100, + }, + { + "label": _("Sanctioned Amount"), + "fieldname": "sanctioned_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Disbursed Amount"), + "fieldname": "disbursed_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Penalty Amount"), + "fieldname": "penalty", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Accrued Interest"), + "fieldname": "accrued_interest", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Total Repayment"), + "fieldname": "total_repayment", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Principal Outstanding"), + "fieldname": "principal_outstanding", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Interest Outstanding"), + "fieldname": "interest_outstanding", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Total Outstanding"), + "fieldname": "total_outstanding", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Undue Booked Interest"), + "fieldname": "undue_interest", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Interest %"), + "fieldname": "rate_of_interest", + "fieldtype": "Percent", + "width": 100, + }, + { + "label": _("Penalty Interest %"), + "fieldname": "penalty_interest", + "fieldtype": "Percent", + "width": 100, + }, + { + "label": _("Loan To Value Ratio"), + "fieldname": "loan_to_value", + "fieldtype": "Percent", + "width": 100, + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "options": "Currency", + "hidden": 1, + "width": 100, + }, ] return columns + def get_active_loan_details(filters): filter_obj = {"status": ("!=", "Closed")} - if filters.get('company'): - filter_obj.update({'company': filters.get('company')}) + if filters.get("company"): + filter_obj.update({"company": filters.get("company")}) - loan_details = frappe.get_all("Loan", - fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type", - "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid", - "total_interest_payable", "written_off_amount", "status"], - filters=filter_obj) + loan_details = frappe.get_all( + "Loan", + fields=[ + "name as loan", + "applicant_type", + "applicant as applicant_name", + "loan_type", + "disbursed_amount", + "rate_of_interest", + "total_payment", + "total_principal_paid", + "total_interest_payable", + "written_off_amount", + "status", + ], + filters=filter_obj, + ) loan_list = [d.loan for d in loan_details] @@ -62,70 +169,105 @@ def get_active_loan_details(filters): penal_interest_rate_map = get_penal_interest_rate_map() payments = get_payments(loan_list) accrual_map = get_interest_accruals(loan_list) - currency = erpnext.get_company_currency(filters.get('company')) + currency = erpnext.get_company_currency(filters.get("company")) for loan in loan_details: - total_payment = loan.total_payment if loan.status == 'Disbursed' else loan.disbursed_amount + total_payment = loan.total_payment if loan.status == "Disbursed" else loan.disbursed_amount - loan.update({ - "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)), - "principal_outstanding": flt(total_payment) - flt(loan.total_principal_paid) \ - - flt(loan.total_interest_payable) - flt(loan.written_off_amount), - "total_repayment": flt(payments.get(loan.loan)), - "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), - "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), - "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), - "penalty_interest": penal_interest_rate_map.get(loan.loan_type), - "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")), - "loan_to_value": 0.0, - "currency": currency - }) + loan.update( + { + "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)), + "principal_outstanding": flt(total_payment) + - flt(loan.total_principal_paid) + - flt(loan.total_interest_payable) + - flt(loan.written_off_amount), + "total_repayment": flt(payments.get(loan.loan)), + "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), + "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), + "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), + "penalty_interest": penal_interest_rate_map.get(loan.loan_type), + "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")), + "loan_to_value": 0.0, + "currency": currency, + } + ) - loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \ - + loan['penalty'] + loan["total_outstanding"] = ( + loan["principal_outstanding"] + loan["interest_outstanding"] + loan["penalty"] + ) if loan_wise_security_value.get(loan.loan): - loan['loan_to_value'] = flt((loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan)) + loan["loan_to_value"] = flt( + (loan["principal_outstanding"] * 100) / loan_wise_security_value.get(loan.loan) + ) return loan_details + def get_sanctioned_amount_map(): - return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], - as_list=1)) + return frappe._dict( + frappe.get_all( + "Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"], as_list=1 + ) + ) + def get_payments(loans): - return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"], - filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1)) + return frappe._dict( + frappe.get_all( + "Loan Repayment", + fields=["against_loan", "sum(amount_paid)"], + filters={"against_loan": ("in", loans)}, + group_by="against_loan", + as_list=1, + ) + ) + def get_interest_accruals(loans): accrual_map = {} - interest_accruals = frappe.get_all("Loan Interest Accrual", - fields=["loan", "interest_amount", "posting_date", "penalty_amount", - "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc") + interest_accruals = frappe.get_all( + "Loan Interest Accrual", + fields=[ + "loan", + "interest_amount", + "posting_date", + "penalty_amount", + "paid_interest_amount", + "accrual_type", + ], + filters={"loan": ("in", loans)}, + order_by="posting_date desc", + ) for entry in interest_accruals: - accrual_map.setdefault(entry.loan, { - "accrued_interest": 0.0, - "undue_interest": 0.0, - "interest_outstanding": 0.0, - "last_accrual_date": '', - "due_date": '' - }) + accrual_map.setdefault( + entry.loan, + { + "accrued_interest": 0.0, + "undue_interest": 0.0, + "interest_outstanding": 0.0, + "last_accrual_date": "", + "due_date": "", + }, + ) - if entry.accrual_type == 'Regular': - if not accrual_map[entry.loan]['due_date']: - accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1) - if not accrual_map[entry.loan]['last_accrual_date']: - accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date + if entry.accrual_type == "Regular": + if not accrual_map[entry.loan]["due_date"]: + accrual_map[entry.loan]["due_date"] = add_days(entry.posting_date, 1) + if not accrual_map[entry.loan]["last_accrual_date"]: + accrual_map[entry.loan]["last_accrual_date"] = entry.posting_date - due_date = accrual_map[entry.loan]['due_date'] - last_accrual_date = accrual_map[entry.loan]['last_accrual_date'] + due_date = accrual_map[entry.loan]["due_date"] + last_accrual_date = accrual_map[entry.loan]["last_accrual_date"] if due_date and getdate(entry.posting_date) < getdate(due_date): - accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount + accrual_map[entry.loan]["interest_outstanding"] += ( + entry.interest_amount - entry.paid_interest_amount + ) else: - accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount + accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount @@ -134,8 +276,12 @@ def get_interest_accruals(loans): return accrual_map + def get_penal_interest_rate_map(): - return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1)) + return frappe._dict( + frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1) + ) + def get_loan_wise_pledges(filters): loan_wise_unpledges = {} @@ -143,37 +289,51 @@ def get_loan_wise_pledges(filters): conditions = "" - if filters.get('company'): + if filters.get("company"): conditions = "AND company = %(company)s" - unpledges = frappe.db.sql(""" + unpledges = frappe.db.sql( + """ SELECT up.loan, u.loan_security, sum(u.qty) as qty FROM `tabLoan Security Unpledge` up, `tabUnpledge` u WHERE u.parent = up.name AND up.status = 'Approved' {conditions} GROUP BY up.loan, u.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for unpledge in unpledges: loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty) - pledges = frappe.db.sql(""" + pledges = frappe.db.sql( + """ SELECT lp.loan, p.loan_security, sum(p.qty) as qty FROM `tabLoan Security Pledge` lp, `tabPledge`p WHERE p.parent = lp.name AND lp.status = 'Pledged' {conditions} GROUP BY lp.loan, p.loan_security - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) for security in pledges: current_pledges.setdefault((security.loan, security.loan_security), security.qty) - current_pledges[(security.loan, security.loan_security)] -= \ - loan_wise_unpledges.get((security.loan, security.loan_security), 0.0) + current_pledges[(security.loan, security.loan_security)] -= loan_wise_unpledges.get( + (security.loan, security.loan_security), 0.0 + ) return current_pledges + def get_loan_wise_security_value(filters, current_pledges): loan_security_details = get_loan_security_details() loan_wise_security_value = {} @@ -181,7 +341,8 @@ def get_loan_wise_security_value(filters, current_pledges): for key in current_pledges: qty = current_pledges.get(key) loan_wise_security_value.setdefault(key[0], 0.0) - loan_wise_security_value[key[0]] += \ - flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + loan_wise_security_value[key[0]] += flt( + qty * loan_security_details.get(key[1], {}).get("latest_price", 0) + ) return loan_wise_security_value diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py index 68fd3d8e8b5..253b994ae04 100644 --- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py +++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py @@ -11,101 +11,96 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(): return [ - { - "label": _("Posting Date"), - "fieldtype": "Date", - "fieldname": "posting_date", - "width": 100 - }, - { - "label": _("Loan Repayment"), - "fieldtype": "Link", - "fieldname": "loan_repayment", - "options": "Loan Repayment", - "width": 100 - }, - { - "label": _("Against Loan"), - "fieldtype": "Link", - "fieldname": "against_loan", - "options": "Loan", - "width": 200 - }, - { - "label": _("Applicant"), - "fieldtype": "Data", - "fieldname": "applicant", - "width": 150 - }, - { - "label": _("Payment Type"), - "fieldtype": "Data", - "fieldname": "payment_type", - "width": 150 - }, - { - "label": _("Principal Amount"), - "fieldtype": "Currency", - "fieldname": "principal_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Interest Amount"), - "fieldtype": "Currency", - "fieldname": "interest", - "options": "currency", - "width": 100 - }, - { - "label": _("Penalty Amount"), - "fieldtype": "Currency", - "fieldname": "penalty", - "options": "currency", - "width": 100 - }, - { - "label": _("Payable Amount"), - "fieldtype": "Currency", - "fieldname": "payable_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Paid Amount"), - "fieldtype": "Currency", - "fieldname": "paid_amount", - "options": "currency", - "width": 100 - }, - { - "label": _("Currency"), - "fieldtype": "Link", - "fieldname": "currency", - "options": "Currency", - "width": 100 - } - ] + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 100}, + { + "label": _("Loan Repayment"), + "fieldtype": "Link", + "fieldname": "loan_repayment", + "options": "Loan Repayment", + "width": 100, + }, + { + "label": _("Against Loan"), + "fieldtype": "Link", + "fieldname": "against_loan", + "options": "Loan", + "width": 200, + }, + {"label": _("Applicant"), "fieldtype": "Data", "fieldname": "applicant", "width": 150}, + {"label": _("Payment Type"), "fieldtype": "Data", "fieldname": "payment_type", "width": 150}, + { + "label": _("Principal Amount"), + "fieldtype": "Currency", + "fieldname": "principal_amount", + "options": "currency", + "width": 100, + }, + { + "label": _("Interest Amount"), + "fieldtype": "Currency", + "fieldname": "interest", + "options": "currency", + "width": 100, + }, + { + "label": _("Penalty Amount"), + "fieldtype": "Currency", + "fieldname": "penalty", + "options": "currency", + "width": 100, + }, + { + "label": _("Payable Amount"), + "fieldtype": "Currency", + "fieldname": "payable_amount", + "options": "currency", + "width": 100, + }, + { + "label": _("Paid Amount"), + "fieldtype": "Currency", + "fieldname": "paid_amount", + "options": "currency", + "width": 100, + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "width": 100, + }, + ] + def get_data(filters): data = [] query_filters = { "docstatus": 1, - "company": filters.get('company'), + "company": filters.get("company"), } - if filters.get('applicant'): - query_filters.update({ - "applicant": filters.get('applicant') - }) + if filters.get("applicant"): + query_filters.update({"applicant": filters.get("applicant")}) - loan_repayments = frappe.get_all("Loan Repayment", - filters = query_filters, - fields=["posting_date", "applicant", "name", "against_loan", "payable_amount", - "pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"] + loan_repayments = frappe.get_all( + "Loan Repayment", + filters=query_filters, + fields=[ + "posting_date", + "applicant", + "name", + "against_loan", + "payable_amount", + "pending_principal_amount", + "interest_payable", + "penalty_amount", + "amount_paid", + ], ) default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") @@ -122,7 +117,7 @@ def get_data(filters): "penalty": repayment.penalty_amount, "payable_amount": repayment.payable_amount, "paid_amount": repayment.amount_paid, - "currency": default_currency + "currency": default_currency, } data.append(row) diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index e3d99952902..4a459413876 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -18,46 +18,110 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): columns = [ - {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160}, - {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100}, - {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150}, + { + "label": _("Loan Security"), + "fieldname": "loan_security", + "fieldtype": "Link", + "options": "Loan Security", + "width": 160, + }, + { + "label": _("Loan Security Code"), + "fieldname": "loan_security_code", + "fieldtype": "Data", + "width": 100, + }, + { + "label": _("Loan Security Name"), + "fieldname": "loan_security_name", + "fieldtype": "Data", + "width": 150, + }, {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100}, - {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120}, + { + "label": _("Loan Security Type"), + "fieldname": "loan_security_type", + "fieldtype": "Link", + "options": "Loan Security Type", + "width": 120, + }, {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80}, {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100}, - {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100}, - {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100}, - {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100}, - {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100}, - {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100}, + { + "label": _("Latest Price"), + "fieldname": "latest_price", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("Price Valid Upto"), + "fieldname": "price_valid_upto", + "fieldtype": "Datetime", + "width": 100, + }, + { + "label": _("Current Value"), + "fieldname": "current_value", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("% Of Total Portfolio"), + "fieldname": "portfolio_percent", + "fieldtype": "Percentage", + "width": 100, + }, + { + "label": _("Pledged Applicant Count"), + "fieldname": "pledged_applicant_count", + "fieldtype": "Percentage", + "width": 100, + }, + { + "label": _("Currency"), + "fieldname": "currency", + "fieldtype": "Currency", + "options": "Currency", + "hidden": 1, + "width": 100, + }, ] return columns + def get_data(filters): data = [] loan_security_details = get_loan_security_details() - current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details) - currency = erpnext.get_company_currency(filters.get('company')) + current_pledges, total_portfolio_value = get_company_wise_loan_security_details( + filters, loan_security_details + ) + currency = erpnext.get_company_currency(filters.get("company")) for security, value in iteritems(current_pledges): - if value.get('qty'): + if value.get("qty"): row = {} - current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0)) - valid_upto = loan_security_details.get(security, {}).get('valid_upto') + current_value = flt( + value.get("qty", 0) * loan_security_details.get(security, {}).get("latest_price", 0) + ) + valid_upto = loan_security_details.get(security, {}).get("valid_upto") row.update(loan_security_details.get(security)) - row.update({ - 'total_qty': value.get('qty'), - 'current_value': current_value, - 'price_valid_upto': valid_upto, - 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2), - 'pledged_applicant_count': value.get('applicant_count'), - 'currency': currency - }) + row.update( + { + "total_qty": value.get("qty"), + "current_value": current_value, + "price_valid_upto": valid_upto, + "portfolio_percent": flt(current_value * 100 / total_portfolio_value, 2), + "pledged_applicant_count": value.get("applicant_count"), + "currency": currency, + } + ) data.append(row) @@ -65,21 +129,19 @@ def get_data(filters): def get_company_wise_loan_security_details(filters, loan_security_details): - pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters, - loan_security_details) + pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty( + filters, loan_security_details + ) total_portfolio_value = 0 security_wise_map = {} for key, qty in iteritems(pledge_values): - security_wise_map.setdefault(key[1], { - 'qty': 0.0, - 'applicant_count': 0.0 - }) + security_wise_map.setdefault(key[1], {"qty": 0.0, "applicant_count": 0.0}) - security_wise_map[key[1]]['qty'] += qty + security_wise_map[key[1]]["qty"] += qty if qty: - security_wise_map[key[1]]['applicant_count'] += 1 + security_wise_map[key[1]]["applicant_count"] += 1 - total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) + total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get("latest_price", 0)) return security_wise_map, total_portfolio_value diff --git a/erpnext/loan_management/report/loan_security_status/loan_security_status.py b/erpnext/loan_management/report/loan_security_status/loan_security_status.py index b7e716880e9..9a5a18001ea 100644 --- a/erpnext/loan_management/report/loan_security_status/loan_security_status.py +++ b/erpnext/loan_management/report/loan_security_status/loan_security_status.py @@ -11,66 +11,41 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): - columns= [ + columns = [ { "label": _("Loan Security Pledge"), "fieldtype": "Link", "fieldname": "loan_security_pledge", "options": "Loan Security Pledge", - "width": 200 - }, - { - "label": _("Loan"), - "fieldtype": "Link", - "fieldname": "loan", - "options": "Loan", - "width": 200 - }, - { - "label": _("Applicant"), - "fieldtype": "Data", - "fieldname": "applicant", - "width": 200 - }, - { - "label": _("Status"), - "fieldtype": "Data", - "fieldname": "status", - "width": 100 - }, - { - "label": _("Pledge Time"), - "fieldtype": "Data", - "fieldname": "pledge_time", - "width": 150 + "width": 200, }, + {"label": _("Loan"), "fieldtype": "Link", "fieldname": "loan", "options": "Loan", "width": 200}, + {"label": _("Applicant"), "fieldtype": "Data", "fieldname": "applicant", "width": 200}, + {"label": _("Status"), "fieldtype": "Data", "fieldname": "status", "width": 100}, + {"label": _("Pledge Time"), "fieldtype": "Data", "fieldname": "pledge_time", "width": 150}, { "label": _("Loan Security"), "fieldtype": "Link", "fieldname": "loan_security", "options": "Loan Security", - "width": 150 - }, - { - "label": _("Quantity"), - "fieldtype": "Float", - "fieldname": "qty", - "width": 100 + "width": 150, }, + {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "qty", "width": 100}, { "label": _("Loan Security Price"), "fieldtype": "Currency", "fieldname": "loan_security_price", "options": "currency", - "width": 200 + "width": 200, }, { "label": _("Loan Security Value"), "fieldtype": "Currency", "fieldname": "loan_security_value", "options": "currency", - "width": 200 + "width": 200, }, { "label": _("Currency"), @@ -78,18 +53,20 @@ def get_columns(filters): "fieldname": "currency", "options": "Currency", "width": 50, - "hidden": 1 - } + "hidden": 1, + }, ] return columns + def get_data(filters): data = [] conditions = get_conditions(filters) - loan_security_pledges = frappe.db.sql(""" + loan_security_pledges = frappe.db.sql( + """ SELECT p.name, p.applicant, p.loan, p.status, p.pledge_time, c.loan_security, c.qty, c.loan_security_price, c.amount @@ -100,7 +77,12 @@ def get_data(filters): AND c.parent = p.name AND p.company = %(company)s {conditions} - """.format(conditions = conditions), (filters), as_dict=1) #nosec + """.format( + conditions=conditions + ), + (filters), + as_dict=1, + ) # nosec default_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") @@ -121,6 +103,7 @@ def get_data(filters): return data + def get_conditions(filters): conditions = [] diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index 07d928c221f..256f66071f3 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -16,17 +16,17 @@ class MaintenanceSchedule(TransactionBase): def generate_schedule(self): if self.docstatus != 0: return - self.set('schedules', []) + self.set("schedules", []) count = 1 - for d in self.get('items'): + for d in self.get("items"): self.validate_maintenance_detail() s_list = [] s_list = self.create_schedule_list(d.start_date, d.end_date, d.no_of_visits, d.sales_person) for i in range(d.no_of_visits): - child = self.append('schedules') + child = self.append("schedules") child.item_code = d.item_code child.item_name = d.item_name - child.scheduled_date = s_list[i].strftime('%Y-%m-%d') + child.scheduled_date = s_list[i].strftime("%Y-%m-%d") if d.serial_no: child.serial_no = d.serial_no child.idx = count @@ -37,18 +37,14 @@ class MaintenanceSchedule(TransactionBase): @frappe.whitelist() def validate_end_date_visits(self): - days_in_period = { - "Weekly": 7, - "Monthly": 30, - "Quarterly": 91, - "Half Yearly": 182, - "Yearly": 365 - } + days_in_period = {"Weekly": 7, "Monthly": 30, "Quarterly": 91, "Half Yearly": 182, "Yearly": 365} for item in self.items: if item.periodicity and item.periodicity != "Random" and item.start_date: if not item.end_date: if item.no_of_visits: - item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) + item.end_date = add_days( + item.start_date, item.no_of_visits * days_in_period[item.periodicity] + ) else: item.end_date = add_days(item.start_date, days_in_period[item.periodicity]) @@ -61,20 +57,23 @@ class MaintenanceSchedule(TransactionBase): item.no_of_visits = cint(diff / days_in_period[item.periodicity]) elif item.no_of_visits > no_of_visits: - item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) + item.end_date = add_days( + item.start_date, item.no_of_visits * days_in_period[item.periodicity] + ) elif item.no_of_visits < no_of_visits: - item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity]) - + item.end_date = add_days( + item.start_date, item.no_of_visits * days_in_period[item.periodicity] + ) def on_submit(self): - if not self.get('schedules'): + if not self.get("schedules"): throw(_("Please click on 'Generate Schedule' to get schedule")) self.check_serial_no_added() self.validate_schedule() email_map = {} - for d in self.get('items'): + for d in self.get("items"): if d.serial_no: serial_nos = get_valid_serial_nos(d.serial_no) self.validate_serial_no(d.item_code, serial_nos, d.start_date) @@ -90,29 +89,37 @@ class MaintenanceSchedule(TransactionBase): if no_email_sp: frappe.msgprint( - _("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format( - self.owner, "
    " + "
    ".join(no_email_sp) - ) + _( + "Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}" + ).format(self.owner, "
    " + "
    ".join(no_email_sp)) ) - scheduled_date = frappe.db.sql("""select scheduled_date from + scheduled_date = frappe.db.sql( + """select scheduled_date from `tabMaintenance Schedule Detail` where sales_person=%s and item_code=%s and - parent=%s""", (d.sales_person, d.item_code, self.name), as_dict=1) + parent=%s""", + (d.sales_person, d.item_code, self.name), + as_dict=1, + ) for key in scheduled_date: - description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) - event = frappe.get_doc({ - "doctype": "Event", - "owner": email_map.get(d.sales_person, self.owner), - "subject": description, - "description": description, - "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", - "event_type": "Private", - }) + description = frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format( + self.name, d.item_code, self.customer + ) + event = frappe.get_doc( + { + "doctype": "Event", + "owner": email_map.get(d.sales_person, self.owner), + "subject": description, + "description": description, + "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", + "event_type": "Private", + } + ) event.add_participant(self.doctype, self.name) event.insert(ignore_permissions=1) - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") def create_schedule_list(self, start_date, end_date, no_of_visit, sales_person): schedule_list = [] @@ -121,11 +128,12 @@ class MaintenanceSchedule(TransactionBase): add_by = date_diff / no_of_visit for visit in range(cint(no_of_visit)): - if (getdate(start_date_copy) < getdate(end_date)): + if getdate(start_date_copy) < getdate(end_date): start_date_copy = add_days(start_date_copy, add_by) if len(schedule_list) < no_of_visit: - schedule_date = self.validate_schedule_date_for_holiday_list(getdate(start_date_copy), - sales_person) + schedule_date = self.validate_schedule_date_for_holiday_list( + getdate(start_date_copy), sales_person + ) if schedule_date > getdate(end_date): schedule_date = getdate(end_date) schedule_list.append(schedule_date) @@ -139,9 +147,11 @@ class MaintenanceSchedule(TransactionBase): if employee: holiday_list = get_holiday_list_for_employee(employee) else: - holiday_list = frappe.get_cached_value('Company', self.company, "default_holiday_list") + holiday_list = frappe.get_cached_value("Company", self.company, "default_holiday_list") - holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` where parent=%s''', holiday_list) + holidays = frappe.db.sql_list( + """select holiday_date from `tabHoliday` where parent=%s""", holiday_list + ) if not validated and holidays: @@ -157,25 +167,28 @@ class MaintenanceSchedule(TransactionBase): def validate_dates_with_periodicity(self): for d in self.get("items"): - if d.start_date and d.end_date and d.periodicity and d.periodicity!="Random": + if d.start_date and d.end_date and d.periodicity and d.periodicity != "Random": date_diff = (getdate(d.end_date) - getdate(d.start_date)).days + 1 days_in_period = { "Weekly": 7, "Monthly": 30, "Quarterly": 90, "Half Yearly": 180, - "Yearly": 365 + "Yearly": 365, } if date_diff < days_in_period[d.periodicity]: - throw(_("Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}") - .format(d.idx, d.periodicity, days_in_period[d.periodicity])) + throw( + _( + "Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}" + ).format(d.idx, d.periodicity, days_in_period[d.periodicity]) + ) def validate_maintenance_detail(self): - if not self.get('items'): + if not self.get("items"): throw(_("Please enter Maintaince Details first")) - for d in self.get('items'): + for d in self.get("items"): if not d.item_code: throw(_("Please select item code")) elif not d.start_date or not d.end_date: @@ -189,11 +202,14 @@ class MaintenanceSchedule(TransactionBase): throw(_("Start date should be less than end date for Item {0}").format(d.item_code)) def validate_sales_order(self): - for d in self.get('items'): + for d in self.get("items"): if d.sales_order: - chk = frappe.db.sql("""select ms.name from `tabMaintenance Schedule` ms, + chk = frappe.db.sql( + """select ms.name from `tabMaintenance Schedule` ms, `tabMaintenance Schedule Item` msi where msi.parent=ms.name and - msi.sales_order=%s and ms.docstatus=1""", d.sales_order) + msi.sales_order=%s and ms.docstatus=1""", + d.sales_order, + ) if chk: throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order)) @@ -209,7 +225,7 @@ class MaintenanceSchedule(TransactionBase): self.generate_schedule() def on_update(self): - frappe.db.set(self, 'status', 'Draft') + frappe.db.set(self, "status", "Draft") def update_amc_date(self, serial_nos, amc_expiry_date=None): for serial_no in serial_nos: @@ -219,65 +235,96 @@ class MaintenanceSchedule(TransactionBase): def validate_serial_no(self, item_code, serial_nos, amc_start_date): for serial_no in serial_nos: - sr_details = frappe.db.get_value("Serial No", serial_no, - ["warranty_expiry_date", "amc_expiry_date", "warehouse", "delivery_date", "item_code"], as_dict=1) + sr_details = frappe.db.get_value( + "Serial No", + serial_no, + ["warranty_expiry_date", "amc_expiry_date", "warehouse", "delivery_date", "item_code"], + as_dict=1, + ) if not sr_details: frappe.throw(_("Serial No {0} not found").format(serial_no)) if sr_details.get("item_code") != item_code: - frappe.throw(_("Serial No {0} does not belong to Item {1}") - .format(frappe.bold(serial_no), frappe.bold(item_code)), title="Invalid") + frappe.throw( + _("Serial No {0} does not belong to Item {1}").format( + frappe.bold(serial_no), frappe.bold(item_code) + ), + title="Invalid", + ) - if sr_details.warranty_expiry_date \ - and getdate(sr_details.warranty_expiry_date) >= getdate(amc_start_date): - throw(_("Serial No {0} is under warranty upto {1}") - .format(serial_no, sr_details.warranty_expiry_date)) + if sr_details.warranty_expiry_date and getdate(sr_details.warranty_expiry_date) >= getdate( + amc_start_date + ): + throw( + _("Serial No {0} is under warranty upto {1}").format( + serial_no, sr_details.warranty_expiry_date + ) + ) - if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate(amc_start_date): - throw(_("Serial No {0} is under maintenance contract upto {1}") - .format(serial_no, sr_details.amc_expiry_date)) + if sr_details.amc_expiry_date and getdate(sr_details.amc_expiry_date) >= getdate( + amc_start_date + ): + throw( + _("Serial No {0} is under maintenance contract upto {1}").format( + serial_no, sr_details.amc_expiry_date + ) + ) - if not sr_details.warehouse and sr_details.delivery_date and \ - getdate(sr_details.delivery_date) >= getdate(amc_start_date): - throw(_("Maintenance start date can not be before delivery date for Serial No {0}") - .format(serial_no)) + if ( + not sr_details.warehouse + and sr_details.delivery_date + and getdate(sr_details.delivery_date) >= getdate(amc_start_date) + ): + throw( + _("Maintenance start date can not be before delivery date for Serial No {0}").format( + serial_no + ) + ) def validate_schedule(self): - item_lst1 =[] - item_lst2 =[] - for d in self.get('items'): + item_lst1 = [] + item_lst2 = [] + for d in self.get("items"): if d.item_code not in item_lst1: item_lst1.append(d.item_code) - for m in self.get('schedules'): + for m in self.get("schedules"): if m.item_code not in item_lst2: item_lst2.append(m.item_code) if len(item_lst1) != len(item_lst2): - throw(_("Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule'")) + throw( + _( + "Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule'" + ) + ) else: for x in item_lst1: if x not in item_lst2: throw(_("Please click on 'Generate Schedule'")) def check_serial_no_added(self): - serial_present =[] - for d in self.get('items'): + serial_present = [] + for d in self.get("items"): if d.serial_no: serial_present.append(d.item_code) - for m in self.get('schedules'): + for m in self.get("schedules"): if serial_present: if m.item_code in serial_present and not m.serial_no: - throw(_("Please click on 'Generate Schedule' to fetch Serial No added for Item {0}").format(m.item_code)) + throw( + _("Please click on 'Generate Schedule' to fetch Serial No added for Item {0}").format( + m.item_code + ) + ) def on_cancel(self): - for d in self.get('items'): + for d in self.get("items"): if d.serial_no: serial_nos = get_valid_serial_nos(d.serial_no) self.update_amc_date(serial_nos) - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") delete_events(self.doctype, self.name) def on_trash(self): @@ -301,23 +348,26 @@ class MaintenanceSchedule(TransactionBase): return items elif data_type == "id": for schedule in self.schedules: - if schedule.item_name == item_name and s_date == formatdate(schedule.scheduled_date, "dd-mm-yyyy"): + if schedule.item_name == item_name and s_date == formatdate( + schedule.scheduled_date, "dd-mm-yyyy" + ): return schedule.name + @frappe.whitelist() def get_serial_nos_from_schedule(item_code, schedule=None): serial_nos = [] if schedule: - serial_nos = frappe.db.get_value('Maintenance Schedule Item', { - 'parent': schedule, - 'item_code': item_code - }, 'serial_no') + serial_nos = frappe.db.get_value( + "Maintenance Schedule Item", {"parent": schedule, "item_code": item_code}, "serial_no" + ) if serial_nos: serial_nos = get_serial_nos(serial_nos) return serial_nos + @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=None): from frappe.model.mapper import get_mapped_doc @@ -331,27 +381,26 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No if len(serial_nos) == 1: target.serial_no = serial_nos[0] else: - target.serial_no = '' + target.serial_no = "" - doclist = get_mapped_doc("Maintenance Schedule", source_name, { - "Maintenance Schedule": { - "doctype": "Maintenance Visit", - "field_map": { - "name": "maintenance_schedule" + doclist = get_mapped_doc( + "Maintenance Schedule", + source_name, + { + "Maintenance Schedule": { + "doctype": "Maintenance Visit", + "field_map": {"name": "maintenance_schedule"}, + "validation": {"docstatus": ["=", 1]}, + "postprocess": update_status_and_detail, }, - "validation": { - "docstatus": ["=", 1] + "Maintenance Schedule Item": { + "doctype": "Maintenance Visit Purpose", + "condition": lambda doc: doc.item_name == item_name, + "field_map": {"sales_person": "service_person"}, + "postprocess": update_serial, }, - "postprocess": update_status_and_detail }, - "Maintenance Schedule Item": { - "doctype": "Maintenance Visit Purpose", - "condition": lambda doc: doc.item_name == item_name, - "field_map": { - "sales_person": "service_person" - }, - "postprocess": update_serial - } - }, target_doc) + target_doc, + ) return doclist diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index 6e727e53efd..a98cd10e320 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_i # test_records = frappe.get_test_records('Maintenance Schedule') + class TestMaintenanceSchedule(unittest.TestCase): def test_events_should_be_created_and_deleted(self): ms = make_maintenance_schedule() @@ -42,15 +43,15 @@ class TestMaintenanceSchedule(unittest.TestCase): expected_end_date = add_days(i.start_date, i.no_of_visits * 7) self.assertEqual(i.end_date, expected_end_date) - items = ms.get_pending_data(data_type = "items") - items = items.split('\n') + items = ms.get_pending_data(data_type="items") + items = items.split("\n") items.pop(0) - expected_items = ['_Test Item'] + expected_items = ["_Test Item"] self.assertTrue(items, expected_items) # "dates" contains all generated schedule dates - dates = ms.get_pending_data(data_type = "date", item_name = i.item_name) - dates = dates.split('\n') + dates = ms.get_pending_data(data_type="date", item_name=i.item_name) + dates = dates.split("\n") dates.pop(0) expected_dates.append(formatdate(add_days(i.start_date, 7), "dd-MM-yyyy")) expected_dates.append(formatdate(add_days(i.start_date, 14), "dd-MM-yyyy")) @@ -59,33 +60,38 @@ class TestMaintenanceSchedule(unittest.TestCase): self.assertEqual(dates, expected_dates) ms.submit() - s_id = ms.get_pending_data(data_type = "id", item_name = i.item_name, s_date = expected_dates[1]) + s_id = ms.get_pending_data(data_type="id", item_name=i.item_name, s_date=expected_dates[1]) # Check if item is mapped in visit. - test_map_visit = make_maintenance_visit(source_name = ms.name, item_name = "_Test Item", s_id = s_id) + test_map_visit = make_maintenance_visit(source_name=ms.name, item_name="_Test Item", s_id=s_id) self.assertEqual(len(test_map_visit.purposes), 1) self.assertEqual(test_map_visit.purposes[0].item_name, "_Test Item") - visit = frappe.new_doc('Maintenance Visit') + visit = frappe.new_doc("Maintenance Visit") visit = test_map_visit visit.maintenance_schedule = ms.name visit.maintenance_schedule_detail = s_id visit.completion_status = "Partially Completed" - visit.set('purposes', [{ - 'item_code': i.item_code, - 'description': "test", - 'work_done': "test", - 'service_person': "Sales Team", - }]) + visit.set( + "purposes", + [ + { + "item_code": i.item_code, + "description": "test", + "work_done": "test", + "service_person": "Sales Team", + } + ], + ) visit.save() visit.submit() - ms = frappe.get_doc('Maintenance Schedule', ms.name) + ms = frappe.get_doc("Maintenance Schedule", ms.name) - #checks if visit status is back updated in schedule + # checks if visit status is back updated in schedule self.assertTrue(ms.schedules[1].completion_status, "Partially Completed") self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date)) - #checks if visit status is updated on cancel + # checks if visit status is updated on cancel visit.cancel() ms.reload() self.assertTrue(ms.schedules[1].completion_status, "Pending") @@ -117,22 +123,24 @@ class TestMaintenanceSchedule(unittest.TestCase): frappe.db.rollback() + def make_serial_item_with_serial(item_code): serial_item_doc = create_item(item_code, is_stock_item=1) if not serial_item_doc.has_serial_no or not serial_item_doc.serial_no_series: serial_item_doc.has_serial_no = 1 serial_item_doc.serial_no_series = "TEST.###" serial_item_doc.save(ignore_permissions=True) - active_serials = frappe.db.get_all('Serial No', {"status": "Active", "item_code": item_code}) + active_serials = frappe.db.get_all("Serial No", {"status": "Active", "item_code": item_code}) if len(active_serials) < 2: make_serialized_item(item_code=item_code) + def get_events(ms): - return frappe.get_all("Event Participants", filters={ - "reference_doctype": ms.doctype, - "reference_docname": ms.name, - "parenttype": "Event" - }) + return frappe.get_all( + "Event Participants", + filters={"reference_doctype": ms.doctype, "reference_docname": ms.name, "parenttype": "Event"}, + ) + def make_maintenance_schedule(**args): ms = frappe.new_doc("Maintenance Schedule") @@ -140,14 +148,17 @@ def make_maintenance_schedule(**args): ms.customer = "_Test Customer" ms.transaction_date = today() - ms.append("items", { - "item_code": args.get("item_code") or "_Test Item", - "start_date": today(), - "periodicity": "Weekly", - "no_of_visits": 4, - "serial_no": args.get("serial_no"), - "sales_person": "Sales Team", - }) + ms.append( + "items", + { + "item_code": args.get("item_code") or "_Test Item", + "start_date": today(), + "periodicity": "Weekly", + "no_of_visits": 4, + "serial_no": args.get("serial_no"), + "sales_person": "Sales Team", + }, + ) ms.insert(ignore_permissions=True) return ms diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py index 6fe2466be22..29a17849fd9 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py @@ -14,7 +14,7 @@ class MaintenanceVisit(TransactionBase): return _("To {0}").format(self.customer_name) def validate_serial_no(self): - for d in self.get('purposes'): + for d in self.get("purposes"): if d.serial_no and not frappe.db.exists("Serial No", d.serial_no): frappe.throw(_("Serial No {0} does not exist").format(d.serial_no)) @@ -24,13 +24,19 @@ class MaintenanceVisit(TransactionBase): def validate_maintenance_date(self): if self.maintenance_type == "Scheduled" and self.maintenance_schedule_detail: - item_ref = frappe.db.get_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'item_reference') + item_ref = frappe.db.get_value( + "Maintenance Schedule Detail", self.maintenance_schedule_detail, "item_reference" + ) if item_ref: - start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date']) - if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date): - frappe.throw(_("Date must be between {0} and {1}") - .format(format_date(start_date), format_date(end_date))) - + start_date, end_date = frappe.db.get_value( + "Maintenance Schedule Item", item_ref, ["start_date", "end_date"] + ) + if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime( + self.mntc_date + ) > get_datetime(end_date): + frappe.throw( + _("Date must be between {0} and {1}").format(format_date(start_date), format_date(end_date)) + ) def validate(self): self.validate_serial_no() @@ -44,73 +50,87 @@ class MaintenanceVisit(TransactionBase): status = self.completion_status actual_date = self.mntc_date if self.maintenance_schedule_detail: - frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status) - frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date) + frappe.db.set_value( + "Maintenance Schedule Detail", self.maintenance_schedule_detail, "completion_status", status + ) + frappe.db.set_value( + "Maintenance Schedule Detail", self.maintenance_schedule_detail, "actual_date", actual_date + ) def update_customer_issue(self, flag): if not self.maintenance_schedule: - for d in self.get('purposes'): - if d.prevdoc_docname and d.prevdoc_doctype == 'Warranty Claim' : - if flag==1: + for d in self.get("purposes"): + if d.prevdoc_docname and d.prevdoc_doctype == "Warranty Claim": + if flag == 1: mntc_date = self.mntc_date service_person = d.service_person work_done = d.work_done status = "Open" - if self.completion_status == 'Fully Completed': - status = 'Closed' - elif self.completion_status == 'Partially Completed': - status = 'Work In Progress' + if self.completion_status == "Fully Completed": + status = "Closed" + elif self.completion_status == "Partially Completed": + status = "Work In Progress" else: - nm = frappe.db.sql("select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1", (d.prevdoc_docname, self.name)) + nm = frappe.db.sql( + "select t1.name, t1.mntc_date, t2.service_person, t2.work_done from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.completion_status = 'Partially Completed' and t2.prevdoc_docname = %s and t1.name!=%s and t1.docstatus = 1 order by t1.name desc limit 1", + (d.prevdoc_docname, self.name), + ) if nm: - status = 'Work In Progress' - mntc_date = nm and nm[0][1] or '' - service_person = nm and nm[0][2] or '' - work_done = nm and nm[0][3] or '' + status = "Work In Progress" + mntc_date = nm and nm[0][1] or "" + service_person = nm and nm[0][2] or "" + work_done = nm and nm[0][3] or "" else: - status = 'Open' + status = "Open" mntc_date = None service_person = None work_done = None - wc_doc = frappe.get_doc('Warranty Claim', d.prevdoc_docname) - wc_doc.update({ - 'resolution_date': mntc_date, - 'resolved_by': service_person, - 'resolution_details': work_done, - 'status': status - }) + wc_doc = frappe.get_doc("Warranty Claim", d.prevdoc_docname) + wc_doc.update( + { + "resolution_date": mntc_date, + "resolved_by": service_person, + "resolution_details": work_done, + "status": status, + } + ) wc_doc.db_update() def check_if_last_visit(self): """check if last maintenance visit against same sales order/ Warranty Claim""" check_for_docname = None - for d in self.get('purposes'): + for d in self.get("purposes"): if d.prevdoc_docname: check_for_docname = d.prevdoc_docname - #check_for_doctype = d.prevdoc_doctype + # check_for_doctype = d.prevdoc_doctype if check_for_docname: - check = frappe.db.sql("select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.name!=%s and t2.prevdoc_docname=%s and t1.docstatus = 1 and (t1.mntc_date > %s or (t1.mntc_date = %s and t1.mntc_time > %s))", (self.name, check_for_docname, self.mntc_date, self.mntc_date, self.mntc_time)) + check = frappe.db.sql( + "select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t1.name!=%s and t2.prevdoc_docname=%s and t1.docstatus = 1 and (t1.mntc_date > %s or (t1.mntc_date = %s and t1.mntc_time > %s))", + (self.name, check_for_docname, self.mntc_date, self.mntc_date, self.mntc_time), + ) if check: check_lst = [x[0] for x in check] - check_lst =','.join(check_lst) - frappe.throw(_("Cancel Material Visits {0} before cancelling this Maintenance Visit").format(check_lst)) + check_lst = ",".join(check_lst) + frappe.throw( + _("Cancel Material Visits {0} before cancelling this Maintenance Visit").format(check_lst) + ) raise Exception else: self.update_customer_issue(0) def on_submit(self): self.update_customer_issue(1) - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") self.update_status_and_actual_date() def on_cancel(self): self.check_if_last_visit() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") self.update_status_and_actual_date(cancel=True) def on_update(self): diff --git a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py index a0c1a338e49..bdbed7c0965 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py +++ b/erpnext/maintenance/doctype/maintenance_visit/test_maintenance_visit.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Maintenance Visit') + class TestMaintenanceVisit(unittest.TestCase): pass diff --git a/erpnext/manufacturing/dashboard_fixtures.py b/erpnext/manufacturing/dashboard_fixtures.py index 1bc12ff35eb..9e64f4dc196 100644 --- a/erpnext/manufacturing/dashboard_fixtures.py +++ b/erpnext/manufacturing/dashboard_fixtures.py @@ -11,33 +11,39 @@ import erpnext def get_data(): - return frappe._dict({ - "dashboards": get_dashboards(), - "charts": get_charts(), - "number_cards": get_number_cards(), - }) + return frappe._dict( + { + "dashboards": get_dashboards(), + "charts": get_charts(), + "number_cards": get_number_cards(), + } + ) + def get_dashboards(): - return [{ - "name": "Manufacturing", - "dashboard_name": "Manufacturing", - "charts": [ - { "chart": "Produced Quantity", "width": "Half" }, - { "chart": "Completed Operation", "width": "Half" }, - { "chart": "Work Order Analysis", "width": "Half" }, - { "chart": "Quality Inspection Analysis", "width": "Half" }, - { "chart": "Pending Work Order", "width": "Half" }, - { "chart": "Last Month Downtime Analysis", "width": "Half" }, - { "chart": "Work Order Qty Analysis", "width": "Full" }, - { "chart": "Job Card Analysis", "width": "Full" } - ], - "cards": [ - { "card": "Monthly Total Work Order" }, - { "card": "Monthly Completed Work Order" }, - { "card": "Ongoing Job Card" }, - { "card": "Monthly Quality Inspection"} - ] - }] + return [ + { + "name": "Manufacturing", + "dashboard_name": "Manufacturing", + "charts": [ + {"chart": "Produced Quantity", "width": "Half"}, + {"chart": "Completed Operation", "width": "Half"}, + {"chart": "Work Order Analysis", "width": "Half"}, + {"chart": "Quality Inspection Analysis", "width": "Half"}, + {"chart": "Pending Work Order", "width": "Half"}, + {"chart": "Last Month Downtime Analysis", "width": "Half"}, + {"chart": "Work Order Qty Analysis", "width": "Full"}, + {"chart": "Job Card Analysis", "width": "Full"}, + ], + "cards": [ + {"card": "Monthly Total Work Order"}, + {"card": "Monthly Completed Work Order"}, + {"card": "Ongoing Job Card"}, + {"card": "Monthly Quality Inspection"}, + ], + } + ] + def get_charts(): company = erpnext.get_default_company() @@ -45,200 +51,198 @@ def get_charts(): if not company: company = frappe.db.get_value("Company", {"is_group": 0}, "name") - return [{ - "doctype": "Dashboard Chart", - "based_on": "modified", - "chart_type": "Sum", - "chart_name": _("Produced Quantity"), - "name": "Produced Quantity", - "document_type": "Work Order", - "filters_json": json.dumps([['Work Order', 'docstatus', '=', 1, False]]), - "group_by_type": "Count", - "time_interval": "Monthly", - "timespan": "Last Year", - "owner": "Administrator", - "type": "Line", - "value_based_on": "produced_qty", - "is_public": 1, - "timeseries": 1 - }, { - "doctype": "Dashboard Chart", - "based_on": "creation", - "chart_type": "Sum", - "chart_name": _("Completed Operation"), - "name": "Completed Operation", - "document_type": "Work Order Operation", - "filters_json": json.dumps([['Work Order Operation', 'docstatus', '=', 1, False]]), - "group_by_type": "Count", - "time_interval": "Quarterly", - "timespan": "Last Year", - "owner": "Administrator", - "type": "Line", - "value_based_on": "completed_qty", - "is_public": 1, - "timeseries": 1 - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Work Order Analysis"), - "name": "Work Order Analysis", - "timespan": "Last Year", - "report_name": "Work Order Summary", - "owner": "Administrator", - "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}), - "type": "Donut", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "axisOptions": { - "shortenYAxisNumbers": 1 - }, - "height": 300 - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Quality Inspection Analysis"), - "name": "Quality Inspection Analysis", - "timespan": "Last Year", - "report_name": "Quality Inspection Summary", - "owner": "Administrator", - "filters_json": json.dumps({}), - "type": "Donut", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "axisOptions": { - "shortenYAxisNumbers": 1 - }, - "height": 300 - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Pending Work Order"), - "name": "Pending Work Order", - "timespan": "Last Year", - "report_name": "Work Order Summary", - "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}), - "owner": "Administrator", - "type": "Donut", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "axisOptions": { - "shortenYAxisNumbers": 1 - }, - "height": 300 - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Last Month Downtime Analysis"), - "name": "Last Month Downtime Analysis", - "timespan": "Last Year", - "filters_json": json.dumps({}), - "report_name": "Downtime Analysis", - "owner": "Administrator", - "is_public": 1, - "is_custom": 1, - "type": "Bar" - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Work Order Qty Analysis"), - "name": "Work Order Qty Analysis", - "timespan": "Last Year", - "report_name": "Work Order Summary", - "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}), - "owner": "Administrator", - "type": "Bar", - "is_public": 1, - "is_custom": 1, - "custom_options": json.dumps({ - "barOptions": { "stacked": 1 } - }), - }, { - "doctype": "Dashboard Chart", - "time_interval": "Yearly", - "chart_type": "Report", - "chart_name": _("Job Card Analysis"), - "name": "Job Card Analysis", - "timespan": "Last Year", - "report_name": "Job Card Summary", - "owner": "Administrator", - "is_public": 1, - "is_custom": 1, - "filters_json": json.dumps({"company": company, "docstatus": 1, "range":"Monthly"}), - "custom_options": json.dumps({ - "barOptions": { "stacked": 1 } - }), - "type": "Bar" - }] + return [ + { + "doctype": "Dashboard Chart", + "based_on": "modified", + "chart_type": "Sum", + "chart_name": _("Produced Quantity"), + "name": "Produced Quantity", + "document_type": "Work Order", + "filters_json": json.dumps([["Work Order", "docstatus", "=", 1, False]]), + "group_by_type": "Count", + "time_interval": "Monthly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "produced_qty", + "is_public": 1, + "timeseries": 1, + }, + { + "doctype": "Dashboard Chart", + "based_on": "creation", + "chart_type": "Sum", + "chart_name": _("Completed Operation"), + "name": "Completed Operation", + "document_type": "Work Order Operation", + "filters_json": json.dumps([["Work Order Operation", "docstatus", "=", 1, False]]), + "group_by_type": "Count", + "time_interval": "Quarterly", + "timespan": "Last Year", + "owner": "Administrator", + "type": "Line", + "value_based_on": "completed_qty", + "is_public": 1, + "timeseries": 1, + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Analysis"), + "name": "Work Order Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "owner": "Administrator", + "filters_json": json.dumps({"company": company, "charts_based_on": "Status"}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Quality Inspection Analysis"), + "name": "Quality Inspection Analysis", + "timespan": "Last Year", + "report_name": "Quality Inspection Summary", + "owner": "Administrator", + "filters_json": json.dumps({}), + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Pending Work Order"), + "name": "Pending Work Order", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Age"}), + "owner": "Administrator", + "type": "Donut", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Last Month Downtime Analysis"), + "name": "Last Month Downtime Analysis", + "timespan": "Last Year", + "filters_json": json.dumps({}), + "report_name": "Downtime Analysis", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "type": "Bar", + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Work Order Qty Analysis"), + "name": "Work Order Qty Analysis", + "timespan": "Last Year", + "report_name": "Work Order Summary", + "filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}), + "owner": "Administrator", + "type": "Bar", + "is_public": 1, + "is_custom": 1, + "custom_options": json.dumps({"barOptions": {"stacked": 1}}), + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Yearly", + "chart_type": "Report", + "chart_name": _("Job Card Analysis"), + "name": "Job Card Analysis", + "timespan": "Last Year", + "report_name": "Job Card Summary", + "owner": "Administrator", + "is_public": 1, + "is_custom": 1, + "filters_json": json.dumps({"company": company, "docstatus": 1, "range": "Monthly"}), + "custom_options": json.dumps({"barOptions": {"stacked": 1}}), + "type": "Bar", + }, + ] + def get_number_cards(): start_date = add_months(nowdate(), -1) end_date = nowdate() - return [{ - "doctype": "Number Card", - "document_type": "Work Order", - "name": "Monthly Total Work Order", - "filters_json": json.dumps([ - ['Work Order', 'docstatus', '=', 1], - ['Work Order', 'creation', 'between', [start_date, end_date]] - ]), - "function": "Count", - "is_public": 1, - "label": _("Monthly Total Work Orders"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }, - { - "doctype": "Number Card", - "document_type": "Work Order", - "name": "Monthly Completed Work Order", - "filters_json": json.dumps([ - ['Work Order', 'status', '=', 'Completed'], - ['Work Order', 'docstatus', '=', 1], - ['Work Order', 'creation', 'between', [start_date, end_date]] - ]), - "function": "Count", - "is_public": 1, - "label": _("Monthly Completed Work Orders"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }, - { - "doctype": "Number Card", - "document_type": "Job Card", - "name": "Ongoing Job Card", - "filters_json": json.dumps([ - ['Job Card', 'status','!=','Completed'], - ['Job Card', 'docstatus', '=', 1] - ]), - "function": "Count", - "is_public": 1, - "label": _("Ongoing Job Cards"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }, - { - "doctype": "Number Card", - "document_type": "Quality Inspection", - "name": "Monthly Quality Inspection", - "filters_json": json.dumps([ - ['Quality Inspection', 'docstatus', '=', 1], - ['Quality Inspection', 'creation', 'between', [start_date, end_date]] - ]), - "function": "Count", - "is_public": 1, - "label": _("Monthly Quality Inspections"), - "show_percentage_stats": 1, - "stats_time_interval": "Weekly" - }] + return [ + { + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Monthly Total Work Order", + "filters_json": json.dumps( + [ + ["Work Order", "docstatus", "=", 1], + ["Work Order", "creation", "between", [start_date, end_date]], + ] + ), + "function": "Count", + "is_public": 1, + "label": _("Monthly Total Work Orders"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + { + "doctype": "Number Card", + "document_type": "Work Order", + "name": "Monthly Completed Work Order", + "filters_json": json.dumps( + [ + ["Work Order", "status", "=", "Completed"], + ["Work Order", "docstatus", "=", 1], + ["Work Order", "creation", "between", [start_date, end_date]], + ] + ), + "function": "Count", + "is_public": 1, + "label": _("Monthly Completed Work Orders"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + { + "doctype": "Number Card", + "document_type": "Job Card", + "name": "Ongoing Job Card", + "filters_json": json.dumps( + [["Job Card", "status", "!=", "Completed"], ["Job Card", "docstatus", "=", 1]] + ), + "function": "Count", + "is_public": 1, + "label": _("Ongoing Job Cards"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + { + "doctype": "Number Card", + "document_type": "Quality Inspection", + "name": "Monthly Quality Inspection", + "filters_json": json.dumps( + [ + ["Quality Inspection", "docstatus", "=", 1], + ["Quality Inspection", "creation", "between", [start_date, end_date]], + ] + ), + "function": "Count", + "is_public": 1, + "label": _("Monthly Quality Inspections"), + "show_percentage_stats": 1, + "stats_time_interval": "Weekly", + }, + ] diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 5340c51131f..ff2140199de 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -29,7 +29,9 @@ class BlanketOrder(Document): def update_ordered_qty(self): ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order" - item_ordered_qty = frappe._dict(frappe.db.sql(""" + item_ordered_qty = frappe._dict( + frappe.db.sql( + """ select trans_item.item_code, sum(trans_item.stock_qty) as qty from `tab{0} Item` trans_item, `tab{0}` trans where trans.name = trans_item.parent @@ -37,18 +39,24 @@ class BlanketOrder(Document): and trans.docstatus=1 and trans.status not in ('Closed', 'Stopped') group by trans_item.item_code - """.format(ref_doctype), self.name)) + """.format( + ref_doctype + ), + self.name, + ) + ) for d in self.items: d.db_set("ordered_qty", item_ordered_qty.get(d.item_code, 0)) + @frappe.whitelist() def make_order(source_name): doctype = frappe.flags.args.doctype def update_doc(source_doc, target_doc, source_parent): - if doctype == 'Quotation': - target_doc.quotation_to = 'Customer' + if doctype == "Quotation": + target_doc.quotation_to = "Customer" target_doc.party_name = source_doc.customer def update_item(source, target, source_parent): @@ -62,18 +70,16 @@ def make_order(source_name): target.against_blanket_order = 1 target.blanket_order = source_name - target_doc = get_mapped_doc("Blanket Order", source_name, { - "Blanket Order": { - "doctype": doctype, - "postprocess": update_doc - }, - "Blanket Order Item": { - "doctype": doctype + " Item", - "field_map": { - "rate": "blanket_order_rate", - "parent": "blanket_order" + target_doc = get_mapped_doc( + "Blanket Order", + source_name, + { + "Blanket Order": {"doctype": doctype, "postprocess": update_doc}, + "Blanket Order Item": { + "doctype": doctype + " Item", + "field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"}, + "postprocess": update_item, }, - "postprocess": update_item - } - }) + }, + ) return target_doc diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py index 2556f2f163d..31062342d03 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order_dashboard.py @@ -1,11 +1,5 @@ - - def get_data(): return { - 'fieldname': 'blanket_order', - 'transactions': [ - { - 'items': ['Purchase Order', 'Sales Order', 'Quotation'] - } - ] + "fieldname": "blanket_order", + "transactions": [{"items": ["Purchase Order", "Sales Order", "Quotation"]}], } diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index d4d337d8412..2f1f3ae0f52 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -16,7 +16,7 @@ class TestBlanketOrder(FrappeTestCase): def test_sales_order_creation(self): bo = make_blanket_order(blanket_order_type="Selling") - frappe.flags.args.doctype = 'Sales Order' + frappe.flags.args.doctype = "Sales Order" so = make_order(bo.name) so.currency = get_company_currency(so.company) so.delivery_date = today() @@ -33,16 +33,15 @@ class TestBlanketOrder(FrappeTestCase): self.assertEqual(so.items[0].qty, bo.items[0].ordered_qty) # test the quantity - frappe.flags.args.doctype = 'Sales Order' + frappe.flags.args.doctype = "Sales Order" so1 = make_order(bo.name) so1.currency = get_company_currency(so1.company) - self.assertEqual(so1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty)) - + self.assertEqual(so1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) def test_purchase_order_creation(self): bo = make_blanket_order(blanket_order_type="Purchasing") - frappe.flags.args.doctype = 'Purchase Order' + frappe.flags.args.doctype = "Purchase Order" po = make_order(bo.name) po.currency = get_company_currency(po.company) po.schedule_date = today() @@ -59,11 +58,10 @@ class TestBlanketOrder(FrappeTestCase): self.assertEqual(po.items[0].qty, bo.items[0].ordered_qty) # test the quantity - frappe.flags.args.doctype = 'Purchase Order' + frappe.flags.args.doctype = "Purchase Order" po1 = make_order(bo.name) po1.currency = get_company_currency(po1.company) - self.assertEqual(po1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty)) - + self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) def make_blanket_order(**args): @@ -80,11 +78,14 @@ def make_blanket_order(**args): bo.from_date = today() bo.to_date = add_months(bo.from_date, months=12) - bo.append("items", { - "item_code": args.item_code or "_Test Item", - "qty": args.quantity or 1000, - "rate": args.rate or 100 - }) + bo.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "qty": args.quantity or 1000, + "rate": args.rate or 100, + }, + ) bo.insert() bo.submit() diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 5f151c3fe4f..e12cd3131cd 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -19,9 +19,7 @@ from erpnext.setup.utils import get_exchange_rate from erpnext.stock.doctype.item.item import get_item_details from erpnext.stock.get_item_details import get_conversion_factor, get_price_list_rate -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} class BOMTree: @@ -31,10 +29,12 @@ class BOMTree: # ref: https://docs.python.org/3/reference/datamodel.html#slots __slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"] - def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None: + def __init__( + self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 + ) -> None: self.name = name # name of node, BOM number if is_bom else item_code self.child_items: List["BOMTree"] = [] # list of child items - self.is_bom = is_bom # true if the node is a BOM and not a leaf item + self.is_bom = is_bom # true if the node is a BOM and not a leaf item self.item_code: str = None # item_code associated with node self.qty = qty # required unit quantity to make one unit of parent item. self.exploded_qty = exploded_qty # total exploded qty required for making root of tree. @@ -62,12 +62,12 @@ class BOMTree: """Get level order traversal of tree. E.g. for following tree the traversal will return list of nodes in order from top to bottom. BOM: - - SubAssy1 - - item1 - - item2 - - SubAssy2 - - item3 - - item4 + - SubAssy1 + - item1 + - item2 + - SubAssy2 + - item3 + - item4 returns = [SubAssy1, item1, item2, SubAssy2, item3, item4] """ @@ -96,19 +96,18 @@ class BOMTree: rep += child.__repr__(level=level + 1) return rep + class BOM(WebsiteGenerator): website = frappe._dict( # page_title_field = "item_name", - condition_field = "show_in_website", - template = "templates/generators/bom.html" + condition_field="show_in_website", + template="templates/generators/bom.html", ) def autoname(self): # ignore amended documents while calculating current index existing_boms = frappe.get_all( - "BOM", - filters={"item": self.item, "amended_from": ["is", "not set"]}, - pluck="name" + "BOM", filters={"item": self.item, "amended_from": ["is", "not set"]}, pluck="name" ) if existing_boms: @@ -135,11 +134,15 @@ class BOM(WebsiteGenerator): conflicting_bom = frappe.get_doc("BOM", name) if conflicting_bom.item != self.item: - msg = (_("A BOM with name {0} already exists for item {1}.") - .format(frappe.bold(name), frappe.bold(conflicting_bom.item))) + msg = _("A BOM with name {0} already exists for item {1}.").format( + frappe.bold(name), frappe.bold(conflicting_bom.item) + ) - frappe.throw(_("{0}{1} Did you rename the item? Please contact Administrator / Tech support") - .format(msg, "
    ")) + frappe.throw( + _("{0}{1} Did you rename the item? Please contact Administrator / Tech support").format( + msg, "
    " + ) + ) self.name = name @@ -164,7 +167,7 @@ class BOM(WebsiteGenerator): return index def validate(self): - self.route = frappe.scrub(self.name).replace('_', '-') + self.route = frappe.scrub(self.name).replace("_", "-") if not self.company: frappe.throw(_("Please select a Company first."), title=_("Mandatory")) @@ -185,13 +188,13 @@ class BOM(WebsiteGenerator): self.calculate_cost() self.update_stock_qty() self.validate_scrap_items() - self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) + self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) def get_context(self, context): - context.parents = [{'name': 'boms', 'title': _('All BOMs') }] + context.parents = [{"name": "boms", "title": _("All BOMs")}] def on_update(self): - frappe.cache().hdel('bom_children', self.name) + frappe.cache().hdel("bom_children", self.name) self.check_recursion() def on_submit(self): @@ -221,32 +224,47 @@ class BOM(WebsiteGenerator): def get_routing(self): if self.routing: self.set("operations", []) - fields = ["sequence_id", "operation", "workstation", "description", - "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate", - "set_cost_based_on_bom_qty"] + fields = [ + "sequence_id", + "operation", + "workstation", + "description", + "time_in_mins", + "batch_size", + "operating_cost", + "idx", + "hour_rate", + "set_cost_based_on_bom_qty", + ] - for row in frappe.get_all("BOM Operation", fields = fields, - filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"): - child = self.append('operations', row) + for row in frappe.get_all( + "BOM Operation", + fields=fields, + filters={"parenttype": "Routing", "parent": self.routing}, + order_by="sequence_id, idx", + ): + child = self.append("operations", row) child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate")) def set_bom_material_details(self): for item in self.get("items"): self.validate_bom_currency(item) - ret = self.get_bom_material_detail({ - "company": self.company, - "item_code": item.item_code, - "item_name": item.item_name, - "bom_no": item.bom_no, - "stock_qty": item.stock_qty, - "include_item_in_manufacturing": item.include_item_in_manufacturing, - "qty": item.qty, - "uom": item.uom, - "stock_uom": item.stock_uom, - "conversion_factor": item.conversion_factor, - "sourced_by_supplier": item.sourced_by_supplier - }) + ret = self.get_bom_material_detail( + { + "company": self.company, + "item_code": item.item_code, + "item_name": item.item_name, + "bom_no": item.bom_no, + "stock_qty": item.stock_qty, + "include_item_in_manufacturing": item.include_item_in_manufacturing, + "qty": item.qty, + "uom": item.uom, + "stock_uom": item.stock_uom, + "conversion_factor": item.conversion_factor, + "sourced_by_supplier": item.sourced_by_supplier, + } + ) for r in ret: if not item.get(r): item.set(r, ret[r]) @@ -257,7 +275,7 @@ class BOM(WebsiteGenerator): "item_code": item.item_code, "company": self.company, "scrap_items": True, - "bom_no": '', + "bom_no": "", } ret = self.get_bom_material_detail(args) for key, value in ret.items(): @@ -266,72 +284,90 @@ class BOM(WebsiteGenerator): @frappe.whitelist() def get_bom_material_detail(self, args=None): - """ Get raw material details like uom, desc and rate""" + """Get raw material details like uom, desc and rate""" if not args: - args = frappe.form_dict.get('args') + args = frappe.form_dict.get("args") if isinstance(args, str): import json + args = json.loads(args) - item = self.get_item_det(args['item_code']) + item = self.get_item_det(args["item_code"]) - args['bom_no'] = args['bom_no'] or item and cstr(item['default_bom']) or '' - args['transfer_for_manufacture'] = (cstr(args.get('include_item_in_manufacturing', '')) or - item and item.include_item_in_manufacturing or 0) + args["bom_no"] = args["bom_no"] or item and cstr(item["default_bom"]) or "" + args["transfer_for_manufacture"] = ( + cstr(args.get("include_item_in_manufacturing", "")) + or item + and item.include_item_in_manufacturing + or 0 + ) args.update(item) rate = self.get_rm_rate(args) ret_item = { - 'item_name' : item and args['item_name'] or '', - 'description' : item and args['description'] or '', - 'image' : item and args['image'] or '', - 'stock_uom' : item and args['stock_uom'] or '', - 'uom' : item and args['stock_uom'] or '', - 'conversion_factor': 1, - 'bom_no' : args['bom_no'], - 'rate' : rate, - 'qty' : args.get("qty") or args.get("stock_qty") or 1, - 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, - 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), - 'include_item_in_manufacturing': cint(args.get('transfer_for_manufacture')), - 'sourced_by_supplier' : args.get('sourced_by_supplier', 0) + "item_name": item and args["item_name"] or "", + "description": item and args["description"] or "", + "image": item and args["image"] or "", + "stock_uom": item and args["stock_uom"] or "", + "uom": item and args["stock_uom"] or "", + "conversion_factor": 1, + "bom_no": args["bom_no"], + "rate": rate, + "qty": args.get("qty") or args.get("stock_qty") or 1, + "stock_qty": args.get("qty") or args.get("stock_qty") or 1, + "base_rate": flt(rate) * (flt(self.conversion_rate) or 1), + "include_item_in_manufacturing": cint(args.get("transfer_for_manufacture")), + "sourced_by_supplier": args.get("sourced_by_supplier", 0), } return ret_item def validate_bom_currency(self, item): - if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency: - frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}") - .format(item.idx, item.bom_no, self.currency)) + if ( + item.get("bom_no") + and frappe.db.get_value("BOM", item.get("bom_no"), "currency") != self.currency + ): + frappe.throw( + _("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}").format( + item.idx, item.bom_no, self.currency + ) + ) def get_rm_rate(self, arg): - """ Get raw material rate as per selected method, if bom exists takes bom cost """ + """Get raw material rate as per selected method, if bom exists takes bom cost""" rate = 0 if not self.rm_cost_as_per: self.rm_cost_as_per = "Valuation Rate" - if arg.get('scrap_items'): + if arg.get("scrap_items"): rate = get_valuation_rate(arg) elif arg: - #Customer Provided parts and Supplier sourced parts will have zero rate - if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item') and not arg.get('sourced_by_supplier'): - if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom: - rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1) + # Customer Provided parts and Supplier sourced parts will have zero rate + if not frappe.db.get_value( + "Item", arg["item_code"], "is_customer_provided_item" + ) and not arg.get("sourced_by_supplier"): + if arg.get("bom_no") and self.set_rate_of_sub_assembly_item_based_on_bom: + rate = flt(self.get_bom_unitcost(arg["bom_no"])) * (arg.get("conversion_factor") or 1) else: rate = get_bom_item_rate(arg, self) if not rate: if self.rm_cost_as_per == "Price List": - frappe.msgprint(_("Price not found for item {0} in price list {1}") - .format(arg["item_code"], self.buying_price_list), alert=True) + frappe.msgprint( + _("Price not found for item {0} in price list {1}").format( + arg["item_code"], self.buying_price_list + ), + alert=True, + ) else: - frappe.msgprint(_("{0} not found for item {1}") - .format(self.rm_cost_as_per, arg["item_code"]), alert=True) + frappe.msgprint( + _("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]), alert=True + ) return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) @frappe.whitelist() - def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True): + def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate=True, save=True): if self.docstatus == 2: return @@ -341,16 +377,18 @@ class BOM(WebsiteGenerator): if not d.item_code: continue - rate = self.get_rm_rate({ - "company": self.company, - "item_code": d.item_code, - "bom_no": d.bom_no, - "qty": d.qty, - "uom": d.uom, - "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier - }) + rate = self.get_rm_rate( + { + "company": self.company, + "item_code": d.item_code, + "bom_no": d.bom_no, + "qty": d.qty, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "sourced_by_supplier": d.sourced_by_supplier, + } + ) if rate: d.rate = rate @@ -371,8 +409,11 @@ class BOM(WebsiteGenerator): # update parent BOMs if self.total_cost != existing_bom_cost and update_parent: - parent_boms = frappe.db.sql_list("""select distinct parent from `tabBOM Item` - where bom_no = %s and docstatus=1 and parenttype='BOM'""", self.name) + parent_boms = frappe.db.sql_list( + """select distinct parent from `tabBOM Item` + where bom_no = %s and docstatus=1 and parenttype='BOM'""", + self.name, + ) for bom in parent_boms: frappe.get_doc("BOM", bom).update_cost(from_child_bom=True) @@ -384,45 +425,54 @@ class BOM(WebsiteGenerator): if self.total_cost: cost = self.total_cost / self.quantity - frappe.db.sql("""update `tabBOM Item` set rate=%s, amount=stock_qty*%s + frappe.db.sql( + """update `tabBOM Item` set rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""", - (cost, cost, self.name)) + (cost, cost, self.name), + ) def get_bom_unitcost(self, bom_no): - bom = frappe.db.sql("""select name, base_total_cost/quantity as unit_cost from `tabBOM` - where is_active = 1 and name = %s""", bom_no, as_dict=1) - return bom and bom[0]['unit_cost'] or 0 + bom = frappe.db.sql( + """select name, base_total_cost/quantity as unit_cost from `tabBOM` + where is_active = 1 and name = %s""", + bom_no, + as_dict=1, + ) + return bom and bom[0]["unit_cost"] or 0 def manage_default_bom(self): - """ Uncheck others if current one is selected as default or - check the current one as default if it the only bom for the selected item, - update default bom in item master + """Uncheck others if current one is selected as default or + check the current one as default if it the only bom for the selected item, + update default bom in item master """ if self.is_default and self.is_active: from frappe.model.utils import set_default + set_default(self, "item") item = frappe.get_doc("Item", self.item) if item.default_bom != self.name: - frappe.db.set_value('Item', self.item, 'default_bom', self.name) - elif not frappe.db.exists(dict(doctype='BOM', docstatus=1, item=self.item, is_default=1)) \ - and self.is_active: + frappe.db.set_value("Item", self.item, "default_bom", self.name) + elif ( + not frappe.db.exists(dict(doctype="BOM", docstatus=1, item=self.item, is_default=1)) + and self.is_active + ): frappe.db.set(self, "is_default", 1) else: frappe.db.set(self, "is_default", 0) item = frappe.get_doc("Item", self.item) if item.default_bom == self.name: - frappe.db.set_value('Item', self.item, 'default_bom', None) + frappe.db.set_value("Item", self.item, "default_bom", None) def clear_operations(self): if not self.with_operations: - self.set('operations', []) + self.set("operations", []) def clear_inspection(self): if not self.inspection_required: self.quality_inspection_template = None def validate_main_item(self): - """ Validate main FG item""" + """Validate main FG item""" item = self.get_item_det(self.item) if not item: frappe.throw(_("Item {0} does not exist in the system or has expired").format(self.item)) @@ -430,30 +480,34 @@ class BOM(WebsiteGenerator): ret = frappe.db.get_value("Item", self.item, ["description", "stock_uom", "item_name"]) self.description = ret[0] self.uom = ret[1] - self.item_name= ret[2] + self.item_name = ret[2] if not self.quantity: frappe.throw(_("Quantity should be greater than 0")) def validate_currency(self): - if self.rm_cost_as_per == 'Price List': - price_list_currency = frappe.db.get_value('Price List', self.buying_price_list, 'currency') + if self.rm_cost_as_per == "Price List": + price_list_currency = frappe.db.get_value("Price List", self.buying_price_list, "currency") if price_list_currency not in (self.currency, self.company_currency()): - frappe.throw(_("Currency of the price list {0} must be {1} or {2}") - .format(self.buying_price_list, self.currency, self.company_currency())) + frappe.throw( + _("Currency of the price list {0} must be {1} or {2}").format( + self.buying_price_list, self.currency, self.company_currency() + ) + ) def update_stock_qty(self): - for m in self.get('items'): + for m in self.get("items"): if not m.conversion_factor: - m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)['conversion_factor']) + m.conversion_factor = flt(get_conversion_factor(m.item_code, m.uom)["conversion_factor"]) if m.uom and m.qty: - m.stock_qty = flt(m.conversion_factor)*flt(m.qty) + m.stock_qty = flt(m.conversion_factor) * flt(m.qty) if not m.uom and m.stock_uom: m.uom = m.stock_uom m.qty = m.stock_qty def validate_uom_is_interger(self): from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "uom", "qty", "BOM Item") validate_uom_is_integer(self, "stock_uom", "stock_qty", "BOM Item") @@ -461,23 +515,26 @@ class BOM(WebsiteGenerator): if self.currency == self.company_currency(): self.conversion_rate = 1 elif self.conversion_rate == 1 or flt(self.conversion_rate) <= 0: - self.conversion_rate = get_exchange_rate(self.currency, self.company_currency(), args="for_buying") + self.conversion_rate = get_exchange_rate( + self.currency, self.company_currency(), args="for_buying" + ) def set_plc_conversion_rate(self): if self.rm_cost_as_per in ["Valuation Rate", "Last Purchase Rate"]: self.plc_conversion_rate = 1 elif not self.plc_conversion_rate and self.price_list_currency: - self.plc_conversion_rate = get_exchange_rate(self.price_list_currency, - self.company_currency(), args="for_buying") + self.plc_conversion_rate = get_exchange_rate( + self.price_list_currency, self.company_currency(), args="for_buying" + ) def validate_materials(self): - """ Validate raw material entries """ + """Validate raw material entries""" - if not self.get('items'): + if not self.get("items"): frappe.throw(_("Raw Materials cannot be blank.")) check_list = [] - for m in self.get('items'): + for m in self.get("items"): if m.bom_no: validate_bom_no(m.item_code, m.bom_no) if flt(m.qty) <= 0: @@ -485,13 +542,20 @@ class BOM(WebsiteGenerator): check_list.append(m) def check_recursion(self, bom_list=None): - """ Check whether recursion occurs in any bom""" + """Check whether recursion occurs in any bom""" + def _throw_error(bom_name): frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name)) bom_list = self.traverse_tree() - child_items = frappe.get_all('BOM Item', fields=["bom_no", "item_code"], - filters={'parent': ('in', bom_list), 'parenttype': 'BOM'}) or [] + child_items = ( + frappe.get_all( + "BOM Item", + fields=["bom_no", "item_code"], + filters={"parent": ("in", bom_list), "parenttype": "BOM"}, + ) + or [] + ) child_bom = {d.bom_no for d in child_items} child_items_codes = {d.item_code for d in child_items} @@ -502,19 +566,26 @@ class BOM(WebsiteGenerator): if self.item in child_items_codes: _throw_error(self.item) - bom_nos = frappe.get_all('BOM Item', fields=["parent"], - filters={'bom_no': self.name, 'parenttype': 'BOM'}) or [] + bom_nos = ( + frappe.get_all( + "BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"} + ) + or [] + ) if self.name in {d.parent for d in bom_nos}: _throw_error(self.name) def traverse_tree(self, bom_list=None): def _get_children(bom_no): - children = frappe.cache().hget('bom_children', bom_no) + children = frappe.cache().hget("bom_children", bom_no) if children is None: - children = frappe.db.sql_list("""SELECT `bom_no` FROM `tabBOM Item` - WHERE `parent`=%s AND `bom_no`!='' AND `parenttype`='BOM'""", bom_no) - frappe.cache().hset('bom_children', bom_no, children) + children = frappe.db.sql_list( + """SELECT `bom_no` FROM `tabBOM Item` + WHERE `parent`=%s AND `bom_no`!='' AND `parenttype`='BOM'""", + bom_no, + ) + frappe.cache().hset("bom_children", bom_no, children) return children count = 0 @@ -524,7 +595,7 @@ class BOM(WebsiteGenerator): if self.name not in bom_list: bom_list.append(self.name) - while(count < len(bom_list)): + while count < len(bom_list): for child_bom in _get_children(bom_list[count]): if child_bom not in bom_list: bom_list.append(child_bom) @@ -532,19 +603,21 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self, update_hour_rate = False): + def calculate_cost(self, update_hour_rate=False): """Calculate bom totals""" self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost() self.calculate_sm_cost() self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost - self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost + self.base_total_cost = ( + self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost + ) - def calculate_op_cost(self, update_hour_rate = False): + def calculate_op_cost(self, update_hour_rate=False): """Update workstation rate and calculates totals""" self.operating_cost = 0 self.base_operating_cost = 0 - for d in self.get('operations'): + for d in self.get("operations"): if d.workstation: self.update_rate_and_time(d, update_hour_rate) @@ -557,13 +630,14 @@ class BOM(WebsiteGenerator): self.operating_cost += flt(operating_cost) self.base_operating_cost += flt(base_operating_cost) - def update_rate_and_time(self, row, update_hour_rate = False): + def update_rate_and_time(self, row, update_hour_rate=False): if not row.hour_rate or update_hour_rate: hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) if hour_rate: - row.hour_rate = (hour_rate / flt(self.conversion_rate) - if self.conversion_rate and hour_rate else hour_rate) + row.hour_rate = ( + hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate + ) if row.hour_rate and row.time_in_mins: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) @@ -580,12 +654,13 @@ class BOM(WebsiteGenerator): total_rm_cost = 0 base_total_rm_cost = 0 - for d in self.get('items'): + for d in self.get("items"): d.base_rate = flt(d.rate) * flt(self.conversion_rate) d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) d.base_amount = d.amount * flt(self.conversion_rate) - d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) \ - / flt(self.quantity, self.precision("quantity")) + d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt( + self.quantity, self.precision("quantity") + ) total_rm_cost += d.amount base_total_rm_cost += d.base_amount @@ -598,10 +673,14 @@ class BOM(WebsiteGenerator): total_sm_cost = 0 base_total_sm_cost = 0 - for d in self.get('scrap_items'): - d.base_rate = flt(d.rate, d.precision("rate")) * flt(self.conversion_rate, self.precision("conversion_rate")) + for d in self.get("scrap_items"): + d.base_rate = flt(d.rate, d.precision("rate")) * flt( + self.conversion_rate, self.precision("conversion_rate") + ) d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")) - d.base_amount = flt(d.amount, d.precision("amount")) * flt(self.conversion_rate, self.precision("conversion_rate")) + d.base_amount = flt(d.amount, d.precision("amount")) * flt( + self.conversion_rate, self.precision("conversion_rate") + ) total_sm_cost += d.amount base_total_sm_cost += d.base_amount @@ -610,37 +689,42 @@ class BOM(WebsiteGenerator): def update_new_bom(self, old_bom, new_bom, rate): for d in self.get("items"): - if d.bom_no != old_bom: continue + if d.bom_no != old_bom: + continue d.bom_no = new_bom d.rate = rate d.amount = (d.stock_qty or d.qty) * rate def update_exploded_items(self, save=True): - """ Update Flat BOM, following will be correct data""" + """Update Flat BOM, following will be correct data""" self.get_exploded_items() self.add_exploded_items(save=save) def get_exploded_items(self): - """ Get all raw materials including items from child bom""" + """Get all raw materials including items from child bom""" self.cur_exploded_items = {} - for d in self.get('items'): + for d in self.get("items"): if d.bom_no: self.get_child_exploded_items(d.bom_no, d.stock_qty) elif d.item_code: - self.add_to_cur_exploded_items(frappe._dict({ - 'item_code' : d.item_code, - 'item_name' : d.item_name, - 'operation' : d.operation, - 'source_warehouse': d.source_warehouse, - 'description' : d.description, - 'image' : d.image, - 'stock_uom' : d.stock_uom, - 'stock_qty' : flt(d.stock_qty), - 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), - 'include_item_in_manufacturing': d.include_item_in_manufacturing, - 'sourced_by_supplier': d.sourced_by_supplier - })) + self.add_to_cur_exploded_items( + frappe._dict( + { + "item_code": d.item_code, + "item_name": d.item_name, + "operation": d.operation, + "source_warehouse": d.source_warehouse, + "description": d.description, + "image": d.image, + "stock_uom": d.stock_uom, + "stock_qty": flt(d.stock_qty), + "rate": flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), + "include_item_in_manufacturing": d.include_item_in_manufacturing, + "sourced_by_supplier": d.sourced_by_supplier, + } + ) + ) def company_currency(self): return erpnext.get_company_currency(self.company) @@ -652,9 +736,10 @@ class BOM(WebsiteGenerator): self.cur_exploded_items[args.item_code] = args def get_child_exploded_items(self, bom_no, stock_qty): - """ Add all items from Flat BOM of child BOM""" + """Add all items from Flat BOM of child BOM""" # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss - child_fb_items = frappe.db.sql(""" + child_fb_items = frappe.db.sql( + """ SELECT bom_item.item_code, bom_item.item_name, @@ -672,31 +757,38 @@ class BOM(WebsiteGenerator): bom_item.parent = bom.name AND bom.name = %s AND bom.docstatus = 1 - """, bom_no, as_dict = 1) + """, + bom_no, + as_dict=1, + ) for d in child_fb_items: - self.add_to_cur_exploded_items(frappe._dict({ - 'item_code' : d['item_code'], - 'item_name' : d['item_name'], - 'source_warehouse' : d['source_warehouse'], - 'operation' : d['operation'], - 'description' : d['description'], - 'stock_uom' : d['stock_uom'], - 'stock_qty' : d['qty_consumed_per_unit'] * stock_qty, - 'rate' : flt(d['rate']), - 'include_item_in_manufacturing': d.get('include_item_in_manufacturing', 0), - 'sourced_by_supplier': d.get('sourced_by_supplier', 0) - })) + self.add_to_cur_exploded_items( + frappe._dict( + { + "item_code": d["item_code"], + "item_name": d["item_name"], + "source_warehouse": d["source_warehouse"], + "operation": d["operation"], + "description": d["description"], + "stock_uom": d["stock_uom"], + "stock_qty": d["qty_consumed_per_unit"] * stock_qty, + "rate": flt(d["rate"]), + "include_item_in_manufacturing": d.get("include_item_in_manufacturing", 0), + "sourced_by_supplier": d.get("sourced_by_supplier", 0), + } + ) + ) def add_exploded_items(self, save=True): "Add items to Flat BOM table" - self.set('exploded_items', []) + self.set("exploded_items", []) if save: frappe.db.sql("""delete from `tabBOM Explosion Item` where parent=%s""", self.name) for d in sorted(self.cur_exploded_items, key=itemgetter(0)): - ch = self.append('exploded_items', {}) + ch = self.append("exploded_items", {}) for i in self.cur_exploded_items[d].keys(): ch.set(i, self.cur_exploded_items[d][i]) ch.amount = flt(ch.stock_qty) * flt(ch.rate) @@ -708,10 +800,13 @@ class BOM(WebsiteGenerator): def validate_bom_links(self): if not self.is_active: - act_pbom = frappe.db.sql("""select distinct bom_item.parent from `tabBOM Item` bom_item + act_pbom = frappe.db.sql( + """select distinct bom_item.parent from `tabBOM Item` bom_item where bom_item.bom_no = %s and bom_item.docstatus = 1 and bom_item.parenttype='BOM' and exists (select * from `tabBOM` where name = bom_item.parent - and docstatus = 1 and is_active = 1)""", self.name) + and docstatus = 1 and is_active = 1)""", + self.name, + ) if act_pbom and act_pbom[0][0]: frappe.throw(_("Cannot deactivate or cancel BOM as it is linked with other BOMs")) @@ -720,20 +815,23 @@ class BOM(WebsiteGenerator): if not self.with_operations: self.transfer_material_against = "Work Order" if not self.transfer_material_against and not self.is_new(): - frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value")) + frappe.throw( + _("Setting {} is required").format(self.meta.get_label("transfer_material_against")), + title=_("Missing value"), + ) def set_routing_operations(self): if self.routing and self.with_operations and not self.operations: self.get_routing() def validate_operations(self): - if self.with_operations and not self.get('operations') and self.docstatus == 1: + if self.with_operations and not self.get("operations") and self.docstatus == 1: frappe.throw(_("Operations cannot be left blank")) if self.with_operations: for d in self.operations: if not d.description: - d.description = frappe.db.get_value('Operation', d.operation, 'description') + d.description = frappe.db.get_value("Operation", d.operation, "description") if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 @@ -741,24 +839,29 @@ class BOM(WebsiteGenerator): for item in self.scrap_items: msg = "" if item.item_code == self.item and not item.is_process_loss: - msg = _('Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked.') \ - .format(frappe.bold(item.item_code)) + msg = _( + "Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked." + ).format(frappe.bold(item.item_code)) elif item.item_code != self.item and item.is_process_loss: - msg = _('Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked') \ - .format(frappe.bold(item.item_code)) + msg = _( + "Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked" + ).format(frappe.bold(item.item_code)) must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") if item.is_process_loss and must_be_whole_number: - msg = _("Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM.") \ - .format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) + msg = _( + "Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM." + ).format(frappe.bold(item.item_code), frappe.bold(item.stock_uom)) if item.is_process_loss and (item.stock_qty >= self.quantity): - msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.") \ - .format(frappe.bold(item.item_code)) + msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format( + frappe.bold(item.item_code) + ) if item.is_process_loss and (item.rate > 0): - msg = _("Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked.") \ - .format(frappe.bold(item.item_code)) + msg = _( + "Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked." + ).format(frappe.bold(item.item_code)) if msg: frappe.throw(msg, title=_("Note")) @@ -767,42 +870,48 @@ class BOM(WebsiteGenerator): """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) + def get_bom_item_rate(args, bom_doc): - if bom_doc.rm_cost_as_per == 'Valuation Rate': + if bom_doc.rm_cost_as_per == "Valuation Rate": rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) - elif bom_doc.rm_cost_as_per == 'Last Purchase Rate': - rate = (flt(args.get('last_purchase_rate')) - or flt(frappe.db.get_value("Item", args['item_code'], "last_purchase_rate"))) \ - * (args.get("conversion_factor") or 1) + elif bom_doc.rm_cost_as_per == "Last Purchase Rate": + rate = ( + flt(args.get("last_purchase_rate")) + or flt(frappe.db.get_value("Item", args["item_code"], "last_purchase_rate")) + ) * (args.get("conversion_factor") or 1) elif bom_doc.rm_cost_as_per == "Price List": if not bom_doc.buying_price_list: frappe.throw(_("Please select Price List")) - bom_args = frappe._dict({ - "doctype": "BOM", - "price_list": bom_doc.buying_price_list, - "qty": args.get("qty") or 1, - "uom": args.get("uom") or args.get("stock_uom"), - "stock_uom": args.get("stock_uom"), - "transaction_type": "buying", - "company": bom_doc.company, - "currency": bom_doc.currency, - "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function - "conversion_factor": args.get("conversion_factor") or 1, - "plc_conversion_rate": 1, - "ignore_party": True, - "ignore_conversion_rate": True - }) + bom_args = frappe._dict( + { + "doctype": "BOM", + "price_list": bom_doc.buying_price_list, + "qty": args.get("qty") or 1, + "uom": args.get("uom") or args.get("stock_uom"), + "stock_uom": args.get("stock_uom"), + "transaction_type": "buying", + "company": bom_doc.company, + "currency": bom_doc.currency, + "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function + "conversion_factor": args.get("conversion_factor") or 1, + "plc_conversion_rate": 1, + "ignore_party": True, + "ignore_conversion_rate": True, + } + ) item_doc = frappe.get_cached_doc("Item", args.get("item_code")) price_list_data = get_price_list_rate(bom_args, item_doc) rate = price_list_data.price_list_rate return flt(rate) + def get_valuation_rate(args): - """ Get weighted average of valuation rate from all warehouses """ + """Get weighted average of valuation rate from all warehouses""" total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 - item_bins = frappe.db.sql(""" + item_bins = frappe.db.sql( + """ select bin.actual_qty, bin.stock_value from @@ -811,33 +920,48 @@ def get_valuation_rate(args): bin.item_code=%(item)s and bin.warehouse = warehouse.name and warehouse.company=%(company)s""", - {"item": args['item_code'], "company": args['company']}, as_dict=1) + {"item": args["item_code"], "company": args["company"]}, + as_dict=1, + ) for d in item_bins: total_qty += flt(d.actual_qty) total_value += flt(d.stock_value) if total_qty: - valuation_rate = total_value / total_qty + valuation_rate = total_value / total_qty if valuation_rate <= 0: - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` where item_code = %s and valuation_rate > 0 and is_cancelled = 0 - order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) + order by posting_date desc, posting_time desc, creation desc limit 1""", + args["item_code"], + ) valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 if not valuation_rate: - valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate") + valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate") return flt(valuation_rate) + def get_list_context(context): context.title = _("Bill of Materials") # context.introduction = _('Boms') -def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_items=0, include_non_stock_items=False, fetch_qty_in_stock_uom=True): + +def get_bom_items_as_dict( + bom, + company, + qty=1, + fetch_exploded=1, + fetch_scrap_items=0, + include_non_stock_items=False, + fetch_qty_in_stock_uom=True, +): item_dict = {} # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss @@ -874,30 +998,40 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite is_stock_item = 0 if include_non_stock_items else 1 if cint(fetch_exploded): - query = query.format(table="BOM Explosion Item", + query = query.format( + table="BOM Explosion Item", where_conditions="", is_stock_item=is_stock_item, qty_field="stock_qty", - select_columns = """, bom_item.source_warehouse, bom_item.operation, + select_columns=""", bom_item.source_warehouse, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier, - (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""") - - items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) - elif fetch_scrap_items: - query = query.format( - table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description, is_process_loss", - is_stock_item=is_stock_item, qty_field="stock_qty" + (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""", ) - items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) + items = frappe.db.sql( + query, {"parent": bom, "qty": qty, "bom": bom, "company": company}, as_dict=True + ) + elif fetch_scrap_items: + query = query.format( + table="BOM Scrap Item", + where_conditions="", + select_columns=", bom_item.idx, item.description, is_process_loss", + is_stock_item=is_stock_item, + qty_field="stock_qty", + ) + + items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True) else: - query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item, + query = query.format( + table="BOM Item", + where_conditions="", + is_stock_item=is_stock_item, qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", - select_columns = """, bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, + select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, - bom_item.description, bom_item.base_rate as rate """) - items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) + bom_item.description, bom_item.base_rate as rate """, + ) + items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True) for item in items: if item.item_code in item_dict: @@ -906,21 +1040,28 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item_dict[item.item_code] = item for item, item_details in item_dict.items(): - for d in [["Account", "expense_account", "stock_adjustment_account"], - ["Cost Center", "cost_center", "cost_center"], ["Warehouse", "default_warehouse", ""]]: - company_in_record = frappe.db.get_value(d[0], item_details.get(d[1]), "company") - if not item_details.get(d[1]) or (company_in_record and company != company_in_record): - item_dict[item][d[1]] = frappe.get_cached_value('Company', company, d[2]) if d[2] else None + for d in [ + ["Account", "expense_account", "stock_adjustment_account"], + ["Cost Center", "cost_center", "cost_center"], + ["Warehouse", "default_warehouse", ""], + ]: + company_in_record = frappe.db.get_value(d[0], item_details.get(d[1]), "company") + if not item_details.get(d[1]) or (company_in_record and company != company_in_record): + item_dict[item][d[1]] = frappe.get_cached_value("Company", company, d[2]) if d[2] else None return item_dict + @frappe.whitelist() def get_bom_items(bom, company, qty=1, fetch_exploded=1): - items = get_bom_items_as_dict(bom, company, qty, fetch_exploded, include_non_stock_items=True).values() + items = get_bom_items_as_dict( + bom, company, qty, fetch_exploded, include_non_stock_items=True + ).values() items = list(items) - items.sort(key = functools.cmp_to_key(lambda a, b: a.item_code > b.item_code and 1 or -1)) + items.sort(key=functools.cmp_to_key(lambda a, b: a.item_code > b.item_code and 1 or -1)) return items + def validate_bom_no(item, bom_no): """Validate BOM No of sub-contracted items""" bom = frappe.get_doc("BOM", bom_no) @@ -932,21 +1073,24 @@ def validate_bom_no(item, bom_no): if item: rm_item_exists = False for d in bom.items: - if (d.item_code.lower() == item.lower()): + if d.item_code.lower() == item.lower(): rm_item_exists = True for d in bom.scrap_items: - if (d.item_code.lower() == item.lower()): + if d.item_code.lower() == item.lower(): rm_item_exists = True - if bom.item.lower() == item.lower() or \ - bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower(): - rm_item_exists = True + if ( + bom.item.lower() == item.lower() + or bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower() + ): + rm_item_exists = True if not rm_item_exists: frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) + @frappe.whitelist() def get_children(doctype, parent=None, is_root=False, **filters): - if not parent or parent=="BOM": - frappe.msgprint(_('Please select a BOM')) + if not parent or parent == "BOM": + frappe.msgprint(_("Please select a BOM")) return if parent: @@ -956,38 +1100,45 @@ def get_children(doctype, parent=None, is_root=False, **filters): bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent) frappe.has_permission("BOM", doc=bom_doc, throw=True) - bom_items = frappe.get_all('BOM Item', - fields=['item_code', 'bom_no as value', 'stock_qty'], - filters=[['parent', '=', frappe.form_dict.parent]], - order_by='idx') + bom_items = frappe.get_all( + "BOM Item", + fields=["item_code", "bom_no as value", "stock_qty"], + filters=[["parent", "=", frappe.form_dict.parent]], + order_by="idx", + ) - item_names = tuple(d.get('item_code') for d in bom_items) + item_names = tuple(d.get("item_code") for d in bom_items) - items = frappe.get_list('Item', - fields=['image', 'description', 'name', 'stock_uom', 'item_name', 'is_sub_contracted_item'], - filters=[['name', 'in', item_names]]) # to get only required item dicts + items = frappe.get_list( + "Item", + fields=["image", "description", "name", "stock_uom", "item_name", "is_sub_contracted_item"], + filters=[["name", "in", item_names]], + ) # to get only required item dicts for bom_item in bom_items: # extend bom_item dict with respective item dict bom_item.update( # returns an item dict from items list which matches with item_code - next(item for item in items if item.get('name') - == bom_item.get('item_code')) + next(item for item in items if item.get("name") == bom_item.get("item_code")) ) bom_item.parent_bom_qty = bom_doc.quantity - bom_item.expandable = 0 if bom_item.value in ('', None) else 1 + bom_item.expandable = 0 if bom_item.value in ("", None) else 1 bom_item.image = frappe.db.escape(bom_item.image) return bom_items + def get_boms_in_bottom_up_order(bom_no=None): def _get_parent(bom_no): - return frappe.db.sql_list(""" + return frappe.db.sql_list( + """ select distinct bom_item.parent from `tabBOM Item` bom_item where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM' and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1) - """, bom_no) + """, + bom_no, + ) count = 0 bom_list = [] @@ -995,12 +1146,14 @@ def get_boms_in_bottom_up_order(bom_no=None): bom_list.append(bom_no) else: # get all leaf BOMs - bom_list = frappe.db.sql_list("""select name from `tabBOM` bom + bom_list = frappe.db.sql_list( + """select name from `tabBOM` bom where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and ifnull(bom_no, '')!='')""") + where parent=bom.name and ifnull(bom_no, '')!='')""" + ) - while(count < len(bom_list)): + while count < len(bom_list): for child_bom in _get_parent(bom_list[count]): if child_bom not in bom_list: bom_list.append(child_bom) @@ -1008,69 +1161,92 @@ def get_boms_in_bottom_up_order(bom_no=None): return bom_list + def add_additional_cost(stock_entry, work_order): # Add non stock items cost in the additional cost stock_entry.additional_costs = [] - expenses_included_in_valuation = frappe.get_cached_value("Company", work_order.company, - "expenses_included_in_valuation") + expenses_included_in_valuation = frappe.get_cached_value( + "Company", work_order.company, "expenses_included_in_valuation" + ) add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation) add_operations_cost(stock_entry, work_order, expenses_included_in_valuation) + def add_non_stock_items_cost(stock_entry, work_order, expense_account): - bom = frappe.get_doc('BOM', work_order.bom_no) - table = 'exploded_items' if work_order.get('use_multi_level_bom') else 'items' + bom = frappe.get_doc("BOM", work_order.bom_no) + table = "exploded_items" if work_order.get("use_multi_level_bom") else "items" items = {} for d in bom.get(table): items.setdefault(d.item_code, d.amount) - non_stock_items = frappe.get_all('Item', - fields="name", filters={'name': ('in', list(items.keys())), 'ifnull(is_stock_item, 0)': 0}, as_list=1) + non_stock_items = frappe.get_all( + "Item", + fields="name", + filters={"name": ("in", list(items.keys())), "ifnull(is_stock_item, 0)": 0}, + as_list=1, + ) non_stock_items_cost = 0.0 for name in non_stock_items: - non_stock_items_cost += flt(items.get(name[0])) * flt(stock_entry.fg_completed_qty) / flt(bom.quantity) + non_stock_items_cost += ( + flt(items.get(name[0])) * flt(stock_entry.fg_completed_qty) / flt(bom.quantity) + ) if non_stock_items_cost: - stock_entry.append('additional_costs', { - 'expense_account': expense_account, - 'description': _("Non stock items"), - 'amount': non_stock_items_cost - }) + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": _("Non stock items"), + "amount": non_stock_items_cost, + }, + ) + def add_operations_cost(stock_entry, work_order=None, expense_account=None): from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit + operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no) if operating_cost_per_unit: - stock_entry.append('additional_costs', { - "expense_account": expense_account, - "description": _("Operating Cost as per Work Order / BOM"), - "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty) - }) + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": _("Operating Cost as per Work Order / BOM"), + "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty), + }, + ) if work_order and work_order.additional_operating_cost and work_order.qty: - additional_operating_cost_per_unit = \ - flt(work_order.additional_operating_cost) / flt(work_order.qty) + additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt( + work_order.qty + ) if additional_operating_cost_per_unit: - stock_entry.append('additional_costs', { - "expense_account": expense_account, - "description": "Additional Operating Cost", - "amount": additional_operating_cost_per_unit * flt(stock_entry.fg_completed_qty) - }) + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": "Additional Operating Cost", + "amount": additional_operating_cost_per_unit * flt(stock_entry.fg_completed_qty), + }, + ) + @frappe.whitelist() def get_bom_diff(bom1, bom2): from frappe.model import table_fields if bom1 == bom2: - frappe.throw(_("BOM 1 {0} and BOM 2 {1} should not be same") - .format(frappe.bold(bom1), frappe.bold(bom2))) + frappe.throw( + _("BOM 1 {0} and BOM 2 {1} should not be same").format(frappe.bold(bom1), frappe.bold(bom2)) + ) - doc1 = frappe.get_doc('BOM', bom1) - doc2 = frappe.get_doc('BOM', bom2) + doc1 = frappe.get_doc("BOM", bom1) + doc2 = frappe.get_doc("BOM", bom2) out = get_diff(doc1, doc2) out.row_changed = [] @@ -1080,10 +1256,10 @@ def get_bom_diff(bom1, bom2): meta = doc1.meta identifiers = { - 'operations': 'operation', - 'items': 'item_code', - 'scrap_items': 'item_code', - 'exploded_items': 'item_code' + "operations": "operation", + "items": "item_code", + "scrap_items": "item_code", + "exploded_items": "item_code", } for df in meta.fields: @@ -1114,6 +1290,7 @@ def get_bom_diff(bom1, bom2): return out + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): @@ -1123,25 +1300,28 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): order_by = "idx desc, name, item_name" fields = ["name", "item_group", "item_name", "description"] - fields.extend([field for field in searchfields - if not field in ["name", "item_group", "description"]]) + fields.extend( + [field for field in searchfields if not field in ["name", "item_group", "description"]] + ) - searchfields = searchfields + [field for field in [searchfield or "name", "item_code", "item_group", "item_name"] - if not field in searchfields] + searchfields = searchfields + [ + field + for field in [searchfield or "name", "item_code", "item_group", "item_name"] + if not field in searchfields + ] - query_filters = { - "disabled": 0, - "ifnull(end_of_life, '5050-50-50')": (">", today()) - } + query_filters = {"disabled": 0, "ifnull(end_of_life, '5050-50-50')": (">", today())} or_cond_filters = {} if txt: for s_field in searchfields: or_cond_filters[s_field] = ("like", "%{0}%".format(txt)) - barcodes = frappe.get_all("Item Barcode", + barcodes = frappe.get_all( + "Item Barcode", fields=["distinct parent as item_code"], - filters = {"barcode": ("like", "%{0}%".format(txt))}) + filters={"barcode": ("like", "%{0}%".format(txt))}, + ) barcodes = [d.item_code for d in barcodes] if barcodes: @@ -1155,10 +1335,17 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if filters and filters.get("is_stock_item"): query_filters["is_stock_item"] = 1 - return frappe.get_list("Item", - fields = fields, filters=query_filters, - or_filters = or_cond_filters, order_by=order_by, - limit_start=start, limit_page_length=page_len, as_list=1) + return frappe.get_list( + "Item", + fields=fields, + filters=query_filters, + or_filters=or_cond_filters, + order_by=order_by, + limit_start=start, + limit_page_length=page_len, + as_list=1, + ) + @frappe.whitelist() def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): @@ -1169,28 +1356,31 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): doc.quantity = 1 item_data = get_item_details(item) - doc.update({ - "item_name": item_data.item_name, - "description": item_data.description, - "uom": item_data.stock_uom, - "allow_alternative_item": item_data.allow_alternative_item - }) + doc.update( + { + "item_name": item_data.item_name, + "description": item_data.description, + "uom": item_data.stock_uom, + "allow_alternative_item": item_data.allow_alternative_item, + } + ) add_variant_item(variant_items, doc, source_name) - doc = get_mapped_doc('BOM', source_name, { - 'BOM': { - 'doctype': 'BOM', - 'validation': { - 'docstatus': ['=', 1] - } + doc = get_mapped_doc( + "BOM", + source_name, + { + "BOM": {"doctype": "BOM", "validation": {"docstatus": ["=", 1]}}, + "BOM Item": { + "doctype": "BOM Item", + # stop get_mapped_doc copying parent bom_no to children + "field_no_map": ["bom_no"], + "condition": lambda doc: doc.has_variants == 0, + }, }, - 'BOM Item': { - 'doctype': 'BOM Item', - # stop get_mapped_doc copying parent bom_no to children - 'field_no_map': ['bom_no'], - 'condition': lambda doc: doc.has_variants == 0 - }, - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doc diff --git a/erpnext/manufacturing/doctype/bom/bom_dashboard.py b/erpnext/manufacturing/doctype/bom/bom_dashboard.py index 9b8f6bff095..d8a810bd0ad 100644 --- a/erpnext/manufacturing/doctype/bom/bom_dashboard.py +++ b/erpnext/manufacturing/doctype/bom/bom_dashboard.py @@ -1,30 +1,30 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'bom_no', - 'non_standard_fieldnames': { - 'Item': 'default_bom', - 'Purchase Order': 'bom', - 'Purchase Receipt': 'bom', - 'Purchase Invoice': 'bom' + "fieldname": "bom_no", + "non_standard_fieldnames": { + "Item": "default_bom", + "Purchase Order": "bom", + "Purchase Receipt": "bom", + "Purchase Invoice": "bom", }, - 'transactions': [ + "transactions": [ + {"label": _("Stock"), "items": ["Item", "Stock Entry", "Quality Inspection"]}, + {"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]}, { - 'label': _('Stock'), - 'items': ['Item', 'Stock Entry', 'Quality Inspection'] + "label": _("Subcontract"), + "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"], }, - { - 'label': _('Manufacture'), - 'items': ['BOM', 'Work Order', 'Job Card'] - }, - { - 'label': _('Subcontract'), - 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - } ], - 'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt", - "Purchase Invoice", "Job Card", "Stock Entry", "BOM"] + "disable_create_buttons": [ + "Item", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + "Job Card", + "Stock Entry", + "BOM", + ], } diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index c4056191428..455e3f9d9c3 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -18,22 +18,27 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ) from erpnext.tests.test_subcontracting import set_backflush_based_on -test_records = frappe.get_test_records('BOM') +test_records = frappe.get_test_records("BOM") test_dependencies = ["Item", "Quality Inspection Template"] + class TestBOM(FrappeTestCase): def test_get_items(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict - items_dict = get_bom_items_as_dict(bom=get_default_bom(), - company="_Test Company", qty=1, fetch_exploded=0) + + items_dict = get_bom_items_as_dict( + bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=0 + ) self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict) self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict) self.assertEqual(len(items_dict.values()), 2) def test_get_items_exploded(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict - items_dict = get_bom_items_as_dict(bom=get_default_bom(), - company="_Test Company", qty=1, fetch_exploded=1) + + items_dict = get_bom_items_as_dict( + bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=1 + ) self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict) self.assertFalse(test_records[2]["items"][1]["item_code"] in items_dict) self.assertTrue(test_records[0]["items"][0]["item_code"] in items_dict) @@ -42,13 +47,14 @@ class TestBOM(FrappeTestCase): def test_get_items_list(self): from erpnext.manufacturing.doctype.bom.bom import get_bom_items + self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3) def test_default_bom(self): def _get_default_bom_in_item(): return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom")) - bom = frappe.get_doc("BOM", {"item":"_Test FG Item 2", "is_default": 1}) + bom = frappe.get_doc("BOM", {"item": "_Test FG Item 2", "is_default": 1}) self.assertEqual(_get_default_bom_in_item(), bom.name) bom.is_active = 0 @@ -56,28 +62,33 @@ class TestBOM(FrappeTestCase): self.assertEqual(_get_default_bom_in_item(), "") bom.is_active = 1 - bom.is_default=1 + bom.is_default = 1 bom.save() self.assertTrue(_get_default_bom_in_item(), bom.name) def test_update_bom_cost_in_all_boms(self): # get current rate for '_Test Item 2' - rm_rate = frappe.db.sql("""select rate from `tabBOM Item` + rm_rate = frappe.db.sql( + """select rate from `tabBOM Item` where parent='BOM-_Test Item Home Desktop Manufactured-001' - and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""") + and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""" + ) rm_rate = rm_rate[0][0] if rm_rate else 0 # Reset item valuation rate - reset_item_valuation_rate(item_code='_Test Item 2', qty=200, rate=rm_rate + 10) + reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10) # update cost of all BOMs based on latest valuation rate update_cost() # check if new valuation rate updated in all BOMs - for d in frappe.db.sql("""select rate from `tabBOM Item` - where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", as_dict=1): - self.assertEqual(d.rate, rm_rate + 10) + for d in frappe.db.sql( + """select rate from `tabBOM Item` + where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", + as_dict=1, + ): + self.assertEqual(d.rate, rm_rate + 10) def test_bom_cost(self): bom = frappe.copy_doc(test_records[2]) @@ -92,7 +103,9 @@ class TestBOM(FrappeTestCase): for row in bom.items: raw_material_cost += row.amount - base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) + base_raw_material_cost = raw_material_cost * flt( + bom.conversion_rate, bom.precision("conversion_rate") + ) base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) # test amounts in selected currency, almostEqual checks for 7 digits by default @@ -120,14 +133,15 @@ class TestBOM(FrappeTestCase): for op_row in bom.operations: self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2) - self.assertAlmostEqual(bom.operating_cost, op_cost/2) + self.assertAlmostEqual(bom.operating_cost, op_cost / 2) bom.delete() def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)): - frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", - item_code) + frappe.db.sql( + "delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", item_code + ) item_price = frappe.new_doc("Item Price") item_price.price_list = "_Test Price List" item_price.item_code = item_code @@ -142,7 +156,7 @@ class TestBOM(FrappeTestCase): bom.items[0].conversion_factor = 5 bom.insert() - bom.update_cost(update_hour_rate = False) + bom.update_cost(update_hour_rate=False) # test amounts in selected currency self.assertEqual(bom.items[0].rate, 300) @@ -167,11 +181,12 @@ class TestBOM(FrappeTestCase): bom.insert() reset_item_valuation_rate( - item_code='_Test Item', - warehouse_list=frappe.get_all("Warehouse", - {"is_group":0, "company": bom.company}, pluck="name"), + item_code="_Test Item", + warehouse_list=frappe.get_all( + "Warehouse", {"is_group": 0, "company": bom.company}, pluck="name" + ), qty=200, - rate=200 + rate=200, ) bom.update_cost() @@ -180,77 +195,72 @@ class TestBOM(FrappeTestCase): def test_subcontractor_sourced_item(self): item_code = "_Test Subcontracted FG Item 1" - set_backflush_based_on('Material Transferred for Subcontract') + set_backflush_based_on("Material Transferred for Subcontract") - if not frappe.db.exists('Item', item_code): - make_item(item_code, { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'stock_uom': 'Nos' - }) + if not frappe.db.exists("Item", item_code): + make_item(item_code, {"is_stock_item": 1, "is_sub_contracted_item": 1, "stock_uom": "Nos"}) - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - 'stock_uom': 'Nos' - }) + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item("Test Extra Item 1", {"is_stock_item": 1, "stock_uom": "Nos"}) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - 'stock_uom': 'Nos' - }) + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item("Test Extra Item 2", {"is_stock_item": 1, "stock_uom": "Nos"}) - if not frappe.db.exists('Item', "Test Extra Item 3"): - make_item("Test Extra Item 3", { - 'is_stock_item': 1, - 'stock_uom': 'Nos' - }) - bom = frappe.get_doc({ - 'doctype': 'BOM', - 'is_default': 1, - 'item': item_code, - 'currency': 'USD', - 'quantity': 1, - 'company': '_Test Company' - }) + if not frappe.db.exists("Item", "Test Extra Item 3"): + make_item("Test Extra Item 3", {"is_stock_item": 1, "stock_uom": "Nos"}) + bom = frappe.get_doc( + { + "doctype": "BOM", + "is_default": 1, + "item": item_code, + "currency": "USD", + "quantity": 1, + "company": "_Test Company", + } + ) for item in ["Test Extra Item 1", "Test Extra Item 2"]: - item_doc = frappe.get_doc('Item', item) + item_doc = frappe.get_doc("Item", item) - bom.append('items', { - 'item_code': item, - 'qty': 1, - 'uom': item_doc.stock_uom, - 'stock_uom': item_doc.stock_uom, - 'rate': item_doc.valuation_rate - }) + bom.append( + "items", + { + "item_code": item, + "qty": 1, + "uom": item_doc.stock_uom, + "stock_uom": item_doc.stock_uom, + "rate": item_doc.valuation_rate, + }, + ) - bom.append('items', { - 'item_code': "Test Extra Item 3", - 'qty': 1, - 'uom': item_doc.stock_uom, - 'stock_uom': item_doc.stock_uom, - 'rate': 0, - 'sourced_by_supplier': 1 - }) + bom.append( + "items", + { + "item_code": "Test Extra Item 3", + "qty": 1, + "uom": item_doc.stock_uom, + "stock_uom": item_doc.stock_uom, + "rate": 0, + "sourced_by_supplier": 1, + }, + ) bom.insert(ignore_permissions=True) bom.update_cost() bom.submit() # test that sourced_by_supplier rate is zero even after updating cost self.assertEqual(bom.items[2].rate, 0) # test in Purchase Order sourced_by_supplier is not added to Supplied Item - po = create_purchase_order(item_code=item_code, qty=1, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1]) supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) self.assertEqual(bom_items, supplied_items) - def test_bom_recursion_1st_level(self): """BOM should not allow BOM item again in child""" item_code = "_Test BOM Recursion" - make_item(item_code, {'is_stock_item': 1}) + make_item(item_code, {"is_stock_item": 1}) bom = frappe.new_doc("BOM") bom.item = item_code @@ -264,8 +274,8 @@ class TestBOM(FrappeTestCase): def test_bom_recursion_transitive(self): item1 = "_Test BOM Recursion" item2 = "_Test BOM Recursion 2" - make_item(item1, {'is_stock_item': 1}) - make_item(item2, {'is_stock_item': 1}) + make_item(item1, {"is_stock_item": 1}) + make_item(item2, {"is_stock_item": 1}) bom1 = frappe.new_doc("BOM") bom1.item = item1 @@ -323,7 +333,10 @@ class TestBOM(FrappeTestCase): def test_bom_tree_representation(self): bom_tree = { "Assembly": { - "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly1": { + "ChildPart1": {}, + "ChildPart2": {}, + }, "SubAssembly2": {"ChildPart3": {}}, "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, "ChildPart5": {}, @@ -334,7 +347,7 @@ class TestBOM(FrappeTestCase): parent_bom = create_nested_bom(bom_tree, prefix="") created_tree = parent_bom.get_tree_representation() - reqd_order = level_order_traversal(bom_tree)[1:] # skip first item + reqd_order = level_order_traversal(bom_tree)[1:] # skip first item created_order = created_tree.level_order_traversal() self.assertEqual(len(reqd_order), len(created_order)) @@ -343,17 +356,28 @@ class TestBOM(FrappeTestCase): self.assertEqual(reqd_item, created_item.item_code) def test_bom_item_query(self): - query = partial(item_query, doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters={"is_stock_item": 1}) + query = partial( + item_query, + doctype="Item", + txt="", + searchfield="name", + start=0, + page_len=20, + filters={"is_stock_item": 1}, + ) test_items = query(txt="_Test") filtered = query(txt="_Test Item 2") - self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results") + self.assertNotEqual( + len(test_items), len(filtered), msg="Item filtering showing excessive results" + ) self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") - def test_valid_transfer_defaults(self): - bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}) + bom_with_op = frappe.db.get_value( + "BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1} + ) bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False) # test defaults @@ -382,12 +406,8 @@ class TestBOM(FrappeTestCase): bom.delete() def test_bom_name_length(self): - """ test >140 char names""" - bom_tree = { - "x" * 140 : { - " ".join(["abc"] * 35): {} - } - } + """test >140 char names""" + bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}} create_nested_bom(bom_tree, prefix="") def test_version_index(self): @@ -405,15 +425,14 @@ class TestBOM(FrappeTestCase): for expected_index, existing_boms in version_index_test_cases: with self.subTest(): - self.assertEqual(expected_index, bom.get_next_version_index(existing_boms), - msg=f"Incorrect index for {existing_boms}") + self.assertEqual( + expected_index, + bom.get_next_version_index(existing_boms), + msg=f"Incorrect index for {existing_boms}", + ) def test_bom_versioning(self): - bom_tree = { - frappe.generate_hash(length=10) : { - frappe.generate_hash(length=10): {} - } - } + bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}} bom = create_nested_bom(bom_tree, prefix="") self.assertEqual(int(bom.name.split("-")[-1]), 1) original_bom_name = bom.name @@ -454,7 +473,7 @@ class TestBOM(FrappeTestCase): bom.save() bom.reload() - self.assertEqual(bom.quality_inspection_template, '_Test Quality Inspection Template') + self.assertEqual(bom.quality_inspection_template, "_Test Quality Inspection Template") bom.inspection_required = 0 bom.save() @@ -467,8 +486,7 @@ class TestBOM(FrappeTestCase): parent = frappe.generate_hash(length=10) child = frappe.generate_hash(length=10) - bom_tree = {parent: {child: {}} - } + bom_tree = {parent: {child: {}}} bom = create_nested_bom(bom_tree, prefix="") # add last purchase price @@ -487,6 +505,7 @@ class TestBOM(FrappeTestCase): def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) + def level_order_traversal(node): traversal = [] q = deque() @@ -501,9 +520,9 @@ def level_order_traversal(node): return traversal + def create_nested_bom(tree, prefix="_Test bom "): - """ Helper function to create a simple nested bom from tree describing item names. (along with required items) - """ + """Helper function to create a simple nested bom from tree describing item names. (along with required items)""" def create_items(bom_tree): for item_code, subtree in bom_tree.items(): @@ -511,6 +530,7 @@ def create_nested_bom(tree, prefix="_Test bom "): if not frappe.db.exists("Item", bom_item_code): frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert() create_items(subtree) + create_items(tree) def dfs(tree, node): @@ -545,10 +565,13 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non warehouse_list = [warehouse_list] if not warehouse_list: - warehouse_list = frappe.db.sql_list(""" + warehouse_list = frappe.db.sql_list( + """ select warehouse from `tabBin` where item_code=%s and actual_qty > 0 - """, item_code) + """, + item_code, + ) if not warehouse_list: warehouse_list.append("_Test Warehouse - _TC") @@ -556,44 +579,51 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non for warehouse in warehouse_list: create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) + def create_bom_with_process_loss_item( - fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1): + fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1 +): bom_doc = frappe.new_doc("BOM") bom_doc.item = fg_item.item_code bom_doc.quantity = fg_qty - bom_doc.append("items", { - "item_code": bom_item.item_code, - "qty": 1, - "uom": bom_item.stock_uom, - "stock_uom": bom_item.stock_uom, - "rate": 100.0 - }) - bom_doc.append("scrap_items", { - "item_code": fg_item.item_code, - "qty": scrap_qty, - "stock_qty": scrap_qty, - "uom": fg_item.stock_uom, - "stock_uom": fg_item.stock_uom, - "rate": scrap_rate, - "is_process_loss": is_process_loss - }) + bom_doc.append( + "items", + { + "item_code": bom_item.item_code, + "qty": 1, + "uom": bom_item.stock_uom, + "stock_uom": bom_item.stock_uom, + "rate": 100.0, + }, + ) + bom_doc.append( + "scrap_items", + { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + "is_process_loss": is_process_loss, + }, + ) bom_doc.currency = "INR" return bom_doc + def create_process_loss_bom_items(): item_list = [ ("_Test Item - Non Whole UOM", "Kg"), ("_Test Item - Whole UOM", "Unit"), - ("_Test PL BOM Item", "Unit") + ("_Test PL BOM Item", "Unit"), ] return [create_process_loss_bom_item(it) for it in item_list] + def create_process_loss_bom_item(item_tuple): item_code, stock_uom = item_tuple if frappe.db.exists("Item", item_code) is None: - return make_item( - item_code, - {'stock_uom':stock_uom, 'valuation_rate':100} - ) + return make_item(item_code, {"stock_uom": stock_uom, "valuation_rate": 100}) else: return frappe.get_doc("Item", item_code) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index f9c3b062179..9f120d175ed 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -21,14 +21,14 @@ class BOMUpdateTool(Document): unit_cost = get_new_bom_unit_cost(self.new_bom) self.update_new_bom(unit_cost) - frappe.cache().delete_key('bom_children') + frappe.cache().delete_key("bom_children") bom_list = self.get_parent_boms(self.new_bom) with click.progressbar(bom_list) as bom_list: pass for bom in bom_list: try: - bom_obj = frappe.get_cached_doc('BOM', bom) + bom_obj = frappe.get_cached_doc("BOM", bom) # this is only used for versioning and we do not want # to make separate db calls by using load_doc_before_save # which proves to be expensive while doing bulk replace @@ -38,7 +38,7 @@ class BOMUpdateTool(Document): bom_obj.calculate_cost() bom_obj.update_parent_cost() bom_obj.db_update() - if bom_obj.meta.get('track_changes') and not bom_obj.flags.ignore_version: + if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: bom_obj.save_version() except Exception: frappe.log_error(frappe.get_traceback()) @@ -47,20 +47,26 @@ class BOMUpdateTool(Document): if cstr(self.current_bom) == cstr(self.new_bom): frappe.throw(_("Current BOM and New BOM can not be same")) - if frappe.db.get_value("BOM", self.current_bom, "item") \ - != frappe.db.get_value("BOM", self.new_bom, "item"): - frappe.throw(_("The selected BOMs are not for the same item")) + if frappe.db.get_value("BOM", self.current_bom, "item") != frappe.db.get_value( + "BOM", self.new_bom, "item" + ): + frappe.throw(_("The selected BOMs are not for the same item")) def update_new_bom(self, unit_cost): - frappe.db.sql("""update `tabBOM Item` set bom_no=%s, + frappe.db.sql( + """update `tabBOM Item` set bom_no=%s, rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""", - (self.new_bom, unit_cost, unit_cost, self.current_bom)) + (self.new_bom, unit_cost, unit_cost, self.current_bom), + ) def get_parent_boms(self, bom, bom_list=None): if bom_list is None: bom_list = [] - data = frappe.db.sql("""SELECT DISTINCT parent FROM `tabBOM Item` - WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", bom) + data = frappe.db.sql( + """SELECT DISTINCT parent FROM `tabBOM Item` + WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", + bom, + ) for d in data: if self.new_bom == d[0]: @@ -71,29 +77,45 @@ class BOMUpdateTool(Document): return list(set(bom_list)) + def get_new_bom_unit_cost(bom): - new_bom_unitcost = frappe.db.sql("""SELECT `total_cost`/`quantity` - FROM `tabBOM` WHERE name = %s""", bom) + new_bom_unitcost = frappe.db.sql( + """SELECT `total_cost`/`quantity` + FROM `tabBOM` WHERE name = %s""", + bom, + ) return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0 + @frappe.whitelist() def enqueue_replace_bom(args): if isinstance(args, string_types): args = json.loads(args) - frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", args=args, timeout=40000) + frappe.enqueue( + "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", + args=args, + timeout=40000, + ) frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) + @frappe.whitelist() def enqueue_update_cost(): - frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000) - frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) + frappe.enqueue( + "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000 + ) + frappe.msgprint( + _("Queued for updating latest price in all Bill of Materials. It may take a few minutes.") + ) + def update_latest_price_in_all_boms(): if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() + def replace_bom(args): frappe.db.auto_commit_on_many_writes = 1 args = frappe._dict(args) @@ -105,6 +127,7 @@ def replace_bom(args): frappe.db.auto_commit_on_many_writes = 0 + def update_cost(): frappe.db.auto_commit_on_many_writes = 1 bom_list = get_boms_in_bottom_up_order() diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index b4c625d6108..57785e58dd0 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -8,7 +8,8 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item -test_records = frappe.get_test_records('BOM') +test_records = frappe.get_test_records("BOM") + class TestBOMUpdateTool(FrappeTestCase): def test_replace_bom(self): @@ -37,10 +38,13 @@ class TestBOMUpdateTool(FrappeTestCase): if item_doc.valuation_rate != 100.00: frappe.db.set_value("Item", item_doc.name, "valuation_rate", 100) - bom_no = frappe.db.get_value('BOM', {'item': 'BOM Cost Test Item 1'}, "name") + bom_no = frappe.db.get_value("BOM", {"item": "BOM Cost Test Item 1"}, "name") if not bom_no: - doc = make_bom(item = 'BOM Cost Test Item 1', - raw_materials =['BOM Cost Test Item 2', 'BOM Cost Test Item 3'], currency="INR") + doc = make_bom( + item="BOM Cost Test Item 1", + raw_materials=["BOM Cost Test Item 2", "BOM Cost Test Item 3"], + currency="INR", + ) else: doc = frappe.get_doc("BOM", bom_no) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index b5e16dd3c69..c8b37a9c80d 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -26,15 +26,27 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings ) -class OverlapError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + + +class OperationMismatchError(frappe.ValidationError): + pass + + +class OperationSequenceError(frappe.ValidationError): + pass + + +class JobCardCancelError(frappe.ValidationError): + pass -class OperationMismatchError(frappe.ValidationError): pass -class OperationSequenceError(frappe.ValidationError): pass -class JobCardCancelError(frappe.ValidationError): pass class JobCard(Document): def onload(self): - excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + excess_transfer = frappe.db.get_single_value( + "Manufacturing Settings", "job_card_excess_transfer" + ) self.set_onload("job_card_excess_transfer", excess_transfer) self.set_onload("work_order_stopped", self.is_work_order_stopped()) @@ -50,25 +62,33 @@ class JobCard(Document): def set_sub_operations(self): if not self.sub_operations and self.operation: self.sub_operations = [] - for row in frappe.get_all('Sub Operation', - filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'): - row.status = 'Pending' + for row in frappe.get_all( + "Sub Operation", + filters={"parent": self.operation}, + fields=["operation", "idx"], + order_by="idx", + ): + row.status = "Pending" row.sub_operation = row.operation - self.append('sub_operations', row) + self.append("sub_operations", row) def validate_time_logs(self): self.total_time_in_mins = 0.0 self.total_completed_qty = 0.0 - if self.get('time_logs'): - for d in self.get('time_logs'): + if self.get("time_logs"): + for d in self.get("time_logs"): if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time): frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx)) data = self.get_overlap_for(d) if data: - frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") - .format(d.idx, self.name, data.name), OverlapError) + frappe.throw( + _("Row {0}: From Time and To Time of {1} is overlapping with {2}").format( + d.idx, self.name, data.name + ), + OverlapError, + ) if d.from_time and d.to_time: d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 @@ -86,8 +106,9 @@ class JobCard(Document): production_capacity = 1 if self.workstation: - production_capacity = frappe.get_cached_value("Workstation", - self.workstation, 'production_capacity') or 1 + production_capacity = ( + frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1 + ) validate_overlap_for = " and jc.workstation = %(workstation)s " if args.get("employee"): @@ -95,11 +116,12 @@ class JobCard(Document): production_capacity = 1 validate_overlap_for = " and jctl.employee = %(employee)s " - extra_cond = '' + extra_cond = "" if check_next_available_slot: extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)" - existing = frappe.db.sql("""select jc.name as name, jctl.to_time from + existing = frappe.db.sql( + """select jc.name as name, jctl.to_time from `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and ( (%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or @@ -107,15 +129,19 @@ class JobCard(Document): (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0} ) and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1} - order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for), + order by jctl.to_time desc limit 1""".format( + extra_cond, validate_overlap_for + ), { "from_time": args.from_time, "to_time": args.to_time, "name": args.name or "No Name", "parent": args.parent or "No Name", "employee": args.get("employee"), - "workstation": self.workstation - }, as_dict=True) + "workstation": self.workstation, + }, + as_dict=True, + ) if existing and production_capacity > len(existing): return @@ -125,10 +151,7 @@ class JobCard(Document): def schedule_time_logs(self, row): row.remaining_time_in_mins = row.time_in_mins while row.remaining_time_in_mins > 0: - args = frappe._dict({ - "from_time": row.planned_start_time, - "to_time": row.planned_end_time - }) + args = frappe._dict({"from_time": row.planned_start_time, "to_time": row.planned_end_time}) self.validate_overlap_for_workstation(args, row) self.check_workstation_time(row) @@ -141,13 +164,16 @@ class JobCard(Document): def check_workstation_time(self, row): workstation_doc = frappe.get_cached_doc("Workstation", self.workstation) - if (not workstation_doc.working_hours or - cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))): + if not workstation_doc.working_hours or cint( + frappe.db.get_single_value("Manufacturing Settings", "allow_overtime") + ): if get_datetime(row.planned_end_time) < get_datetime(row.planned_start_time): row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.time_in_mins) row.remaining_time_in_mins = 0.0 else: - row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time, row.planned_start_time) + row.remaining_time_in_mins -= time_diff_in_minutes( + row.planned_end_time, row.planned_start_time + ) self.update_time_logs(row) return @@ -167,14 +193,15 @@ class JobCard(Document): workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time)) workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time)) - if (get_datetime(row.planned_start_time) >= workstation_start_time and - get_datetime(row.planned_start_time) <= workstation_end_time): + if ( + get_datetime(row.planned_start_time) >= workstation_start_time + and get_datetime(row.planned_start_time) <= workstation_end_time + ): time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time) # If remaining time fit in workstation time logs else split hours as per workstation time if time_in_mins > row.remaining_time_in_mins: - row.planned_end_time = add_to_date(row.planned_start_time, - minutes=row.remaining_time_in_mins) + row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.remaining_time_in_mins) row.remaining_time_in_mins = 0 else: row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins) @@ -182,14 +209,16 @@ class JobCard(Document): self.update_time_logs(row) - if total_idx != (i+1) and row.remaining_time_in_mins > 0: - row.planned_start_time = datetime.datetime.combine(start_date, - get_time(workstation_doc.working_hours[i+1].start_time)) + if total_idx != (i + 1) and row.remaining_time_in_mins > 0: + row.planned_start_time = datetime.datetime.combine( + start_date, get_time(workstation_doc.working_hours[i + 1].start_time) + ) if row.remaining_time_in_mins > 0: start_date = add_days(start_date, 1) - row.planned_start_time = datetime.datetime.combine(start_date, - get_time(workstation_doc.working_hours[0].start_time)) + row.planned_start_time = datetime.datetime.combine( + start_date, get_time(workstation_doc.working_hours[0].start_time) + ) def add_time_log(self, args): last_row = [] @@ -204,21 +233,25 @@ class JobCard(Document): if last_row and args.get("complete_time"): for row in self.time_logs: if not row.to_time: - row.update({ - "to_time": get_datetime(args.get("complete_time")), - "operation": args.get("sub_operation"), - "completed_qty": args.get("completed_qty") or 0.0 - }) + row.update( + { + "to_time": get_datetime(args.get("complete_time")), + "operation": args.get("sub_operation"), + "completed_qty": args.get("completed_qty") or 0.0, + } + ) elif args.get("start_time"): - new_args = frappe._dict({ - "from_time": get_datetime(args.get("start_time")), - "operation": args.get("sub_operation"), - "completed_qty": 0.0 - }) + new_args = frappe._dict( + { + "from_time": get_datetime(args.get("start_time")), + "operation": args.get("sub_operation"), + "completed_qty": 0.0, + } + ) if employees: for name in employees: - new_args.employee = name.get('employee') + new_args.employee = name.get("employee") self.add_start_time_log(new_args) else: self.add_start_time_log(new_args) @@ -236,10 +269,7 @@ class JobCard(Document): def set_employees(self, employees): for name in employees: - self.append('employee', { - 'employee': name.get('employee'), - 'completed_qty': 0.0 - }) + self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0}) def reset_timer_value(self, args): self.started_time = None @@ -263,13 +293,17 @@ class JobCard(Document): operation_wise_completed_time = {} for time_log in self.time_logs: if time_log.operation not in operation_wise_completed_time: - operation_wise_completed_time.setdefault(time_log.operation, - frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []})) + operation_wise_completed_time.setdefault( + time_log.operation, + frappe._dict( + {"status": "Pending", "completed_qty": 0.0, "completed_time": 0.0, "employee": []} + ), + ) op_row = operation_wise_completed_time[time_log.operation] op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete" - if self.status == 'On Hold': - op_row.status = 'Pause' + if self.status == "On Hold": + op_row.status = "Pause" op_row.employee.append(time_log.employee) if time_log.time_in_mins: @@ -279,7 +313,7 @@ class JobCard(Document): for row in self.sub_operations: operation_deatils = operation_wise_completed_time.get(row.sub_operation) if operation_deatils: - if row.status != 'Complete': + if row.status != "Complete": row.status = operation_deatils.status row.completed_time = operation_deatils.completed_time @@ -289,43 +323,52 @@ class JobCard(Document): if operation_deatils.completed_qty: row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee)) else: - row.status = 'Pending' + row.status = "Pending" row.completed_time = 0.0 row.completed_qty = 0.0 def update_time_logs(self, row): - self.append("time_logs", { - "from_time": row.planned_start_time, - "to_time": row.planned_end_time, - "completed_qty": 0, - "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time), - }) + self.append( + "time_logs", + { + "from_time": row.planned_start_time, + "to_time": row.planned_end_time, + "completed_qty": 0, + "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time), + }, + ) @frappe.whitelist() def get_required_items(self): - if not self.get('work_order'): + if not self.get("work_order"): return - doc = frappe.get_doc('Work Order', self.get('work_order')) - if doc.transfer_material_against == 'Work Order' or doc.skip_transfer: + doc = frappe.get_doc("Work Order", self.get("work_order")) + if doc.transfer_material_against == "Work Order" or doc.skip_transfer: return for d in doc.required_items: if not d.operation: - frappe.throw(_("Row {0} : Operation is required against the raw material item {1}") - .format(d.idx, d.item_code)) + frappe.throw( + _("Row {0} : Operation is required against the raw material item {1}").format( + d.idx, d.item_code + ) + ) - if self.get('operation') == d.operation: - self.append('items', { - "item_code": d.item_code, - "source_warehouse": d.source_warehouse, - "uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'), - "item_name": d.item_name, - "description": d.description, - "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, - "rate": d.rate, - "amount": d.amount - }) + if self.get("operation") == d.operation: + self.append( + "items", + { + "item_code": d.item_code, + "source_warehouse": d.source_warehouse, + "uom": frappe.db.get_value("Item", d.item_code, "stock_uom"), + "item_name": d.item_name, + "description": d.description, + "required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty, + "rate": d.rate, + "amount": d.amount, + }, + ) def on_submit(self): self.validate_transfer_qty() @@ -339,31 +382,52 @@ class JobCard(Document): def validate_transfer_qty(self): if self.items and self.transferred_qty < self.for_quantity: - frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}') - .format(self.name)) + frappe.throw( + _( + "Materials needs to be transferred to the work in progress warehouse for the job card {0}" + ).format(self.name) + ) def validate_job_card(self): - if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped': - frappe.throw(_("Transaction not allowed against stopped Work Order {0}") - .format(get_link_to_form('Work Order', self.work_order))) + if ( + self.work_order + and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped" + ): + frappe.throw( + _("Transaction not allowed against stopped Work Order {0}").format( + get_link_to_form("Work Order", self.work_order) + ) + ) if not self.time_logs: - frappe.throw(_("Time logs are required for {0} {1}") - .format(bold("Job Card"), get_link_to_form("Job Card", self.name))) + frappe.throw( + _("Time logs are required for {0} {1}").format( + bold("Job Card"), get_link_to_form("Job Card", self.name) + ) + ) if self.for_quantity and self.total_completed_qty != self.for_quantity: total_completed_qty = bold(_("Total Completed Qty")) qty_to_manufacture = bold(_("Qty to Manufacture")) - frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})") - .format(total_completed_qty, bold(self.total_completed_qty), qty_to_manufacture,bold(self.for_quantity))) + frappe.throw( + _("The {0} ({1}) must be equal to {2} ({3})").format( + total_completed_qty, + bold(self.total_completed_qty), + qty_to_manufacture, + bold(self.for_quantity), + ) + ) def update_work_order(self): if not self.work_order: return - if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings', - 'add_corrective_operation_cost_in_finished_good_valuation')): + if self.is_corrective_job_card and not cint( + frappe.db.get_single_value( + "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" + ) + ): return for_quantity, time_in_mins = 0, 0 @@ -375,7 +439,7 @@ class JobCard(Document): for_quantity = flt(data[0].completed_qty) time_in_mins = flt(data[0].time_in_mins) - wo = frappe.get_doc('Work Order', self.work_order) + wo = frappe.get_doc("Work Order", self.work_order) if self.is_corrective_job_card: self.update_corrective_in_work_order(wo) @@ -386,8 +450,11 @@ class JobCard(Document): def update_corrective_in_work_order(self, wo): wo.corrective_operation_cost = 0.0 - for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'], - filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}): + for row in frappe.get_all( + "Job Card", + fields=["total_time_in_mins", "hour_rate"], + filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order}, + ): wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) wo.calculate_operating_cost() @@ -395,27 +462,37 @@ class JobCard(Document): wo.save() def validate_produced_quantity(self, for_quantity, wo): - if self.docstatus < 2: return + if self.docstatus < 2: + return if wo.produced_qty > for_quantity: - first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.") - .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item))) + first_part_msg = _( + "The {0} {1} is used to calculate the valuation cost for the finished good {2}." + ).format( + frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item) + ) - second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.") - .format(frappe.bold(get_link_to_form("Work Order", self.work_order)))) + second_part_msg = _( + "Kindly cancel the Manufacturing Entries first against the work order {0}." + ).format(frappe.bold(get_link_to_form("Work Order", self.work_order))) - frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg), - JobCardCancelError, title = _("Error")) + frappe.throw( + _("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error") + ) def update_work_order_data(self, for_quantity, time_in_mins, wo): - time_data = frappe.db.sql(""" + time_data = frappe.db.sql( + """ SELECT min(from_time) as start_time, max(to_time) as end_time FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0 - """, (self.work_order, self.operation_id), as_dict=1) + """, + (self.work_order, self.operation_id), + as_dict=1, + ) for data in wo.operations: if data.get("name") == self.operation_id: @@ -434,91 +511,118 @@ class JobCard(Document): wo.save() def get_current_operation_data(self): - return frappe.get_all('Job Card', - fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id, - "is_corrective_job_card": 0}) + return frappe.get_all( + "Job Card", + fields=["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], + filters={ + "docstatus": 1, + "work_order": self.work_order, + "operation_id": self.operation_id, + "is_corrective_job_card": 0, + }, + ) def set_transferred_qty_in_job_card(self, ste_doc): for row in ste_doc.items: - if not row.job_card_item: continue + if not row.job_card_item: + continue - qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se + qty = frappe.db.sql( + """ SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and se.purpose = 'Material Transfer for Manufacture' - """, (row.job_card_item))[0][0] + """, + (row.job_card_item), + )[0][0] - frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty)) + frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty)) def set_transferred_qty(self, update_status=False): "Set total FG Qty for which RM was transferred." if not self.items: self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 - doc = frappe.get_doc('Work Order', self.get('work_order')) - if doc.transfer_material_against == 'Work Order' or doc.skip_transfer: + doc = frappe.get_doc("Work Order", self.get("work_order")) + if doc.transfer_material_against == "Work Order" or doc.skip_transfer: return if self.items: # sum of 'For Quantity' of Stock Entries against JC - self.transferred_qty = frappe.db.get_value('Stock Entry', { - 'job_card': self.name, - 'work_order': self.work_order, - 'docstatus': 1, - 'purpose': 'Material Transfer for Manufacture' - }, 'sum(fg_completed_qty)') or 0 + self.transferred_qty = ( + frappe.db.get_value( + "Stock Entry", + { + "job_card": self.name, + "work_order": self.work_order, + "docstatus": 1, + "purpose": "Material Transfer for Manufacture", + }, + "sum(fg_completed_qty)", + ) + or 0 + ) self.db_set("transferred_qty", self.transferred_qty) qty = 0 if self.work_order: - doc = frappe.get_doc('Work Order', self.work_order) - if doc.transfer_material_against == 'Job Card' and not doc.skip_transfer: + doc = frappe.get_doc("Work Order", self.work_order) + if doc.transfer_material_against == "Job Card" and not doc.skip_transfer: completed = True for d in doc.operations: - if d.status != 'Completed': + if d.status != "Completed": completed = False break if completed: - job_cards = frappe.get_all('Job Card', filters = {'work_order': self.work_order, - 'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id') + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": self.work_order, "docstatus": ("!=", 2)}, + fields="sum(transferred_qty) as qty", + group_by="operation_id", + ) if job_cards: qty = min(d.qty for d in job_cards) - doc.db_set('material_transferred_for_manufacturing', qty) + doc.db_set("material_transferred_for_manufacturing", qty) self.set_status(update_status) def set_status(self, update_status=False): - if self.status == "On Hold": return + if self.status == "On Hold": + return - self.status = { - 0: "Open", - 1: "Submitted", - 2: "Cancelled" - }[self.docstatus or 0] + self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] if self.for_quantity <= self.transferred_qty: - self.status = 'Material Transferred' + self.status = "Material Transferred" if self.time_logs: - self.status = 'Work In Progress' + self.status = "Work In Progress" - if (self.docstatus == 1 and - (self.for_quantity <= self.total_completed_qty or not self.items)): - self.status = 'Completed' + if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items): + self.status = "Completed" if update_status: - self.db_set('status', self.status) + self.db_set("status", self.status) def validate_operation_id(self): - if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and - frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id): + if ( + self.get("operation_id") + and self.get("operation_row_number") + and self.operation + and self.work_order + and frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") + != self.operation_id + ): work_order = bold(get_link_to_form("Work Order", self.work_order)) - frappe.throw(_("Operation {0} does not belong to the work order {1}") - .format(bold(self.operation), work_order), OperationMismatchError) + frappe.throw( + _("Operation {0} does not belong to the work order {1}").format( + bold(self.operation), work_order + ), + OperationMismatchError, + ) def validate_sequence_id(self): if self.is_corrective_job_card: @@ -534,18 +638,25 @@ class JobCard(Document): current_operation_qty += flt(self.total_completed_qty) - data = frappe.get_all("Work Order Operation", - fields = ["operation", "status", "completed_qty"], - filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)}, - order_by = "sequence_id, idx") + data = frappe.get_all( + "Work Order Operation", + fields=["operation", "status", "completed_qty"], + filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, + order_by="sequence_id, idx", + ) - message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name), - bold(get_link_to_form("Work Order", self.work_order))) + message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format( + bold(self.name), bold(get_link_to_form("Work Order", self.work_order)) + ) for row in data: if row.status != "Completed" and row.completed_qty < current_operation_qty: - frappe.throw(_("{0}, complete the operation {1} before the operation {2}.") - .format(message, bold(row.operation), bold(self.operation)), OperationSequenceError) + frappe.throw( + _("{0}, complete the operation {1} before the operation {2}.").format( + message, bold(row.operation), bold(self.operation) + ), + OperationSequenceError, + ) def validate_work_order(self): if self.is_work_order_stopped(): @@ -553,13 +664,14 @@ class JobCard(Document): def is_work_order_stopped(self): if self.work_order: - status = frappe.get_value('Work Order', self.work_order) + status = frappe.get_value("Work Order", self.work_order) if status == "Closed": return True return False + @frappe.whitelist() def make_time_log(args): if isinstance(args, str): @@ -570,16 +682,17 @@ def make_time_log(args): doc.validate_sequence_id() doc.add_time_log(args) + @frappe.whitelist() def get_operation_details(work_order, operation): if work_order and operation: - return frappe.get_all("Work Order Operation", fields = ["name", "idx"], - filters = { - "parent": work_order, - "operation": operation - } + return frappe.get_all( + "Work Order Operation", + fields=["name", "idx"], + filters={"parent": work_order, "operation": operation}, ) + @frappe.whitelist() def get_operations(doctype, txt, searchfield, start, page_len, filters): if not filters.get("work_order"): @@ -589,12 +702,16 @@ def get_operations(doctype, txt, searchfield, start, page_len, filters): if txt: args["operation"] = ("like", "%{0}%".format(txt)) - return frappe.get_all("Work Order Operation", - filters = args, - fields = ["distinct operation as operation"], - limit_start = start, - limit_page_length = page_len, - order_by="idx asc", as_list=1) + return frappe.get_all( + "Work Order Operation", + filters=args, + fields=["distinct operation as operation"], + limit_start=start, + limit_page_length=page_len, + order_by="idx asc", + as_list=1, + ) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): @@ -604,26 +721,29 @@ def make_material_request(source_name, target_doc=None): def set_missing_values(source, target): target.material_request_type = "Material Transfer" - doclist = get_mapped_doc("Job Card", source_name, { - "Job Card": { - "doctype": "Material Request", - "field_map": { - "name": "job_card", + doclist = get_mapped_doc( + "Job Card", + source_name, + { + "Job Card": { + "doctype": "Material Request", + "field_map": { + "name": "job_card", + }, + }, + "Job Card Item": { + "doctype": "Material Request Item", + "field_map": {"required_qty": "qty", "uom": "stock_uom", "name": "job_card_item"}, + "postprocess": update_item, }, }, - "Job Card Item": { - "doctype": "Material Request Item", - "field_map": { - "required_qty": "qty", - "uom": "stock_uom", - "name": "job_card_item" - }, - "postprocess": update_item, - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): def update_item(source, target, source_parent): @@ -641,7 +761,7 @@ def make_stock_entry(source_name, target_doc=None): target.from_bom = 1 # avoid negative 'For Quantity' - pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0)) + pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0)) target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0 target.set_transfer_qty() @@ -649,36 +769,45 @@ def make_stock_entry(source_name, target_doc=None): target.set_missing_values() target.set_stock_entry_type() - wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item") + wo_allows_alternate_item = frappe.db.get_value( + "Work Order", target.work_order, "allow_alternative_item" + ) for item in target.items: - item.allow_alternative_item = int(wo_allows_alternate_item and - frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")) + item.allow_alternative_item = int( + wo_allows_alternate_item + and frappe.get_cached_value("Item", item.item_code, "allow_alternative_item") + ) - doclist = get_mapped_doc("Job Card", source_name, { - "Job Card": { - "doctype": "Stock Entry", - "field_map": { - "name": "job_card", - "for_quantity": "fg_completed_qty" + doclist = get_mapped_doc( + "Job Card", + source_name, + { + "Job Card": { + "doctype": "Stock Entry", + "field_map": {"name": "job_card", "for_quantity": "fg_completed_qty"}, + }, + "Job Card Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "source_warehouse": "s_warehouse", + "required_qty": "qty", + "name": "job_card_item", + }, + "postprocess": update_item, + "condition": lambda doc: doc.required_qty > 0, }, }, - "Job Card Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "source_warehouse": "s_warehouse", - "required_qty": "qty", - "name": "job_card_item" - }, - "postprocess": update_item, - "condition": lambda doc: doc.required_qty > 0 - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + def time_diff_in_minutes(string_ed_date, string_st_date): return time_diff(string_ed_date, string_st_date).total_seconds() / 60 + @frappe.whitelist() def get_job_details(start, end, filters=None): events = [] @@ -686,41 +815,49 @@ def get_job_details(start, end, filters=None): event_color = { "Completed": "#cdf5a6", "Material Transferred": "#ffdd9e", - "Work In Progress": "#D3D3D3" + "Work In Progress": "#D3D3D3", } from frappe.desk.reportview import get_filters_cond + conditions = get_filters_cond("Job Card", filters, []) - job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order, + job_cards = frappe.db.sql( + """ SELECT `tabJob Card`.name, `tabJob Card`.work_order, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''), min(`tabJob Card Time Log`.from_time) as from_time, max(`tabJob Card Time Log`.to_time) as to_time FROM `tabJob Card` , `tabJob Card Time Log` WHERE `tabJob Card`.name = `tabJob Card Time Log`.parent {0} - group by `tabJob Card`.name""".format(conditions), as_dict=1) + group by `tabJob Card`.name""".format( + conditions + ), + as_dict=1, + ) for d in job_cards: - subject_data = [] - for field in ["name", "work_order", "remarks"]: - if not d.get(field): continue + subject_data = [] + for field in ["name", "work_order", "remarks"]: + if not d.get(field): + continue - subject_data.append(d.get(field)) + subject_data.append(d.get(field)) - color = event_color.get(d.status) - job_card_data = { - 'from_time': d.from_time, - 'to_time': d.to_time, - 'name': d.name, - 'subject': '\n'.join(subject_data), - 'color': color if color else "#89bcde" - } + color = event_color.get(d.status) + job_card_data = { + "from_time": d.from_time, + "to_time": d.to_time, + "name": d.name, + "subject": "\n".join(subject_data), + "color": color if color else "#89bcde", + } - events.append(job_card_data) + events.append(job_card_data) return events + @frappe.whitelist() def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None): def set_missing_values(source, target): @@ -728,20 +865,26 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.operation = operation target.for_operation = for_operation - target.set('time_logs', []) - target.set('employee', []) - target.set('items', []) + target.set("time_logs", []) + target.set("employee", []) + target.set("items", []) target.set_sub_operations() target.get_required_items() target.validate_time_logs() - doclist = get_mapped_doc("Job Card", source_name, { - "Job Card": { - "doctype": "Job Card", - "field_map": { - "name": "for_job_card", - }, - } - }, target_doc, set_missing_values) + doclist = get_mapped_doc( + "Job Card", + source_name, + { + "Job Card": { + "doctype": "Job Card", + "field_map": { + "name": "for_job_card", + }, + } + }, + target_doc, + set_missing_values, + ) return doclist diff --git a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py index 24362f8246d..14c1f36d0dc 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py +++ b/erpnext/manufacturing/doctype/job_card/job_card_dashboard.py @@ -1,21 +1,12 @@ - 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'] - } - ] + "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/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index c5841c16f2d..4647ddf05f7 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -20,13 +20,11 @@ class TestJobCard(FrappeTestCase): transfer_material_against, source_warehouse = None, None - tests_that_skip_setup = ( - "test_job_card_material_transfer_correctness", - ) + tests_that_skip_setup = ("test_job_card_material_transfer_correctness",) tests_that_transfer_against_jc = ( "test_job_card_multiple_materials_transfer", "test_job_card_excess_material_transfer", - "test_job_card_partial_material_transfer" + "test_job_card_partial_material_transfer", ) if self._testMethodName in tests_that_skip_setup: @@ -40,7 +38,7 @@ class TestJobCard(FrappeTestCase): item="_Test FG Item 2", qty=2, transfer_material_against=transfer_material_against, - source_warehouse=source_warehouse + source_warehouse=source_warehouse, ) def tearDown(self): @@ -48,8 +46,9 @@ class TestJobCard(FrappeTestCase): def test_job_card(self): - job_cards = frappe.get_all('Job Card', - filters = {'work_order': self.work_order.name}, fields = ["operation_id", "name"]) + job_cards = frappe.get_all( + "Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"] + ) if job_cards: job_card = job_cards[0] @@ -63,30 +62,38 @@ class TestJobCard(FrappeTestCase): frappe.delete_doc("Job Card", d.name) def test_job_card_with_different_work_station(self): - job_cards = frappe.get_all('Job Card', - filters = {'work_order': self.work_order.name}, - fields = ["operation_id", "workstation", "name", "for_quantity"]) + job_cards = frappe.get_all( + "Job Card", + filters={"work_order": self.work_order.name}, + fields=["operation_id", "workstation", "name", "for_quantity"], + ) job_card = job_cards[0] if job_card: - workstation = frappe.db.get_value("Workstation", - {"name": ("not in", [job_card.workstation])}, "name") + workstation = frappe.db.get_value( + "Workstation", {"name": ("not in", [job_card.workstation])}, "name" + ) if not workstation or job_card.workstation == workstation: workstation = make_workstation(workstation_name=random_string(5)).name doc = frappe.get_doc("Job Card", job_card.name) doc.workstation = workstation - doc.append("time_logs", { - "from_time": "2009-01-01 12:06:25", - "to_time": "2009-01-01 12:37:25", - "time_in_mins": "31.00002", - "completed_qty": job_card.for_quantity - }) + doc.append( + "time_logs", + { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "time_in_mins": "31.00002", + "completed_qty": job_card.for_quantity, + }, + ) doc.submit() - completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty") + completed_qty = frappe.db.get_value( + "Work Order Operation", job_card.operation_id, "completed_qty" + ) self.assertEqual(completed_qty, job_card.for_quantity) doc.cancel() @@ -97,51 +104,49 @@ class TestJobCard(FrappeTestCase): def test_job_card_overlap(self): wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2) - jc1_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) - jc2_name = frappe.db.get_value("Job Card", {'work_order': wo2.name}) + jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) + jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name}) jc1 = frappe.get_doc("Job Card", jc1_name) jc2 = frappe.get_doc("Job Card", jc2_name) - employee = "_T-Employee-00001" # from test records + employee = "_T-Employee-00001" # from test records - jc1.append("time_logs", { - "from_time": "2021-01-01 00:00:00", - "to_time": "2021-01-01 08:00:00", - "completed_qty": 1, - "employee": employee, - }) + jc1.append( + "time_logs", + { + "from_time": "2021-01-01 00:00:00", + "to_time": "2021-01-01 08:00:00", + "completed_qty": 1, + "employee": employee, + }, + ) jc1.save() # add a new entry in same time slice - jc2.append("time_logs", { - "from_time": "2021-01-01 00:01:00", - "to_time": "2021-01-01 06:00:00", - "completed_qty": 1, - "employee": employee, - }) + jc2.append( + "time_logs", + { + "from_time": "2021-01-01 00:01:00", + "to_time": "2021-01-01 06:00:00", + "completed_qty": 1, + "employee": employee, + }, + ) self.assertRaises(OverlapError, jc2.save) def test_job_card_multiple_materials_transfer(self): "Test transferring RMs separately against Job Card with multiple RMs." + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100) make_stock_entry( - item_code="_Test Item", - target="Stores - _TC", - qty=10, - basic_rate=100 - ) - make_stock_entry( - item_code="_Test Item Home Desktop Manufactured", - target="Stores - _TC", - qty=6, - basic_rate=100 + item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100 ) - job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) transfer_entry_1 = make_stock_entry_from_jc(job_card_name) - del transfer_entry_1.items[1] # transfer only 1 of 2 RMs + del transfer_entry_1.items[1] # transfer only 1 of 2 RMs transfer_entry_1.insert() transfer_entry_1.submit() @@ -162,12 +167,12 @@ class TestJobCard(FrappeTestCase): def test_job_card_excess_material_transfer(self): "Test transferring more than required RM against Job Card." - make_stock_entry(item_code="_Test Item", target="Stores - _TC", - qty=25, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop Manufactured", - target="Stores - _TC", qty=15, basic_rate=100) + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100 + ) - job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) self.assertEqual(job_card.status, "Open") @@ -193,11 +198,10 @@ class TestJobCard(FrappeTestCase): transfer_entry_3 = make_stock_entry_from_jc(job_card_name) self.assertEqual(transfer_entry_3.fg_completed_qty, 0) - job_card.append("time_logs", { - "from_time": "2021-01-01 00:01:00", - "to_time": "2021-01-01 06:00:00", - "completed_qty": 2 - }) + job_card.append( + "time_logs", + {"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2}, + ) job_card.save() job_card.submit() @@ -207,12 +211,12 @@ class TestJobCard(FrappeTestCase): def test_job_card_partial_material_transfer(self): "Test partial material transfer against Job Card" - make_stock_entry(item_code="_Test Item", target="Stores - _TC", - qty=25, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop Manufactured", - target="Stores - _TC", qty=15, basic_rate=100) + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100 + ) - job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name}) + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) # partially transfer @@ -242,15 +246,14 @@ class TestJobCard(FrappeTestCase): def test_job_card_material_transfer_correctness(self): """ - 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card - 2. Test impact of changing 'For Qty' in such a Stock Entry + 1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card + 2. Test impact of changing 'For Qty' in such a Stock Entry """ create_bom_with_multiple_operations() work_order = make_wo_with_transfer_against_jc() job_card_name = frappe.db.get_value( - "Job Card", - {"work_order": work_order.name,"operation": "Test Operation A"} + "Job Card", {"work_order": work_order.name, "operation": "Test Operation A"} ) job_card = frappe.get_doc("Job Card", job_card_name) @@ -276,6 +279,7 @@ class TestJobCard(FrappeTestCase): # rollback via tearDown method + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" from erpnext.manufacturing.doctype.operation.test_operation import make_operation @@ -287,19 +291,22 @@ def create_bom_with_multiple_operations(): "operation": "Test Operation A", "workstation": "_Test Workstation A", "hour_rate_rent": 300, - "time_in_mins": 60 + "time_in_mins": 60, } make_workstation(row) make_operation(row) - bom_doc.append("operations", { - "operation": "Test Operation A", - "description": "Test Operation A", - "workstation": "_Test Workstation A", - "hour_rate": 300, - "time_in_mins": 60, - "operating_cost": 100 - }) + bom_doc.append( + "operations", + { + "operation": "Test Operation A", + "description": "Test Operation A", + "workstation": "_Test Workstation A", + "hour_rate": 300, + "time_in_mins": 60, + "operating_cost": 100, + }, + ) bom_doc.transfer_material_against = "Job Card" bom_doc.save() @@ -307,6 +314,7 @@ def create_bom_with_multiple_operations(): return bom_doc + def make_wo_with_transfer_against_jc(): "Create a WO with multiple operations and Material Transfer against Job Card" @@ -315,7 +323,7 @@ def make_wo_with_transfer_against_jc(): qty=4, transfer_material_against="Job Card", source_warehouse="Stores - _TC", - do_not_submit=True + do_not_submit=True, ) work_order.required_items[0].operation = "Test Operation A" work_order.required_items[1].operation = "_Test Operation 1" @@ -323,8 +331,9 @@ def make_wo_with_transfer_against_jc(): return work_order + def make_bom_for_jc_tests(): - test_records = frappe.get_test_records('BOM') + test_records = frappe.get_test_records("BOM") bom = frappe.copy_doc(test_records[2]) bom.set_rate_of_sub_assembly_item_based_on_bom = 0 bom.rm_cost_as_per = "Valuation Rate" diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index c919e8bef1d..730a8575247 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -11,14 +11,19 @@ from frappe.utils import cint class ManufacturingSettings(Document): pass + def get_mins_between_operations(): - return relativedelta(minutes=cint(frappe.db.get_single_value("Manufacturing Settings", - "mins_between_operations")) or 10) + return relativedelta( + minutes=cint(frappe.db.get_single_value("Manufacturing Settings", "mins_between_operations")) + or 10 + ) + @frappe.whitelist() def is_material_consumption_enabled(): - if not hasattr(frappe.local, 'material_consumption'): - frappe.local.material_consumption = cint(frappe.db.get_single_value('Manufacturing Settings', - 'material_consumption')) + if not hasattr(frappe.local, "material_consumption"): + frappe.local.material_consumption = cint( + frappe.db.get_single_value("Manufacturing Settings", "material_consumption") + ) return frappe.local.material_consumption diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py index 41726f30cf5..9c8f9ac8d05 100644 --- a/erpnext/manufacturing/doctype/operation/operation.py +++ b/erpnext/manufacturing/doctype/operation/operation.py @@ -19,12 +19,14 @@ class Operation(Document): operation_list = [] for row in self.sub_operations: if row.operation in operation_list: - frappe.throw(_("The operation {0} can not add multiple times") - .format(frappe.bold(row.operation))) + frappe.throw( + _("The operation {0} can not add multiple times").format(frappe.bold(row.operation)) + ) if self.name == row.operation: - frappe.throw(_("The operation {0} can not be the sub operation") - .format(frappe.bold(row.operation))) + frappe.throw( + _("The operation {0} can not be the sub operation").format(frappe.bold(row.operation)) + ) operation_list.append(row.operation) diff --git a/erpnext/manufacturing/doctype/operation/operation_dashboard.py b/erpnext/manufacturing/doctype/operation/operation_dashboard.py index 4a548a64709..8dc901a2968 100644 --- a/erpnext/manufacturing/doctype/operation/operation_dashboard.py +++ b/erpnext/manufacturing/doctype/operation/operation_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'operation', - 'transactions': [ - { - 'label': _('Manufacture'), - 'items': ['BOM', 'Work Order', 'Job Card'] - } - ] + "fieldname": "operation", + "transactions": [{"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]}], } diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py index e511084e7d0..ce9f8e06495 100644 --- a/erpnext/manufacturing/doctype/operation/test_operation.py +++ b/erpnext/manufacturing/doctype/operation/test_operation.py @@ -5,11 +5,13 @@ import unittest import frappe -test_records = frappe.get_test_records('Operation') +test_records = frappe.get_test_records("Operation") + class TestOperation(unittest.TestCase): pass + def make_operation(*args, **kwargs): args = args if args else kwargs if isinstance(args, tuple): @@ -18,11 +20,9 @@ def make_operation(*args, **kwargs): args = frappe._dict(args) if not frappe.db.exists("Operation", args.operation): - doc = frappe.get_doc({ - "doctype": "Operation", - "name": args.operation, - "workstation": args.workstation - }) + doc = frappe.get_doc( + {"doctype": "Operation", "name": args.operation, "workstation": args.workstation} + ) doc.insert() return doc diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a262b91cb5f..2139260df60 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -36,7 +36,7 @@ class ProductionPlan(Document): def set_pending_qty_in_row_without_reference(self): "Set Pending Qty in independent rows (not from SO or MR)." - if self.docstatus > 0: # set only to initialise value before submit + if self.docstatus > 0: # set only to initialise value before submit return for item in self.po_items: @@ -49,7 +49,7 @@ class ProductionPlan(Document): self.total_planned_qty += flt(d.planned_qty) def validate_data(self): - for d in self.get('po_items'): + for d in self.get("po_items"): if not d.bom_no: frappe.throw(_("Please select BOM for Item in Row {0}").format(d.idx)) else: @@ -59,9 +59,9 @@ class ProductionPlan(Document): frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx)) def _rename_temporary_references(self): - """ po_items and sub_assembly_items items are both constructed client side without saving. + """po_items and sub_assembly_items items are both constructed client side without saving. - Attempt to fix linkages by using temporary names to map final row names. + Attempt to fix linkages by using temporary names to map final row names. """ new_name_map = {d.temporary_name: d.name for d in self.po_items if d.temporary_name} actual_names = {d.name for d in self.po_items} @@ -72,7 +72,7 @@ class ProductionPlan(Document): @frappe.whitelist() def get_open_sales_orders(self): - """ Pull sales orders which are pending to deliver based on criteria selected""" + """Pull sales orders which are pending to deliver based on criteria selected""" open_so = get_sales_orders(self) if open_so: @@ -81,20 +81,23 @@ class ProductionPlan(Document): frappe.msgprint(_("Sales orders are not available for production")) def add_so_in_table(self, open_so): - """ Add sales orders in the table""" - self.set('sales_orders', []) + """Add sales orders in the table""" + self.set("sales_orders", []) for data in open_so: - self.append('sales_orders', { - 'sales_order': data.name, - 'sales_order_date': data.transaction_date, - 'customer': data.customer, - 'grand_total': data.base_grand_total - }) + self.append( + "sales_orders", + { + "sales_order": data.name, + "sales_order_date": data.transaction_date, + "customer": data.customer, + "grand_total": data.base_grand_total, + }, + ) @frappe.whitelist() def get_pending_material_requests(self): - """ Pull Material Requests that are pending based on criteria selected""" + """Pull Material Requests that are pending based on criteria selected""" mr_filter = item_filter = "" if self.from_date: mr_filter += " and mr.transaction_date >= %(from_date)s" @@ -106,7 +109,8 @@ class ProductionPlan(Document): if self.item_code: item_filter += " and mr_item.item_code = %(item)s" - pending_mr = frappe.db.sql(""" + pending_mr = frappe.db.sql( + """ select distinct mr.name, mr.transaction_date from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr_item.parent = mr.name @@ -115,29 +119,34 @@ class ProductionPlan(Document): and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code and bom.is_active = 1)) - """.format(mr_filter, item_filter), { + """.format( + mr_filter, item_filter + ), + { "from_date": self.from_date, "to_date": self.to_date, "warehouse": self.warehouse, "item": self.item_code, - "company": self.company - }, as_dict=1) + "company": self.company, + }, + as_dict=1, + ) self.add_mr_in_table(pending_mr) def add_mr_in_table(self, pending_mr): - """ Add Material Requests in the table""" - self.set('material_requests', []) + """Add Material Requests in the table""" + self.set("material_requests", []) for data in pending_mr: - self.append('material_requests', { - 'material_request': data.name, - 'material_request_date': data.transaction_date - }) + self.append( + "material_requests", + {"material_request": data.name, "material_request_date": data.transaction_date}, + ) @frappe.whitelist() def get_items(self): - self.set('po_items', []) + self.set("po_items", []) if self.get_items_from == "Sales Order": self.get_so_items() @@ -152,10 +161,12 @@ class ProductionPlan(Document): def get_bom_item(self): """Check if Item or if its Template has a BOM.""" bom_item = None - has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1}) + has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1}) if not has_bom: - template_item = frappe.db.get_value('Item', self.item_code, ['variant_of']) - bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item + template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"]) + bom_item = ( + "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item + ) return bom_item def get_so_items(self): @@ -167,11 +178,12 @@ class ProductionPlan(Document): item_condition = "" bom_item = "bom.item = so_item.item_code" - if self.item_code and frappe.db.exists('Item', self.item_code): + if self.item_code and frappe.db.exists("Item", self.item_code): bom_item = self.get_bom_item() or bom_item - item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) + item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select distinct parent, item_code, warehouse, (qty - work_order_qty) * conversion_factor as pending_qty, @@ -181,16 +193,17 @@ class ProductionPlan(Document): where parent in (%s) and docstatus = 1 and qty > work_order_qty and exists (select name from `tabBOM` bom where %s - and bom.is_active = 1) %s""" % - (", ".join(["%s"] * len(so_list)), - bom_item, - item_condition), - tuple(so_list), as_dict=1) + and bom.is_active = 1) %s""" + % (", ".join(["%s"] * len(so_list)), bom_item, item_condition), + tuple(so_list), + as_dict=1, + ) if self.item_code: - item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) + item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) - packed_items = frappe.db.sql("""select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, + packed_items = frappe.db.sql( + """select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty) as pending_qty, pi.parent_item, pi.description, so_item.name from `tabSales Order Item` so_item, `tabPacked Item` pi @@ -198,16 +211,23 @@ class ProductionPlan(Document): and pi.parent_item = so_item.item_code and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1) + and bom.is_active = 1) %s""" + % (", ".join(["%s"] * len(so_list)), item_condition), + tuple(so_list), + as_dict=1, + ) self.add_items(items + packed_items) self.calculate_total_planned_qty() def get_mr_items(self): # Check for empty table or empty rows - if not self.get("material_requests") or not self.get_so_mr_list("material_request", "material_requests"): - frappe.throw(_("Please fill the Material Requests table"), title=_("Material Requests Required")) + if not self.get("material_requests") or not self.get_so_mr_list( + "material_request", "material_requests" + ): + frappe.throw( + _("Please fill the Material Requests table"), title=_("Material Requests Required") + ) mr_list = self.get_so_mr_list("material_request", "material_requests") @@ -215,13 +235,17 @@ class ProductionPlan(Document): if self.item_code: item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code)) - items = frappe.db.sql("""select distinct parent, name, item_code, warehouse, description, + items = frappe.db.sql( + """select distinct parent, name, item_code, warehouse, description, (qty - ordered_qty) * conversion_factor as pending_qty from `tabMaterial Request Item` mr_item where parent in (%s) and docstatus = 1 and qty > ordered_qty and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1) %s""" % \ - (", ".join(["%s"] * len(mr_list)), item_condition), tuple(mr_list), as_dict=1) + and bom.is_active = 1) %s""" + % (", ".join(["%s"] * len(mr_list)), item_condition), + tuple(mr_list), + as_dict=1, + ) self.add_items(items) self.calculate_total_planned_qty() @@ -232,37 +256,36 @@ class ProductionPlan(Document): item_details = get_item_details(data.item_code) if self.combine_items: if item_details.bom_no in refs: - refs[item_details.bom_no]['so_details'].append({ - 'sales_order': data.parent, - 'sales_order_item': data.name, - 'qty': data.pending_qty - }) - refs[item_details.bom_no]['qty'] += data.pending_qty + refs[item_details.bom_no]["so_details"].append( + {"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty} + ) + refs[item_details.bom_no]["qty"] += data.pending_qty continue else: refs[item_details.bom_no] = { - 'qty': data.pending_qty, - 'po_item_ref': data.name, - 'so_details': [] + "qty": data.pending_qty, + "po_item_ref": data.name, + "so_details": [], } - refs[item_details.bom_no]['so_details'].append({ - 'sales_order': data.parent, - 'sales_order_item': data.name, - 'qty': data.pending_qty - }) + refs[item_details.bom_no]["so_details"].append( + {"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty} + ) - pi = self.append('po_items', { - 'warehouse': data.warehouse, - 'item_code': data.item_code, - 'description': data.description or item_details.description, - 'stock_uom': item_details and item_details.stock_uom or '', - 'bom_no': item_details and item_details.bom_no or '', - 'planned_qty': data.pending_qty, - 'pending_qty': data.pending_qty, - 'planned_start_date': now_datetime(), - 'product_bundle_item': data.parent_item - }) + pi = self.append( + "po_items", + { + "warehouse": data.warehouse, + "item_code": data.item_code, + "description": data.description or item_details.description, + "stock_uom": item_details and item_details.stock_uom or "", + "bom_no": item_details and item_details.bom_no or "", + "planned_qty": data.pending_qty, + "pending_qty": data.pending_qty, + "planned_start_date": now_datetime(), + "product_bundle_item": data.parent_item, + }, + ) pi._set_defaults() if self.get_items_from == "Sales Order": @@ -277,20 +300,23 @@ class ProductionPlan(Document): if refs: for po_item in self.po_items: - po_item.planned_qty = refs[po_item.bom_no]['qty'] - po_item.pending_qty = refs[po_item.bom_no]['qty'] - po_item.sales_order = '' + po_item.planned_qty = refs[po_item.bom_no]["qty"] + po_item.pending_qty = refs[po_item.bom_no]["qty"] + po_item.sales_order = "" self.add_pp_ref(refs) def add_pp_ref(self, refs): for bom_no in refs: - for so_detail in refs[bom_no]['so_details']: - self.append('prod_plan_references', { - 'item_reference': refs[bom_no]['po_item_ref'], - 'sales_order': so_detail['sales_order'], - 'sales_order_item': so_detail['sales_order_item'], - 'qty': so_detail['qty'] - }) + for so_detail in refs[bom_no]["so_details"]: + self.append( + "prod_plan_references", + { + "item_reference": refs[bom_no]["po_item_ref"], + "sales_order": so_detail["sales_order"], + "sales_order_item": so_detail["sales_order_item"], + "qty": so_detail["qty"], + }, + ) def calculate_total_produced_qty(self): self.total_produced_qty = 0 @@ -308,27 +334,24 @@ class ProductionPlan(Document): self.calculate_total_produced_qty() self.set_status() - self.db_set('status', self.status) + self.db_set("status", self.status) def on_cancel(self): - self.db_set('status', 'Cancelled') + self.db_set("status", "Cancelled") self.delete_draft_work_order() def delete_draft_work_order(self): - for d in frappe.get_all('Work Order', fields = ["name"], - filters = {'docstatus': 0, 'production_plan': ("=", self.name)}): - frappe.delete_doc('Work Order', d.name) + for d in frappe.get_all( + "Work Order", fields=["name"], filters={"docstatus": 0, "production_plan": ("=", self.name)} + ): + frappe.delete_doc("Work Order", d.name) @frappe.whitelist() def set_status(self, close=None): - self.status = { - 0: 'Draft', - 1: 'Submitted', - 2: 'Cancelled' - }.get(self.docstatus) + self.status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}.get(self.docstatus) if close: - self.db_set('status', 'Closed') + self.db_set("status", "Closed") return if self.total_produced_qty > 0: @@ -336,12 +359,12 @@ class ProductionPlan(Document): if self.all_items_completed(): self.status = "Completed" - if self.status != 'Completed': + if self.status != "Completed": self.update_ordered_status() self.update_requested_status() if close is not None: - self.db_set('status', self.status) + self.db_set("status", self.status) def update_ordered_status(self): update_status = False @@ -349,8 +372,8 @@ class ProductionPlan(Document): if d.planned_qty == d.ordered_qty: update_status = True - if update_status and self.status != 'Completed': - self.status = 'In Process' + if update_status and self.status != "Completed": + self.status = "In Process" def update_requested_status(self): if not self.mr_items: @@ -362,44 +385,44 @@ class ProductionPlan(Document): update_status = False if update_status: - self.status = 'Material Requested' + self.status = "Material Requested" def get_production_items(self): item_dict = {} for d in self.po_items: item_details = { - "production_item" : d.item_code, - "use_multi_level_bom" : d.include_exploded_items, - "sales_order" : d.sales_order, - "sales_order_item" : d.sales_order_item, - "material_request" : d.material_request, - "material_request_item" : d.material_request_item, - "bom_no" : d.bom_no, - "description" : d.description, - "stock_uom" : d.stock_uom, - "company" : self.company, - "fg_warehouse" : d.warehouse, - "production_plan" : self.name, - "production_plan_item" : d.name, - "product_bundle_item" : d.product_bundle_item, - "planned_start_date" : d.planned_start_date, - "project" : self.project + "production_item": d.item_code, + "use_multi_level_bom": d.include_exploded_items, + "sales_order": d.sales_order, + "sales_order_item": d.sales_order_item, + "material_request": d.material_request, + "material_request_item": d.material_request_item, + "bom_no": d.bom_no, + "description": d.description, + "stock_uom": d.stock_uom, + "company": self.company, + "fg_warehouse": d.warehouse, + "production_plan": self.name, + "production_plan_item": d.name, + "product_bundle_item": d.product_bundle_item, + "planned_start_date": d.planned_start_date, + "project": self.project, } - if not item_details['project'] and d.sales_order: - item_details['project'] = frappe.get_cached_value("Sales Order", d.sales_order, "project") + if not item_details["project"] and d.sales_order: + item_details["project"] = frappe.get_cached_value("Sales Order", d.sales_order, "project") if self.get_items_from == "Material Request": - item_details.update({ - "qty": d.planned_qty - }) + item_details.update({"qty": d.planned_qty}) item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details else: - item_details.update({ - "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse),{}) - .get("qty")) + (flt(d.planned_qty) - flt(d.ordered_qty)) - }) + item_details.update( + { + "qty": flt(item_dict.get((d.item_code, d.sales_order, d.warehouse), {}).get("qty")) + + (flt(d.planned_qty) - flt(d.ordered_qty)) + } + ) item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details return item_dict @@ -415,15 +438,15 @@ class ProductionPlan(Document): self.make_work_order_for_finished_goods(wo_list, default_warehouses) self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) self.make_subcontracted_purchase_order(subcontracted_po, po_list) - self.show_list_created_message('Work Order', wo_list) - self.show_list_created_message('Purchase Order', po_list) + self.show_list_created_message("Work Order", wo_list) + self.show_list_created_message("Purchase Order", po_list) def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() for key, item in items_data.items(): if self.sub_assembly_items: - item['use_multi_level_bom'] = 0 + item["use_multi_level_bom"] = 0 set_default_warehouses(item, default_warehouses) work_order = self.create_work_order(item) @@ -432,13 +455,13 @@ class ProductionPlan(Document): def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): for row in self.sub_assembly_items: - if row.type_of_manufacturing == 'Subcontract': + if row.type_of_manufacturing == "Subcontract": subcontracted_po.setdefault(row.supplier, []).append(row) continue work_order_data = { - 'wip_warehouse': default_warehouses.get('wip_warehouse'), - 'fg_warehouse': default_warehouses.get('fg_warehouse') + "wip_warehouse": default_warehouses.get("wip_warehouse"), + "fg_warehouse": default_warehouses.get("fg_warehouse"), } self.prepare_data_for_sub_assembly_items(row, work_order_data) @@ -447,41 +470,59 @@ class ProductionPlan(Document): wo_list.append(work_order) def prepare_data_for_sub_assembly_items(self, row, wo_data): - for field in ["production_item", "item_name", "qty", "fg_warehouse", - "description", "bom_no", "stock_uom", "bom_level", - "production_plan_item", "schedule_date"]: + for field in [ + "production_item", + "item_name", + "qty", + "fg_warehouse", + "description", + "bom_no", + "stock_uom", + "bom_level", + "production_plan_item", + "schedule_date", + ]: if row.get(field): wo_data[field] = row.get(field) - wo_data.update({ - "use_multi_level_bom": 0, - "production_plan": self.name, - "production_plan_sub_assembly_item": row.name - }) + wo_data.update( + { + "use_multi_level_bom": 0, + "production_plan": self.name, + "production_plan_sub_assembly_item": row.name, + } + ) def make_subcontracted_purchase_order(self, subcontracted_po, purchase_orders): if not subcontracted_po: return for supplier, po_list in subcontracted_po.items(): - po = frappe.new_doc('Purchase Order') + po = frappe.new_doc("Purchase Order") po.supplier = supplier po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() - po.is_subcontracted = 'Yes' + po.is_subcontracted = "Yes" for row in po_list: po_data = { - 'item_code': row.production_item, - 'warehouse': row.fg_warehouse, - 'production_plan_sub_assembly_item': row.name, - 'bom': row.bom_no, - 'production_plan': self.name + "item_code": row.production_item, + "warehouse": row.fg_warehouse, + "production_plan_sub_assembly_item": row.name, + "bom": row.bom_no, + "production_plan": self.name, } - for field in ['schedule_date', 'qty', 'uom', 'stock_uom', 'item_name', - 'description', 'production_plan_item']: + for field in [ + "schedule_date", + "qty", + "uom", + "stock_uom", + "item_name", + "description", + "production_plan_item", + ]: po_data[field] = row.get(field) - po.append('items', po_data) + po.append("items", po_data) po.set_missing_values() po.flags.ignore_mandatory = True @@ -503,7 +544,7 @@ class ProductionPlan(Document): wo = frappe.new_doc("Work Order") wo.update(item) - wo.planned_start_date = item.get('planned_start_date') or item.get('schedule_date') + wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date") if item.get("warehouse"): wo.fg_warehouse = item.get("warehouse") @@ -521,54 +562,60 @@ class ProductionPlan(Document): @frappe.whitelist() def make_material_request(self): - '''Create Material Requests grouped by Sales Order and Material Request Type''' + """Create Material Requests grouped by Sales Order and Material Request Type""" material_request_list = [] material_request_map = {} for item in self.mr_items: - item_doc = frappe.get_cached_doc('Item', item.item_code) + item_doc = frappe.get_cached_doc("Item", item.item_code) material_request_type = item.material_request_type or item_doc.default_material_request_type # key for Sales Order:Material Request Type:Customer - key = '{}:{}:{}'.format(item.sales_order, material_request_type, item_doc.customer or '') + key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "") schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) if not key in material_request_map: # make a new MR for the combination material_request_map[key] = frappe.new_doc("Material Request") material_request = material_request_map[key] - material_request.update({ - "transaction_date": nowdate(), - "status": "Draft", - "company": self.company, - 'material_request_type': material_request_type, - 'customer': item_doc.customer or '' - }) + material_request.update( + { + "transaction_date": nowdate(), + "status": "Draft", + "company": self.company, + "material_request_type": material_request_type, + "customer": item_doc.customer or "", + } + ) material_request_list.append(material_request) else: material_request = material_request_map[key] # add item - material_request.append("items", { - "item_code": item.item_code, - "from_warehouse": item.from_warehouse, - "qty": item.quantity, - "schedule_date": schedule_date, - "warehouse": item.warehouse, - "sales_order": item.sales_order, - 'production_plan': self.name, - 'material_request_plan_item': item.name, - "project": frappe.db.get_value("Sales Order", item.sales_order, "project") \ - if item.sales_order else None - }) + material_request.append( + "items", + { + "item_code": item.item_code, + "from_warehouse": item.from_warehouse, + "qty": item.quantity, + "schedule_date": schedule_date, + "warehouse": item.warehouse, + "sales_order": item.sales_order, + "production_plan": self.name, + "material_request_plan_item": item.name, + "project": frappe.db.get_value("Sales Order", item.sales_order, "project") + if item.sales_order + else None, + }, + ) for material_request in material_request_list: # submit material_request.flags.ignore_permissions = 1 material_request.run_method("set_missing_values") - if self.get('submit_material_request'): + if self.get("submit_material_request"): material_request.submit() else: material_request.save() @@ -576,10 +623,12 @@ class ProductionPlan(Document): frappe.flags.mute_messages = False if material_request_list: - material_request_list = ["""{1}""".format(m.name, m.name) \ - for m in material_request_list] + material_request_list = [ + """{1}""".format(m.name, m.name) + for m in material_request_list + ] msgprint(_("{0} created").format(comma_and(material_request_list))) - else : + else: msgprint(_("No material request created")) @frappe.whitelist() @@ -590,7 +639,7 @@ class ProductionPlan(Document): get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) - self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True) + self.sub_assembly_items.sort(key=lambda d: d.bom_level, reverse=True) for idx, row in enumerate(self.sub_assembly_items, start=1): row.idx = idx @@ -600,14 +649,16 @@ class ProductionPlan(Document): data.production_plan_item = row.name data.fg_warehouse = row.warehouse data.schedule_date = row.planned_start_date - data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item - else "In House") + data.type_of_manufacturing = manufacturing_type or ( + "Subcontract" if data.is_sub_contracted_item else "In House" + ) self.append("sub_assembly_items", data) def all_items_completed(self): - all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 - for d in self.po_items) + all_items_produced = all( + flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 for d in self.po_items + ) if not all_items_produced: return False @@ -624,40 +675,81 @@ class ProductionPlan(Document): all_work_orders_completed = all(s == "Completed" for s in wo_status) return all_work_orders_completed + @frappe.whitelist() def download_raw_materials(doc, warehouses=None): if isinstance(doc, str): doc = frappe._dict(json.loads(doc)) - item_list = [['Item Code', 'Item Name', 'Description', - 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', - 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty', - 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']] + item_list = [ + [ + "Item Code", + "Item Name", + "Description", + "Stock UOM", + "Warehouse", + "Required Qty as per BOM", + "Projected Qty", + "Available Qty In Hand", + "Ordered Qty", + "Planned Qty", + "Reserved Qty for Production", + "Safety Stock", + "Required Qty", + ] + ] doc.warehouse = None frappe.flags.show_qty_in_stock_uom = 1 - items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) + items = get_items_for_material_requests( + doc, warehouses=warehouses, get_parent_warehouse_data=True + ) for d in items: - item_list.append([d.get('item_code'), d.get('item_name'), - d.get('description'), d.get('stock_uom'), d.get('warehouse'), - d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), - d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) + item_list.append( + [ + d.get("item_code"), + d.get("item_name"), + d.get("description"), + d.get("stock_uom"), + d.get("warehouse"), + d.get("required_bom_qty"), + d.get("projected_qty"), + d.get("actual_qty"), + d.get("ordered_qty"), + d.get("planned_qty"), + d.get("reserved_qty_for_production"), + d.get("safety_stock"), + d.get("quantity"), + ] + ) - if not doc.get('for_warehouse'): - row = {'item_code': d.get('item_code')} + if not doc.get("for_warehouse"): + row = {"item_code": d.get("item_code")} for bin_dict in get_bin_details(row, doc.company, all_warehouse=True): - if d.get("warehouse") == bin_dict.get('warehouse'): + if d.get("warehouse") == bin_dict.get("warehouse"): continue - item_list.append(['', '', '', bin_dict.get('warehouse'), '', - bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0), - bin_dict.get('ordered_qty', 0), bin_dict.get('reserved_qty_for_production', 0)]) + item_list.append( + [ + "", + "", + "", + bin_dict.get("warehouse"), + "", + bin_dict.get("projected_qty", 0), + bin_dict.get("actual_qty", 0), + bin_dict.get("ordered_qty", 0), + bin_dict.get("reserved_qty_for_production", 0), + ] + ) build_csv_response(item_list, doc.name) + def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): - for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom, + for d in frappe.db.sql( + """select bei.item_code, item.default_bom as bom, ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, @@ -673,21 +765,38 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p where bei.docstatus < 2 and bom.name=%s and item.is_stock_item in (1, {0}) - group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1), - (planned_qty, company, bom_no), as_dict=1): + group by bei.item_code, bei.stock_uom""".format( + 0 if include_non_stock_items else 1 + ), + (planned_qty, company, bom_no), + as_dict=1, + ): if not d.conversion_factor and d.purchase_uom: d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom) - item_details.setdefault(d.get('item_code'), d) + item_details.setdefault(d.get("item_code"), d) return item_details -def get_uom_conversion_factor(item_code, uom): - return frappe.db.get_value('UOM Conversion Detail', - {'parent': item_code, 'uom': uom}, 'conversion_factor') -def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_items, - include_subcontracted_items, parent_qty, planned_qty=1): - items = frappe.db.sql(""" +def get_uom_conversion_factor(item_code, uom): + return frappe.db.get_value( + "UOM Conversion Detail", {"parent": item_code, "uom": uom}, "conversion_factor" + ) + + +def get_subitems( + doc, + data, + item_details, + bom_no, + company, + include_non_stock_items, + include_subcontracted_items, + parent_qty, + planned_qty=1, +): + items = frappe.db.sql( + """ SELECT bom_item.item_code, default_material_request_type, item.item_name, ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, @@ -707,15 +816,15 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite bom.name = %(bom)s and bom_item.docstatus < 2 and item.is_stock_item in (1, {0}) - group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{ - 'bom': bom_no, - 'parent_qty': parent_qty, - 'planned_qty': planned_qty, - 'company': company - }, as_dict=1) + group by bom_item.item_code""".format( + 0 if include_non_stock_items else 1 + ), + {"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company}, + as_dict=1, + ) for d in items: - if not data.get('include_exploded_items') or not d.default_bom: + if not data.get("include_exploded_items") or not d.default_bom: if d.item_code in item_details: item_details[d.item_code].qty = item_details[d.item_code].qty + d.qty else: @@ -724,89 +833,107 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite item_details[d.item_code] = d - if data.get('include_exploded_items') and d.default_bom: - if ((d.default_material_request_type in ["Manufacture", "Purchase"] and - not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)): + if data.get("include_exploded_items") and d.default_bom: + if ( + d.default_material_request_type in ["Manufacture", "Purchase"] and not d.is_sub_contracted + ) or (d.is_sub_contracted and include_subcontracted_items): if d.qty > 0: - get_subitems(doc, data, item_details, d.default_bom, company, - include_non_stock_items, include_subcontracted_items, d.qty) + get_subitems( + doc, + data, + item_details, + d.default_bom, + company, + include_non_stock_items, + include_subcontracted_items, + d.qty, + ) return item_details -def get_material_request_items(row, sales_order, company, - ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict): - total_qty = row['qty'] + +def get_material_request_items( + row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict +): + total_qty = row["qty"] required_qty = 0 if ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0: required_qty = total_qty elif total_qty > bin_dict.get("projected_qty", 0): required_qty = total_qty - bin_dict.get("projected_qty", 0) - if required_qty > 0 and required_qty < row['min_order_qty']: - required_qty = row['min_order_qty'] + if required_qty > 0 and required_qty < row["min_order_qty"]: + required_qty = row["min_order_qty"] item_group_defaults = get_item_group_defaults(row.item_code, company) - if not row['purchase_uom']: - row['purchase_uom'] = row['stock_uom'] + if not row["purchase_uom"]: + row["purchase_uom"] = row["stock_uom"] - if row['purchase_uom'] != row['stock_uom']: - if not (row['conversion_factor'] or frappe.flags.show_qty_in_stock_uom): - frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}") - .format(row['purchase_uom'], row['stock_uom'], row.item_code)) + if row["purchase_uom"] != row["stock_uom"]: + if not (row["conversion_factor"] or frappe.flags.show_qty_in_stock_uom): + frappe.throw( + _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( + row["purchase_uom"], row["stock_uom"], row.item_code + ) + ) - required_qty = required_qty / row['conversion_factor'] + required_qty = required_qty / row["conversion_factor"] - if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): + if frappe.db.get_value("UOM", row["purchase_uom"], "must_be_whole_number"): required_qty = ceil(required_qty) if include_safety_stock: - required_qty += flt(row['safety_stock']) + required_qty += flt(row["safety_stock"]) if required_qty > 0: return { - 'item_code': row.item_code, - 'item_name': row.item_name, - 'quantity': required_qty, - 'required_bom_qty': total_qty, - 'stock_uom': row.get("stock_uom"), - 'warehouse': warehouse or row.get('source_warehouse') \ - or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), - 'safety_stock': row.safety_stock, - 'actual_qty': bin_dict.get("actual_qty", 0), - 'projected_qty': bin_dict.get("projected_qty", 0), - 'ordered_qty': bin_dict.get("ordered_qty", 0), - 'reserved_qty_for_production': bin_dict.get("reserved_qty_for_production", 0), - 'min_order_qty': row['min_order_qty'], - 'material_request_type': row.get("default_material_request_type"), - 'sales_order': sales_order, - 'description': row.get("description"), - 'uom': row.get("purchase_uom") or row.get("stock_uom") + "item_code": row.item_code, + "item_name": row.item_name, + "quantity": required_qty, + "required_bom_qty": total_qty, + "stock_uom": row.get("stock_uom"), + "warehouse": warehouse + or row.get("source_warehouse") + or row.get("default_warehouse") + or item_group_defaults.get("default_warehouse"), + "safety_stock": row.safety_stock, + "actual_qty": bin_dict.get("actual_qty", 0), + "projected_qty": bin_dict.get("projected_qty", 0), + "ordered_qty": bin_dict.get("ordered_qty", 0), + "reserved_qty_for_production": bin_dict.get("reserved_qty_for_production", 0), + "min_order_qty": row["min_order_qty"], + "material_request_type": row.get("default_material_request_type"), + "sales_order": sales_order, + "description": row.get("description"), + "uom": row.get("purchase_uom") or row.get("stock_uom"), } + def get_sales_orders(self): so_filter = item_filter = "" bom_item = "bom.item = so_item.item_code" date_field_mapper = { - 'from_date': ('>=', 'so.transaction_date'), - 'to_date': ('<=', 'so.transaction_date'), - 'from_delivery_date': ('>=', 'so_item.delivery_date'), - 'to_delivery_date': ('<=', 'so_item.delivery_date') + "from_date": (">=", "so.transaction_date"), + "to_date": ("<=", "so.transaction_date"), + "from_delivery_date": (">=", "so_item.delivery_date"), + "to_delivery_date": ("<=", "so_item.delivery_date"), } for field, value in date_field_mapper.items(): if self.get(field): so_filter += f" and {value[1]} {value[0]} %({field})s" - for field in ['customer', 'project', 'sales_order_status']: + for field in ["customer", "project", "sales_order_status"]: if self.get(field): - so_field = 'status' if field == 'sales_order_status' else field + so_field = "status" if field == "sales_order_status" else field so_filter += f" and so.{so_field} = %({field})s" - if self.item_code and frappe.db.exists('Item', self.item_code): + if self.item_code and frappe.db.exists("Item", self.item_code): bom_item = self.get_bom_item() or bom_item item_filter += " and so_item.item_code = %(item_code)s" - open_so = frappe.db.sql(f""" + open_so = frappe.db.sql( + f""" select distinct so.name, so.transaction_date, so.customer, so.base_grand_total from `tabSales Order` so, `tabSales Order Item` so_item where so_item.parent = so.name @@ -819,10 +946,14 @@ def get_sales_orders(self): where pi.parent = so.name and pi.parent_item = so_item.item_code and exists (select name from `tabBOM` bom where bom.item=pi.item_code and bom.is_active = 1))) - """, self.as_dict(), as_dict=1) + """, + self.as_dict(), + as_dict=1, + ) return open_so + @frappe.whitelist() def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): if isinstance(row, str): @@ -831,30 +962,42 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): company = frappe.db.escape(company) conditions, warehouse = "", "" - conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format(company) + conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format( + company + ) if not all_warehouse: - warehouse = for_warehouse or row.get('source_warehouse') or row.get('default_warehouse') + warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse") if warehouse: lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) conditions = """ and warehouse in (select name from `tabWarehouse` where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2}) - """.format(lft, rgt, company) + """.format( + lft, rgt, company + ) - return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, + return frappe.db.sql( + """ select ifnull(sum(projected_qty),0) as projected_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, ifnull(sum(planned_qty),0) as planned_qty from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse - """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) + """.format( + conditions=conditions + ), + {"item_code": row["item_code"]}, + as_dict=1, + ) + @frappe.whitelist() def get_so_details(sales_order): - return frappe.db.get_value("Sales Order", sales_order, - ['transaction_date', 'customer', 'grand_total'], as_dict=1 + return frappe.db.get_value( + "Sales Order", sales_order, ["transaction_date", "customer", "grand_total"], as_dict=1 ) + def get_warehouse_list(warehouses): warehouse_list = [] @@ -862,7 +1005,7 @@ def get_warehouse_list(warehouses): warehouses = json.loads(warehouses) for row in warehouses: - child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse")) + child_warehouses = frappe.db.get_descendants("Warehouse", row.get("warehouse")) if child_warehouses: warehouse_list.extend(child_warehouses) else: @@ -870,6 +1013,7 @@ def get_warehouse_list(warehouses): return warehouse_list + @frappe.whitelist() def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): if isinstance(doc, str): @@ -878,73 +1022,92 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d if warehouses: warehouses = list(set(get_warehouse_list(warehouses))) - if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: + if ( + doc.get("for_warehouse") + and not get_parent_warehouse_data + and doc.get("for_warehouse") in warehouses + ): warehouses.remove(doc.get("for_warehouse")) - doc['mr_items'] = [] + doc["mr_items"] = [] - po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') + po_items = doc.get("po_items") if doc.get("po_items") else doc.get("items") # Check for empty table or empty rows - if not po_items or not [row.get('item_code') for row in po_items if row.get('item_code')]: - frappe.throw(_("Items to Manufacture are required to pull the Raw Materials associated with it."), - title=_("Items Required")) + if not po_items or not [row.get("item_code") for row in po_items if row.get("item_code")]: + frappe.throw( + _("Items to Manufacture are required to pull the Raw Materials associated with it."), + title=_("Items Required"), + ) - company = doc.get('company') - ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') - include_safety_stock = doc.get('include_safety_stock') + company = doc.get("company") + ignore_existing_ordered_qty = doc.get("ignore_existing_ordered_qty") + include_safety_stock = doc.get("include_safety_stock") so_item_details = frappe._dict() for data in po_items: if not data.get("include_exploded_items") and doc.get("sub_assembly_items"): data["include_exploded_items"] = 1 - planned_qty = data.get('required_qty') or data.get('planned_qty') - ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty - warehouse = doc.get('for_warehouse') + planned_qty = data.get("required_qty") or data.get("planned_qty") + ignore_existing_ordered_qty = ( + data.get("ignore_existing_ordered_qty") or ignore_existing_ordered_qty + ) + warehouse = doc.get("for_warehouse") item_details = {} if data.get("bom") or data.get("bom_no"): - if data.get('required_qty'): - bom_no = data.get('bom') + if data.get("required_qty"): + bom_no = data.get("bom") include_non_stock_items = 1 - include_subcontracted_items = 1 if data.get('include_exploded_items') else 0 + include_subcontracted_items = 1 if data.get("include_exploded_items") else 0 else: - bom_no = data.get('bom_no') - include_subcontracted_items = doc.get('include_subcontracted_items') - include_non_stock_items = doc.get('include_non_stock_items') + bom_no = data.get("bom_no") + include_subcontracted_items = doc.get("include_subcontracted_items") + include_non_stock_items = doc.get("include_non_stock_items") if not planned_qty: - frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx'))) + frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx"))) if bom_no: - if data.get('include_exploded_items') and include_subcontracted_items: + if data.get("include_exploded_items") and include_subcontracted_items: # fetch exploded items from BOM - item_details = get_exploded_items(item_details, - company, bom_no, include_non_stock_items, planned_qty=planned_qty) + item_details = get_exploded_items( + item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty + ) else: - item_details = get_subitems(doc, data, item_details, bom_no, company, - include_non_stock_items, include_subcontracted_items, 1, planned_qty=planned_qty) - elif data.get('item_code'): - item_master = frappe.get_doc('Item', data['item_code']).as_dict() + item_details = get_subitems( + doc, + data, + item_details, + bom_no, + company, + include_non_stock_items, + include_subcontracted_items, + 1, + planned_qty=planned_qty, + ) + elif data.get("item_code"): + item_master = frappe.get_doc("Item", data["item_code"]).as_dict() purchase_uom = item_master.purchase_uom or item_master.stock_uom - conversion_factor = (get_uom_conversion_factor(item_master.name, purchase_uom) - if item_master.purchase_uom else 1.0) + conversion_factor = ( + get_uom_conversion_factor(item_master.name, purchase_uom) if item_master.purchase_uom else 1.0 + ) item_details[item_master.name] = frappe._dict( { - 'item_name' : item_master.item_name, - 'default_bom' : doc.bom, - 'purchase_uom' : purchase_uom, - 'default_warehouse': item_master.default_warehouse, - 'min_order_qty' : item_master.min_order_qty, - 'default_material_request_type' : item_master.default_material_request_type, - 'qty': planned_qty or 1, - 'is_sub_contracted' : item_master.is_subcontracted_item, - 'item_code' : item_master.name, - 'description' : item_master.description, - 'stock_uom' : item_master.stock_uom, - 'conversion_factor' : conversion_factor, - 'safety_stock': item_master.safety_stock + "item_name": item_master.item_name, + "default_bom": doc.bom, + "purchase_uom": purchase_uom, + "default_warehouse": item_master.default_warehouse, + "min_order_qty": item_master.min_order_qty, + "default_material_request_type": item_master.default_material_request_type, + "qty": planned_qty or 1, + "is_sub_contracted": item_master.is_subcontracted_item, + "item_code": item_master.name, + "description": item_master.description, + "stock_uom": item_master.stock_uom, + "conversion_factor": conversion_factor, + "safety_stock": item_master.safety_stock, } ) @@ -953,7 +1116,9 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d for item_code, details in iteritems(item_details): so_item_details.setdefault(sales_order, frappe._dict()) if item_code in so_item_details.get(sales_order, {}): - so_item_details[sales_order][item_code]['qty'] = so_item_details[sales_order][item_code].get("qty", 0) + flt(details.qty) + so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get( + "qty", 0 + ) + flt(details.qty) else: so_item_details[sales_order][item_code] = details @@ -965,8 +1130,15 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d bin_dict = bin_dict[0] if bin_dict else {} if details.qty > 0: - items = get_material_request_items(details, sales_order, company, - ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict) + items = get_material_request_items( + details, + sales_order, + company, + ignore_existing_ordered_qty, + include_safety_stock, + warehouse, + bin_dict, + ) if items: mr_items.append(items) @@ -979,18 +1151,26 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d if not mr_items: to_enable = frappe.bold(_("Ignore Existing Projected Quantity")) - warehouse = frappe.bold(doc.get('for_warehouse')) - message = _("As there are sufficient raw materials, Material Request is not required for Warehouse {0}.").format(warehouse) + "

    " + warehouse = frappe.bold(doc.get("for_warehouse")) + message = ( + _( + "As there are sufficient raw materials, Material Request is not required for Warehouse {0}." + ).format(warehouse) + + "

    " + ) message += _("If you still want to proceed, please enable {0}.").format(to_enable) frappe.msgprint(message, title=_("Note")) return mr_items + def get_materials_from_other_locations(item, warehouses, new_mr_items, company): from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations - locations = get_available_item_locations(item.get("item_code"), - warehouses, item.get("quantity"), company, ignore_validation=True) + + locations = get_available_item_locations( + item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True + ) required_qty = item.get("quantity") # get available material by transferring to production warehouse @@ -1001,12 +1181,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") - new_dict.update({ - "quantity": quantity, - "material_request_type": "Material Transfer", - "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM - "from_warehouse": d.get("warehouse") - }) + new_dict.update( + { + "quantity": quantity, + "material_request_type": "Material Transfer", + "uom": new_dict.get("stock_uom"), # internal transfer should be in stock UOM + "from_warehouse": d.get("warehouse"), + } + ) required_qty -= quantity new_mr_items.append(new_dict) @@ -1014,16 +1196,17 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): # raise purchase request for remaining qty if required_qty: stock_uom, purchase_uom = frappe.db.get_value( - 'Item', - item['item_code'], - ['stock_uom', 'purchase_uom'] + "Item", item["item_code"], ["stock_uom", "purchase_uom"] ) - if purchase_uom != stock_uom and purchase_uom == item['uom']: - conversion_factor = get_uom_conversion_factor(item['item_code'], item['uom']) + if purchase_uom != stock_uom and purchase_uom == item["uom"]: + conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) if not (conversion_factor or frappe.flags.show_qty_in_stock_uom): - frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}") - .format(purchase_uom, stock_uom, item['item_code'])) + frappe.throw( + _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( + purchase_uom, stock_uom, item["item_code"] + ) + ) required_qty = required_qty / conversion_factor @@ -1034,6 +1217,7 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): new_mr_items.append(item) + @frappe.whitelist() def get_item_data(item_code): item_details = get_item_details(item_code) @@ -1041,33 +1225,39 @@ def get_item_data(item_code): return { "bom_no": item_details.get("bom_no"), "stock_uom": item_details.get("stock_uom") -# "description": item_details.get("description") + # "description": item_details.get("description") } + def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): - data = get_children('BOM', parent = bom_no) + data = get_children("BOM", parent=bom_no) for d in data: if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) - bom_data.append(frappe._dict({ - 'parent_item_code': parent_item_code, - 'description': d.description, - 'production_item': d.item_code, - 'item_name': d.item_name, - 'stock_uom': d.stock_uom, - 'uom': d.stock_uom, - 'bom_no': d.value, - 'is_sub_contracted_item': d.is_sub_contracted_item, - 'bom_level': indent, - 'indent': indent, - 'stock_qty': stock_qty - })) + bom_data.append( + frappe._dict( + { + "parent_item_code": parent_item_code, + "description": d.description, + "production_item": d.item_code, + "item_name": d.item_name, + "stock_uom": d.stock_uom, + "uom": d.stock_uom, + "bom_no": d.value, + "is_sub_contracted_item": d.is_sub_contracted_item, + "bom_level": indent, + "indent": indent, + "stock_qty": stock_qty, + } + ) + ) if d.value: - get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent+1) + get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1) + def set_default_warehouses(row, default_warehouses): - for field in ['wip_warehouse', 'fg_warehouse']: + for field in ["wip_warehouse", "fg_warehouse"]: if not row.get(field): row[field] = default_warehouses.get(field) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py index ef009765f92..6fc28a30971 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'production_plan', - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Work Order', 'Material Request'] - }, - { - 'label': _('Subcontract'), - 'items': ['Purchase Order'] - }, - ] + "fieldname": "production_plan", + "transactions": [ + {"label": _("Transactions"), "items": ["Work Order", "Material Request"]}, + {"label": _("Subcontract"), "items": ["Purchase Order"]}, + ], } diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index dae16e4bd30..e70f997a53c 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -21,77 +21,85 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import class TestProductionPlan(FrappeTestCase): def setUp(self): - for item in ['Test Production Item 1', 'Subassembly Item 1', - 'Raw Material Item 1', 'Raw Material Item 2']: + for item in [ + "Test Production Item 1", + "Subassembly Item 1", + "Raw Material Item 1", + "Raw Material Item 2", + ]: create_item(item, valuation_rate=100) - sr = frappe.db.get_value('Stock Reconciliation Item', - {'item_code': item, 'docstatus': 1}, 'parent') + sr = frappe.db.get_value( + "Stock Reconciliation Item", {"item_code": item, "docstatus": 1}, "parent" + ) if sr: - sr_doc = frappe.get_doc('Stock Reconciliation', sr) + sr_doc = frappe.get_doc("Stock Reconciliation", sr) sr_doc.cancel() - create_item('Test Non Stock Raw Material', is_stock_item=0) - for item, raw_materials in {'Subassembly Item 1': ['Raw Material Item 1', 'Raw Material Item 2'], - 'Test Production Item 1': ['Raw Material Item 1', 'Subassembly Item 1', - 'Test Non Stock Raw Material']}.items(): - if not frappe.db.get_value('BOM', {'item': item}): - make_bom(item = item, raw_materials = raw_materials) + create_item("Test Non Stock Raw Material", is_stock_item=0) + for item, raw_materials in { + "Subassembly Item 1": ["Raw Material Item 1", "Raw Material Item 2"], + "Test Production Item 1": [ + "Raw Material Item 1", + "Subassembly Item 1", + "Test Non Stock Raw Material", + ], + }.items(): + if not frappe.db.get_value("BOM", {"item": item}): + make_bom(item=item, raw_materials=raw_materials) def test_production_plan_mr_creation(self): "Test if MRs are created for unavailable raw materials." - pln = create_production_plan(item_code='Test Production Item 1') + pln = create_production_plan(item_code="Test Production Item 1") self.assertTrue(len(pln.mr_items), 2) pln.make_material_request() pln.reload() - self.assertTrue(pln.status, 'Material Requested') + self.assertTrue(pln.status, "Material Requested") material_requests = frappe.get_all( - 'Material Request Item', - fields = ['distinct parent'], - filters = {'production_plan': pln.name}, - as_list=1 + "Material Request Item", + fields=["distinct parent"], + filters={"production_plan": pln.name}, + as_list=1, ) self.assertTrue(len(material_requests), 2) pln.make_work_order() - work_orders = frappe.get_all('Work Order', fields = ['name'], - filters = {'production_plan': pln.name}, as_list=1) + work_orders = frappe.get_all( + "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 + ) self.assertTrue(len(work_orders), len(pln.po_items)) for name in material_requests: - mr = frappe.get_doc('Material Request', name[0]) + mr = frappe.get_doc("Material Request", name[0]) if mr.docstatus != 0: mr.cancel() for name in work_orders: - mr = frappe.delete_doc('Work Order', name[0]) + mr = frappe.delete_doc("Work Order", name[0]) - pln = frappe.get_doc('Production Plan', pln.name) + pln = frappe.get_doc("Production Plan", pln.name) pln.cancel() def test_production_plan_start_date(self): "Test if Work Order has same Planned Start Date as Prod Plan." planned_date = add_to_date(date=None, days=3) plan = create_production_plan( - item_code='Test Production Item 1', - planned_start_date=planned_date + item_code="Test Production Item 1", planned_start_date=planned_date ) plan.make_work_order() work_orders = frappe.get_all( - 'Work Order', - fields = ['name', 'planned_start_date'], - filters = {'production_plan': plan.name} + "Work Order", fields=["name", "planned_start_date"], filters={"production_plan": plan.name} ) self.assertEqual(work_orders[0].planned_start_date, planned_date) for wo in work_orders: - frappe.delete_doc('Work Order', wo.name) + frappe.delete_doc("Work Order", wo.name) plan.reload() plan.cancel() @@ -101,15 +109,14 @@ class TestProductionPlan(FrappeTestCase): - Enable 'ignore_existing_ordered_qty'. - Test if MR Planning table pulls Raw Material Qty even if it is in stock. """ - sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", qty=1, rate=110) - sr2 = create_stock_reconciliation(item_code="Raw Material Item 2", - target="_Test Warehouse - _TC", qty=1, rate=120) - - pln = create_production_plan( - item_code='Test Production Item 1', - ignore_existing_ordered_qty=1 + sr1 = create_stock_reconciliation( + item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=110 ) + sr2 = create_stock_reconciliation( + item_code="Raw Material Item 2", target="_Test Warehouse - _TC", qty=1, rate=120 + ) + + pln = create_production_plan(item_code="Test Production Item 1", ignore_existing_ordered_qty=1) self.assertTrue(len(pln.mr_items), 1) self.assertTrue(flt(pln.mr_items[0].quantity), 1.0) @@ -119,19 +126,13 @@ class TestProductionPlan(FrappeTestCase): def test_production_plan_with_non_stock_item(self): "Test if MR Planning table includes Non Stock RM." - pln = create_production_plan( - item_code='Test Production Item 1', - include_non_stock_items=1 - ) + pln = create_production_plan(item_code="Test Production Item 1", include_non_stock_items=1) self.assertTrue(len(pln.mr_items), 3) pln.cancel() def test_production_plan_without_multi_level(self): "Test MR Planning table for non exploded BOM." - pln = create_production_plan( - item_code='Test Production Item 1', - use_multi_level_bom=0 - ) + pln = create_production_plan(item_code="Test Production Item 1", use_multi_level_bom=0) self.assertTrue(len(pln.mr_items), 2) pln.cancel() @@ -141,15 +142,15 @@ class TestProductionPlan(FrappeTestCase): - Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for non exploded BOM. """ - sr1 = create_stock_reconciliation(item_code="Raw Material Item 1", - target="_Test Warehouse - _TC", qty=1, rate=130) - sr2 = create_stock_reconciliation(item_code="Subassembly Item 1", - target="_Test Warehouse - _TC", qty=1, rate=140) + sr1 = create_stock_reconciliation( + item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=130 + ) + sr2 = create_stock_reconciliation( + item_code="Subassembly Item 1", target="_Test Warehouse - _TC", qty=1, rate=140 + ) pln = create_production_plan( - item_code='Test Production Item 1', - use_multi_level_bom=0, - ignore_existing_ordered_qty=0 + item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=0 ) self.assertTrue(len(pln.mr_items), 0) @@ -159,73 +160,86 @@ class TestProductionPlan(FrappeTestCase): def test_production_plan_sales_orders(self): "Test if previously fulfilled SO (with WO) is pulled into Prod Plan." - item = 'Test Production Item 1' + item = "Test Production Item 1" so = make_sales_order(item_code=item, qty=1) sales_order = so.name sales_order_item = so.items[0].name - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = so.company - pln.get_items_from = 'Sales Order' + pln.get_items_from = "Sales Order" - pln.append('sales_orders', { - 'sales_order': so.name, - 'sales_order_date': so.transaction_date, - 'customer': so.customer, - 'grand_total': so.grand_total - }) + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) pln.get_so_items() pln.submit() pln.make_work_order() - work_order = frappe.db.get_value('Work Order', {'sales_order': sales_order, - 'production_plan': pln.name, 'sales_order_item': sales_order_item}, 'name') + work_order = frappe.db.get_value( + "Work Order", + {"sales_order": sales_order, "production_plan": pln.name, "sales_order_item": sales_order_item}, + "name", + ) - wo_doc = frappe.get_doc('Work Order', work_order) - wo_doc.update({ - 'wip_warehouse': 'Work In Progress - _TC', - 'fg_warehouse': 'Finished Goods - _TC' - }) + wo_doc = frappe.get_doc("Work Order", work_order) + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) wo_doc.submit() - so_wo_qty = frappe.db.get_value('Sales Order Item', sales_order_item, 'work_order_qty') + so_wo_qty = frappe.db.get_value("Sales Order Item", sales_order_item, "work_order_qty") self.assertTrue(so_wo_qty, 5) - pln = frappe.new_doc('Production Plan') - pln.update({ - 'from_date': so.transaction_date, - 'to_date': so.transaction_date, - 'customer': so.customer, - 'item_code': item, - 'sales_order_status': so.status - }) + pln = frappe.new_doc("Production Plan") + pln.update( + { + "from_date": so.transaction_date, + "to_date": so.transaction_date, + "customer": so.customer, + "item_code": item, + "sales_order_status": so.status, + } + ) sales_orders = get_sales_orders(pln) or {} - sales_orders = [d.get('name') for d in sales_orders if d.get('name') == sales_order] + sales_orders = [d.get("name") for d in sales_orders if d.get("name") == sales_order] self.assertEqual(sales_orders, []) def test_production_plan_combine_items(self): "Test combining FG items in Production Plan." - item = 'Test Production Item 1' + item = "Test Production Item 1" so1 = make_sales_order(item_code=item, qty=1) - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = so1.company - pln.get_items_from = 'Sales Order' - pln.append('sales_orders', { - 'sales_order': so1.name, - 'sales_order_date': so1.transaction_date, - 'customer': so1.customer, - 'grand_total': so1.grand_total - }) + pln.get_items_from = "Sales Order" + pln.append( + "sales_orders", + { + "sales_order": so1.name, + "sales_order_date": so1.transaction_date, + "customer": so1.customer, + "grand_total": so1.grand_total, + }, + ) so2 = make_sales_order(item_code=item, qty=2) - pln.append('sales_orders', { - 'sales_order': so2.name, - 'sales_order_date': so2.transaction_date, - 'customer': so2.customer, - 'grand_total': so2.grand_total - }) + pln.append( + "sales_orders", + { + "sales_order": so2.name, + "sales_order_date": so2.transaction_date, + "customer": so2.customer, + "grand_total": so2.grand_total, + }, + ) pln.combine_items = 1 pln.get_items() pln.submit() @@ -233,51 +247,60 @@ class TestProductionPlan(FrappeTestCase): self.assertTrue(pln.po_items[0].planned_qty, 3) pln.make_work_order() - work_order = frappe.db.get_value('Work Order', { - 'production_plan_item': pln.po_items[0].name, - 'production_plan': pln.name - }, 'name') + work_order = frappe.db.get_value( + "Work Order", + {"production_plan_item": pln.po_items[0].name, "production_plan": pln.name}, + "name", + ) - wo_doc = frappe.get_doc('Work Order', work_order) - wo_doc.update({ - 'wip_warehouse': 'Work In Progress - _TC', - }) + wo_doc = frappe.get_doc("Work Order", work_order) + wo_doc.update( + { + "wip_warehouse": "Work In Progress - _TC", + } + ) wo_doc.submit() so_items = [] for plan_reference in pln.prod_plan_references: so_items.append(plan_reference.sales_order_item) - so_wo_qty = frappe.db.get_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty') + so_wo_qty = frappe.db.get_value( + "Sales Order Item", plan_reference.sales_order_item, "work_order_qty" + ) self.assertEqual(so_wo_qty, plan_reference.qty) wo_doc.cancel() for so_item in so_items: - so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty') + so_wo_qty = frappe.db.get_value("Sales Order Item", so_item, "work_order_qty") self.assertEqual(so_wo_qty, 0.0) pln.reload() pln.cancel() def test_pp_to_mr_customer_provided(self): - " Test Material Request from Production Plan for Customer Provided Item." - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) - create_item('Production Item CUST') + "Test Material Request from Production Plan for Customer Provided Item." + create_item( + "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) + create_item("Production Item CUST") - for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items(): - if not frappe.db.get_value('BOM', {'item': item}): - make_bom(item = item, raw_materials = raw_materials) - production_plan = create_production_plan(item_code = 'Production Item CUST') + for item, raw_materials in { + "Production Item CUST": ["Raw Material Item 1", "CUST-0987"] + }.items(): + if not frappe.db.get_value("BOM", {"item": item}): + make_bom(item=item, raw_materials=raw_materials) + production_plan = create_production_plan(item_code="Production Item CUST") production_plan.make_material_request() material_request = frappe.db.get_value( - 'Material Request Item', - {'production_plan': production_plan.name, 'item_code': 'CUST-0987'}, - 'parent' + "Material Request Item", + {"production_plan": production_plan.name, "item_code": "CUST-0987"}, + "parent", ) - mr = frappe.get_doc('Material Request', material_request) + mr = frappe.get_doc("Material Request", material_request) - self.assertTrue(mr.material_request_type, 'Customer Provided') - self.assertTrue(mr.customer, '_Test Customer') + self.assertTrue(mr.material_request_type, "Customer Provided") + self.assertTrue(mr.customer, "_Test Customer") def test_production_plan_with_multi_level_bom(self): """ @@ -291,33 +314,34 @@ class TestProductionPlan(FrappeTestCase): create_item(item_code, is_stock_item=1) # created bom upto 3 level - if not frappe.db.get_value('BOM', {'item': "Test BOM 3"}): - make_bom(item = "Test BOM 3", raw_materials = ["Test RM BOM 1"], rm_qty=3) + if not frappe.db.get_value("BOM", {"item": "Test BOM 3"}): + make_bom(item="Test BOM 3", raw_materials=["Test RM BOM 1"], rm_qty=3) - if not frappe.db.get_value('BOM', {'item': "Test BOM 2"}): - make_bom(item = "Test BOM 2", raw_materials = ["Test BOM 3"], rm_qty=3) + if not frappe.db.get_value("BOM", {"item": "Test BOM 2"}): + make_bom(item="Test BOM 2", raw_materials=["Test BOM 3"], rm_qty=3) - if not frappe.db.get_value('BOM', {'item': "Test BOM 1"}): - make_bom(item = "Test BOM 1", raw_materials = ["Test BOM 2"], rm_qty=2) + if not frappe.db.get_value("BOM", {"item": "Test BOM 1"}): + make_bom(item="Test BOM 1", raw_materials=["Test BOM 2"], rm_qty=2) item_code = "Test BOM 1" - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = "_Test Company" - pln.append("po_items", { - "item_code": item_code, - "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}), - "planned_qty": 3 - }) + pln.append( + "po_items", + { + "item_code": item_code, + "bom_no": frappe.db.get_value("BOM", {"item": "Test BOM 1"}), + "planned_qty": 3, + }, + ) - pln.get_sub_assembly_items('In House') + pln.get_sub_assembly_items("In House") pln.submit() pln.make_work_order() - #last level sub-assembly work order produce qty + # last level sub-assembly work order produce qty to_produce_qty = frappe.db.get_value( - "Work Order", - {"production_plan": pln.name, "production_item": "Test BOM 3"}, - "qty" + "Work Order", {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty" ) self.assertEqual(to_produce_qty, 18.0) @@ -326,70 +350,72 @@ class TestProductionPlan(FrappeTestCase): def test_get_warehouse_list_group(self): "Check if required child warehouses are returned." - warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]' + warehouse_json = '[{"warehouse":"_Test Warehouse Group - _TC"}]' warehouses = set(get_warehouse_list(warehouse_json)) expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"} missing_warehouse = expected_warehouses - warehouses - self.assertTrue(len(missing_warehouse) == 0, - msg=f"Following warehouses were expected {', '.join(missing_warehouse)}") + self.assertTrue( + len(missing_warehouse) == 0, + msg=f"Following warehouses were expected {', '.join(missing_warehouse)}", + ) def test_get_warehouse_list_single(self): "Check if same warehouse is returned in absence of child warehouses." - warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]' + warehouse_json = '[{"warehouse":"_Test Scrap Warehouse - _TC"}]' warehouses = set(get_warehouse_list(warehouse_json)) - expected_warehouses = {"_Test Scrap Warehouse - _TC", } + expected_warehouses = { + "_Test Scrap Warehouse - _TC", + } self.assertEqual(warehouses, expected_warehouses) def test_get_sales_order_with_variant(self): "Check if Template BOM is fetched in absence of Variant BOM." - rm_item = create_item('PIV_RM', valuation_rate = 100) - if not frappe.db.exists('Item', {"item_code": 'PIV'}): - item = create_item('PIV', valuation_rate = 100) + rm_item = create_item("PIV_RM", valuation_rate=100) + if not frappe.db.exists("Item", {"item_code": "PIV"}): + item = create_item("PIV", valuation_rate=100) variant_settings = { "attributes": [ - { - "attribute": "Colour" - }, + {"attribute": "Colour"}, ], - "has_variants": 1 + "has_variants": 1, } item.update(variant_settings) item.save() - parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code]) - if not frappe.db.exists('BOM', {"item": 'PIV'}): - parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code]) + parent_bom = make_bom(item="PIV", raw_materials=[rm_item.item_code]) + if not frappe.db.exists("BOM", {"item": "PIV"}): + parent_bom = make_bom(item="PIV", raw_materials=[rm_item.item_code]) else: - parent_bom = frappe.get_doc('BOM', {"item": 'PIV'}) + parent_bom = frappe.get_doc("BOM", {"item": "PIV"}) - if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}): + if not frappe.db.exists("Item", {"item_code": "PIV-RED"}): variant = create_variant("PIV", {"Colour": "Red"}) variant.save() - variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code]) + variant_bom = make_bom(item=variant.item_code, raw_materials=[rm_item.item_code]) else: - variant = frappe.get_doc('Item', 'PIV-RED') - if not frappe.db.exists('BOM', {"item": 'PIV-RED'}): - variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code]) + variant = frappe.get_doc("Item", "PIV-RED") + if not frappe.db.exists("BOM", {"item": "PIV-RED"}): + variant_bom = make_bom(item=variant.item_code, raw_materials=[rm_item.item_code]) """Testing when item variant has a BOM""" so = make_sales_order(item_code="PIV-RED", qty=5) - pln = frappe.new_doc('Production Plan') + pln = frappe.new_doc("Production Plan") pln.company = so.company - pln.get_items_from = 'Sales Order' - pln.item_code = 'PIV-RED' + pln.get_items_from = "Sales Order" + pln.item_code = "PIV-RED" pln.get_open_sales_orders() self.assertEqual(pln.sales_orders[0].sales_order, so.name) pln.get_so_items() - self.assertEqual(pln.po_items[0].item_code, 'PIV-RED') + self.assertEqual(pln.po_items[0].item_code, "PIV-RED") self.assertEqual(pln.po_items[0].bom_no, variant_bom.name) so.cancel() - frappe.delete_doc('Sales Order', so.name) + frappe.delete_doc("Sales Order", so.name) variant_bom.cancel() - frappe.delete_doc('BOM', variant_bom.name) + frappe.delete_doc("BOM", variant_bom.name) """Testing when item variant doesn't have a BOM""" so = make_sales_order(item_code="PIV-RED", qty=5) @@ -397,7 +423,7 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(pln.sales_orders[0].sales_order, so.name) pln.po_items = [] pln.get_so_items() - self.assertEqual(pln.po_items[0].item_code, 'PIV-RED') + self.assertEqual(pln.po_items[0].item_code, "PIV-RED") self.assertEqual(pln.po_items[0].bom_no, parent_bom.name) frappe.db.rollback() @@ -409,27 +435,35 @@ class TestProductionPlan(FrappeTestCase): prefix = "_TestLevel_" boms = { "Assembly": { - "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly1": { + "ChildPart1": {}, + "ChildPart2": {}, + }, "ChildPart6": {}, "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, }, "MegaDeepAssy": { - "SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},}, - # ^ assert that this is - # first item in subassy table - } + "SecretSubassy": { + "SecretPart": {"VerySecret": {"SuperSecret": {"Classified": {}}}}, + }, + # ^ assert that this is + # first item in subassy table + }, } create_nested_bom(boms, prefix=prefix) items = [prefix + item_code for item_code in boms.keys()] plan = create_production_plan(item_code=items[0], do_not_save=True) - plan.append("po_items", { - 'use_multi_level_bom': 1, - 'item_code': items[1], - 'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'), - 'planned_qty': 1, - 'planned_start_date': now_datetime() - }) + plan.append( + "po_items", + { + "use_multi_level_bom": 1, + "item_code": items[1], + "bom_no": frappe.db.get_value("Item", items[1], "default_bom"), + "planned_qty": 1, + "planned_start_date": now_datetime(), + }, + ) plan.get_sub_assembly_items() bom_level_order = [d.bom_level for d in plan.sub_assembly_items] @@ -439,6 +473,7 @@ class TestProductionPlan(FrappeTestCase): def test_multiple_work_order_for_production_plan_item(self): "Test producing Prod Plan (making WO) in parts." + def create_work_order(item, pln, qty): # Get Production Items items_data = pln.get_production_items() @@ -449,14 +484,13 @@ class TestProductionPlan(FrappeTestCase): # Create and Submit Work Order for each item in items_data for key, item in items_data.items(): if pln.sub_assembly_items: - item['use_multi_level_bom'] = 0 + item["use_multi_level_bom"] = 0 wo_name = pln.create_work_order(item) wo_doc = frappe.get_doc("Work Order", wo_name) - wo_doc.update({ - 'wip_warehouse': 'Work In Progress - _TC', - 'fg_warehouse': 'Finished Goods - _TC' - }) + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) wo_doc.submit() wo_list.append(wo_name) @@ -506,33 +540,29 @@ class TestProductionPlan(FrappeTestCase): make_stock_entry as make_se_from_wo, ) - make_stock_entry(item_code="Raw Material Item 1", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - make_stock_entry(item_code="Raw Material Item 2", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - item = 'Test Production Item 1' + item = "Test Production Item 1" so = make_sales_order(item_code=item, qty=1) pln = create_production_plan( - company=so.company, - get_items_from="Sales Order", - sales_order=so, - skip_getting_mr_items=True + company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True ) self.assertEqual(pln.po_items[0].pending_qty, 1) wo = make_wo_order_test_record( - item_code=item, qty=1, + item_code=item, + qty=1, company=so.company, - wip_warehouse='Work In Progress - _TC', - fg_warehouse='Finished Goods - _TC', + wip_warehouse="Work In Progress - _TC", + fg_warehouse="Finished Goods - _TC", skip_transfer=1, - do_not_submit=True + do_not_submit=True, ) wo.production_plan = pln.name wo.production_plan_item = pln.po_items[0].name @@ -555,28 +585,24 @@ class TestProductionPlan(FrappeTestCase): make_stock_entry as make_se_from_wo, ) - make_stock_entry(item_code="Raw Material Item 1", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - make_stock_entry(item_code="Raw Material Item 2", - target="Work In Progress - _TC", - qty=2, basic_rate=100 + make_stock_entry( + item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100 ) - pln = create_production_plan( - item_code='Test Production Item 1', - skip_getting_mr_items=True - ) + pln = create_production_plan(item_code="Test Production Item 1", skip_getting_mr_items=True) self.assertEqual(pln.po_items[0].pending_qty, 1) wo = make_wo_order_test_record( - item_code='Test Production Item 1', qty=1, + item_code="Test Production Item 1", + qty=1, company=pln.company, - wip_warehouse='Work In Progress - _TC', - fg_warehouse='Finished Goods - _TC', + wip_warehouse="Work In Progress - _TC", + fg_warehouse="Finished Goods - _TC", skip_transfer=1, - do_not_submit=True + do_not_submit=True, ) wo.production_plan = pln.name wo.production_plan_item = pln.po_items[0].name @@ -594,26 +620,23 @@ class TestProductionPlan(FrappeTestCase): def test_qty_based_status(self): pp = frappe.new_doc("Production Plan") - pp.po_items = [ - frappe._dict(planned_qty=5, produce_qty=4) - ] + pp.po_items = [frappe._dict(planned_qty=5, produce_qty=4)] self.assertFalse(pp.all_items_completed()) pp.po_items = [ frappe._dict(planned_qty=5, produce_qty=10), - frappe._dict(planned_qty=5, produce_qty=4) + frappe._dict(planned_qty=5, produce_qty=4), ] self.assertFalse(pp.all_items_completed()) def test_production_plan_planned_qty(self): pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55) pln.make_work_order() - work_order = frappe.db.get_value('Work Order', {'production_plan': pln.name}, 'name') - wo_doc = frappe.get_doc('Work Order', work_order) - wo_doc.update({ - 'wip_warehouse': 'Work In Progress - _TC', - 'fg_warehouse': 'Finished Goods - _TC' - }) + work_order = frappe.db.get_value("Work Order", {"production_plan": pln.name}, "name") + wo_doc = frappe.get_doc("Work Order", work_order) + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) wo_doc.submit() self.assertEqual(wo_doc.qty, 0.55) @@ -624,22 +647,21 @@ class TestProductionPlan(FrappeTestCase): # this can not be unittested so mocking data that would be expected # from client side. for _ in range(10): - po_item = pp.append("po_items", { - "name": frappe.generate_hash(length=10), - "temporary_name": frappe.generate_hash(length=10), - }) - pp.append("sub_assembly_items", { - "production_plan_item": po_item.temporary_name - }) + po_item = pp.append( + "po_items", + { + "name": frappe.generate_hash(length=10), + "temporary_name": frappe.generate_hash(length=10), + }, + ) + pp.append("sub_assembly_items", {"production_plan_item": po_item.temporary_name}) pp._rename_temporary_references() for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): self.assertEqual(po_item.name, subassy_item.production_plan_item) # bad links should be erased - pp.append("sub_assembly_items", { - "production_plan_item": frappe.generate_hash(length=16) - }) + pp.append("sub_assembly_items", {"production_plan_item": frappe.generate_hash(length=16)}) pp._rename_temporary_references() self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item) pp.sub_assembly_items.pop() @@ -658,40 +680,48 @@ def create_production_plan(**args): """ args = frappe._dict(args) - pln = frappe.get_doc({ - 'doctype': 'Production Plan', - 'company': args.company or '_Test Company', - 'customer': args.customer or '_Test Customer', - 'posting_date': nowdate(), - 'include_non_stock_items': args.include_non_stock_items or 0, - 'include_subcontracted_items': args.include_subcontracted_items or 0, - 'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0, - 'get_items_from': 'Sales Order' - }) + pln = frappe.get_doc( + { + "doctype": "Production Plan", + "company": args.company or "_Test Company", + "customer": args.customer or "_Test Customer", + "posting_date": nowdate(), + "include_non_stock_items": args.include_non_stock_items or 0, + "include_subcontracted_items": args.include_subcontracted_items or 0, + "ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0, + "get_items_from": "Sales Order", + } + ) if not args.get("sales_order"): - pln.append('po_items', { - 'use_multi_level_bom': args.use_multi_level_bom or 1, - 'item_code': args.item_code, - 'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'), - 'planned_qty': args.planned_qty or 1, - 'planned_start_date': args.planned_start_date or now_datetime() - }) + pln.append( + "po_items", + { + "use_multi_level_bom": args.use_multi_level_bom or 1, + "item_code": args.item_code, + "bom_no": frappe.db.get_value("Item", args.item_code, "default_bom"), + "planned_qty": args.planned_qty or 1, + "planned_start_date": args.planned_start_date or now_datetime(), + }, + ) if args.get("get_items_from") == "Sales Order" and args.get("sales_order"): so = args.get("sales_order") - pln.append('sales_orders', { - 'sales_order': so.name, - 'sales_order_date': so.transaction_date, - 'customer': so.customer, - 'grand_total': so.grand_total - }) + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) pln.get_items() if not args.get("skip_getting_mr_items"): mr_items = get_items_for_material_requests(pln.as_dict()) for d in mr_items: - pln.append('mr_items', d) + pln.append("mr_items", d) if not args.do_not_save: pln.insert() @@ -700,31 +730,37 @@ def create_production_plan(**args): return pln + def make_bom(**args): args = frappe._dict(args) - bom = frappe.get_doc({ - 'doctype': 'BOM', - 'is_default': 1, - 'item': args.item, - 'currency': args.currency or 'USD', - 'quantity': args.quantity or 1, - 'company': args.company or '_Test Company', - 'routing': args.routing, - 'with_operations': args.with_operations or 0 - }) + bom = frappe.get_doc( + { + "doctype": "BOM", + "is_default": 1, + "item": args.item, + "currency": args.currency or "USD", + "quantity": args.quantity or 1, + "company": args.company or "_Test Company", + "routing": args.routing, + "with_operations": args.with_operations or 0, + } + ) for item in args.raw_materials: - item_doc = frappe.get_doc('Item', item) + item_doc = frappe.get_doc("Item", item) - bom.append('items', { - 'item_code': item, - 'qty': args.rm_qty or 1.0, - 'uom': item_doc.stock_uom, - 'stock_uom': item_doc.stock_uom, - 'rate': item_doc.valuation_rate or args.rate, - 'source_warehouse': args.source_warehouse - }) + bom.append( + "items", + { + "item_code": item, + "qty": args.rm_qty or 1.0, + "uom": item_doc.stock_uom, + "stock_uom": item_doc.stock_uom, + "rate": item_doc.valuation_rate or args.rate, + "source_warehouse": args.source_warehouse, + }, + ) if not args.do_not_save: bom.insert(ignore_permissions=True) diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index b207906c5e3..d4c37cf79e7 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/erpnext/manufacturing/doctype/routing/routing.py @@ -19,9 +19,11 @@ class Routing(Document): def calculate_operating_cost(self): for operation in self.operations: if not operation.hour_rate: - operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') - operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, - operation.precision("operating_cost")) + operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, "hour_rate") + operation.operating_cost = flt( + flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, + operation.precision("operating_cost"), + ) def set_routing_id(self): sequence_id = 0 @@ -29,7 +31,10 @@ class Routing(Document): if not row.sequence_id: row.sequence_id = sequence_id + 1 elif sequence_id and row.sequence_id and cint(sequence_id) > cint(row.sequence_id): - frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}") - .format(row.idx, row.sequence_id, sequence_id)) + frappe.throw( + _("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}").format( + row.idx, row.sequence_id, sequence_id + ) + ) sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/routing/routing_dashboard.py b/erpnext/manufacturing/doctype/routing/routing_dashboard.py index 4bd4192de5d..65d7a452778 100644 --- a/erpnext/manufacturing/doctype/routing/routing_dashboard.py +++ b/erpnext/manufacturing/doctype/routing/routing_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'routing', - 'transactions': [ - { - 'items': ['BOM'] - } - ] - } + return {"fieldname": "routing", "transactions": [{"items": ["BOM"]}]} diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 696d9bca144..48f1851cb10 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -16,24 +16,27 @@ class TestRouting(FrappeTestCase): @classmethod def tearDownClass(cls): - frappe.db.sql('delete from tabBOM where item=%s', cls.item_code) + frappe.db.sql("delete from tabBOM where item=%s", cls.item_code) def test_sequence_id(self): - operations = [{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30}, - {"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}] + operations = [ + {"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30}, + {"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}, + ] make_test_records("UOM") setup_operations(operations) routing_doc = create_routing(routing_name="Testing Route", operations=operations) bom_doc = setup_bom(item_code=self.item_code, routing=routing_doc.name) - wo_doc = make_wo_order_test_record(production_item = self.item_code, bom_no=bom_doc.name) + wo_doc = make_wo_order_test_record(production_item=self.item_code, bom_no=bom_doc.name) for row in routing_doc.operations: self.assertEqual(row.sequence_id, row.idx) - for data in frappe.get_all("Job Card", - filters={"work_order": wo_doc.name}, order_by="sequence_id desc"): + for data in frappe.get_all( + "Job Card", filters={"work_order": wo_doc.name}, order_by="sequence_id desc" + ): job_card_doc = frappe.get_doc("Job Card", data.name) job_card_doc.time_logs[0].completed_qty = 10 if job_card_doc.sequence_id != 1: @@ -52,33 +55,25 @@ class TestRouting(FrappeTestCase): "operation": "Test Operation A", "workstation": "_Test Workstation A", "hour_rate_rent": 300, - "hour_rate_labour": 750 , - "time_in_mins": 30 + "hour_rate_labour": 750, + "time_in_mins": 30, }, { "operation": "Test Operation B", "workstation": "_Test Workstation B", "hour_rate_labour": 200, "hour_rate_rent": 1000, - "time_in_mins": 20 - } + "time_in_mins": 20, + }, ] test_routing_operations = [ - { - "operation": "Test Operation A", - "workstation": "_Test Workstation A", - "time_in_mins": 30 - }, - { - "operation": "Test Operation B", - "workstation": "_Test Workstation A", - "time_in_mins": 20 - } + {"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 30}, + {"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 20}, ] setup_operations(operations) routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) - bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR') + bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") self.assertEqual(routing_doc.operations[0].time_in_mins, 30) self.assertEqual(routing_doc.operations[1].time_in_mins, 20) routing_doc.operations[0].time_in_mins = 90 @@ -93,10 +88,12 @@ class TestRouting(FrappeTestCase): def setup_operations(rows): from erpnext.manufacturing.doctype.operation.test_operation import make_operation from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation + for row in rows: make_workstation(row) make_operation(row) + def create_routing(**args): args = frappe._dict(args) @@ -108,7 +105,7 @@ def create_routing(**args): doc.insert() except frappe.DuplicateEntryError: doc = frappe.get_doc("Routing", args.routing_name) - doc.delete_key('operations') + doc.delete_key("operations") for operation in args.operations: doc.append("operations", operation) @@ -116,28 +113,35 @@ def create_routing(**args): return doc + def setup_bom(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom args = frappe._dict(args) - if not frappe.db.exists('Item', args.item_code): - make_item(args.item_code, { - 'is_stock_item': 1 - }) + if not frappe.db.exists("Item", args.item_code): + make_item(args.item_code, {"is_stock_item": 1}) if not args.raw_materials: - if not frappe.db.exists('Item', "Test Extra Item N-1"): - make_item("Test Extra Item N-1", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item N-1"): + make_item( + "Test Extra Item N-1", + { + "is_stock_item": 1, + }, + ) - args.raw_materials = ['Test Extra Item N-1'] + args.raw_materials = ["Test Extra Item N-1"] - name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name') + name = frappe.db.get_value("BOM", {"item": args.item_code}, "name") if not name: - bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"), - routing = args.routing, with_operations=1, currency = args.currency) + bom_doc = make_bom( + item=args.item_code, + raw_materials=args.get("raw_materials"), + routing=args.routing, + with_operations=1, + currency=args.currency, + ) else: bom_doc = frappe.get_doc("BOM", name) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ed7f843271b..3709c7db101 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -26,26 +26,33 @@ from erpnext.stock.utils import get_bin class TestWorkOrder(FrappeTestCase): def setUp(self): - self.warehouse = '_Test Warehouse 2 - _TC' - self.item = '_Test Item' + self.warehouse = "_Test Warehouse 2 - _TC" + self.item = "_Test Item" def check_planned_qty(self): - planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item", - "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty") or 0 + planned0 = ( + frappe.db.get_value( + "Bin", {"item_code": "_Test FG Item", "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty" + ) + or 0 + ) wo_order = make_wo_order_test_record() - planned1 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item", - "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty") + planned1 = frappe.db.get_value( + "Bin", {"item_code": "_Test FG Item", "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty" + ) self.assertEqual(planned1, planned0 + 10) # add raw materials to stores - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="Stores - _TC", qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - _TC", qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=100, basic_rate=100 + ) # from stores to wip s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4)) @@ -61,8 +68,9 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(frappe.db.get_value("Work Order", wo_order.name, "produced_qty"), 4) - planned2 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item", - "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty") + planned2 = frappe.db.get_value( + "Bin", {"item_code": "_Test FG Item", "warehouse": "_Test Warehouse 1 - _TC"}, "planned_qty" + ) self.assertEqual(planned2, planned0 + 6) @@ -71,10 +79,12 @@ class TestWorkOrder(FrappeTestCase): def test_over_production(self): wo_doc = self.check_planned_qty() - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100 + ) s = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 7)) s.insert() @@ -82,13 +92,14 @@ class TestWorkOrder(FrappeTestCase): self.assertRaises(StockOverProductionError, s.submit) def test_planned_operating_cost(self): - wo_order = make_wo_order_test_record(item="_Test FG Item 2", - planned_start_date=now(), qty=1, do_not_save=True) + wo_order = make_wo_order_test_record( + item="_Test FG Item 2", planned_start_date=now(), qty=1, do_not_save=True + ) wo_order.set_work_order_operations() cost = wo_order.planned_operating_cost wo_order.qty = 2 wo_order.set_work_order_operations() - self.assertEqual(wo_order.planned_operating_cost, cost*2) + self.assertEqual(wo_order.planned_operating_cost, cost * 2) def test_reserved_qty_for_partial_completion(self): item = "_Test Item" @@ -99,27 +110,30 @@ class TestWorkOrder(FrappeTestCase): # reset to correct value bin1_at_start.update_reserved_qty_for_production() - wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=warehouse, skip_transfer=1) + wo_order = make_wo_order_test_record( + item="_Test FG Item", qty=2, source_warehouse=warehouse, skip_transfer=1 + ) reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production) # reserved qty for production is updated self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission) - - test_stock_entry.make_stock_entry(item_code="_Test Item", - target=warehouse, qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target=warehouse, qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target=warehouse, qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target=warehouse, qty=100, basic_rate=100 + ) s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1)) s.submit() bin1_at_completion = get_bin(item, warehouse) - self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), - reserved_qty_on_submission - 1) + self.assertEqual( + cint(bin1_at_completion.reserved_qty_for_production), reserved_qty_on_submission - 1 + ) def test_production_item(self): wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) @@ -143,16 +157,20 @@ class TestWorkOrder(FrappeTestCase): # reset to correct value self.bin1_at_start.update_reserved_qty_for_production() - self.wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=self.warehouse) + self.wo_order = make_wo_order_test_record( + item="_Test FG Item", qty=2, source_warehouse=self.warehouse + ) self.bin1_on_submit = get_bin(self.item, self.warehouse) # reserved qty for production is updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production) + 2, - cint(self.bin1_on_submit.reserved_qty_for_production)) - self.assertEqual(cint(self.bin1_at_start.projected_qty), - cint(self.bin1_on_submit.projected_qty) + 2) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production) + 2, + cint(self.bin1_on_submit.reserved_qty_for_production), + ) + self.assertEqual( + cint(self.bin1_at_start.projected_qty), cint(self.bin1_on_submit.projected_qty) + 2 + ) def test_reserved_qty_for_production_cancel(self): self.test_reserved_qty_for_production_submit() @@ -162,52 +180,57 @@ class TestWorkOrder(FrappeTestCase): bin1_on_cancel = get_bin(self.item, self.warehouse) # reserved_qty_for_producion updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production), - cint(bin1_on_cancel.reserved_qty_for_production)) - self.assertEqual(self.bin1_at_start.projected_qty, - cint(bin1_on_cancel.projected_qty)) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production), + cint(bin1_on_cancel.reserved_qty_for_production), + ) + self.assertEqual(self.bin1_at_start.projected_qty, cint(bin1_on_cancel.projected_qty)) def test_reserved_qty_for_production_on_stock_entry(self): - test_stock_entry.make_stock_entry(item_code="_Test Item", - target= self.warehouse, qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target= self.warehouse, qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target=self.warehouse, qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target=self.warehouse, qty=100, basic_rate=100 + ) self.test_reserved_qty_for_production_submit() - s = frappe.get_doc(make_stock_entry(self.wo_order.name, - "Material Transfer for Manufacture", 2)) + s = frappe.get_doc(make_stock_entry(self.wo_order.name, "Material Transfer for Manufacture", 2)) s.submit() bin1_on_start_production = get_bin(self.item, self.warehouse) # reserved_qty_for_producion updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production), - cint(bin1_on_start_production.reserved_qty_for_production)) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production), + cint(bin1_on_start_production.reserved_qty_for_production), + ) # projected qty will now be 2 less (becuase of item movement) - self.assertEqual(cint(self.bin1_at_start.projected_qty), - cint(bin1_on_start_production.projected_qty) + 2) + self.assertEqual( + cint(self.bin1_at_start.projected_qty), cint(bin1_on_start_production.projected_qty) + 2 + ) s = frappe.get_doc(make_stock_entry(self.wo_order.name, "Manufacture", 2)) bin1_on_end_production = get_bin(self.item, self.warehouse) # no change in reserved / projected - self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production), - cint(bin1_on_start_production.reserved_qty_for_production)) + self.assertEqual( + cint(bin1_on_end_production.reserved_qty_for_production), + cint(bin1_on_start_production.reserved_qty_for_production), + ) def test_reserved_qty_for_production_closed(self): - wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=self.warehouse) + wo1 = make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=self.warehouse) item = wo1.required_items[0].item_code bin_before = get_bin(item, self.warehouse) bin_before.update_reserved_qty_for_production() - make_wo_order_test_record(item="_Test FG Item", qty=2, - source_warehouse=self.warehouse) + make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=self.warehouse) close_work_order(wo1.name, "Closed") bin_after = get_bin(item, self.warehouse) @@ -217,10 +240,15 @@ class TestWorkOrder(FrappeTestCase): cancel_stock_entry = [] allow_overproduction("overproduction_percentage_for_work_order", 30) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100) - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0 + ) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=240, + basic_rate=1000.0, + ) cancel_stock_entry.extend([ste1.name, ste2.name]) @@ -250,33 +278,37 @@ class TestWorkOrder(FrappeTestCase): allow_overproduction("overproduction_percentage_for_work_order", 0) def test_reserved_qty_for_stopped_production(self): - test_stock_entry.make_stock_entry(item_code="_Test Item", - target= self.warehouse, qty=100, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target= self.warehouse, qty=100, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target=self.warehouse, qty=100, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target=self.warehouse, qty=100, basic_rate=100 + ) # 0 0 0 self.test_reserved_qty_for_production_submit() - #2 0 -2 + # 2 0 -2 - s = frappe.get_doc(make_stock_entry(self.wo_order.name, - "Material Transfer for Manufacture", 1)) + s = frappe.get_doc(make_stock_entry(self.wo_order.name, "Material Transfer for Manufacture", 1)) s.submit() - #1 -1 0 + # 1 -1 0 bin1_on_start_production = get_bin(self.item, self.warehouse) # reserved_qty_for_producion updated - self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production) + 1, - cint(bin1_on_start_production.reserved_qty_for_production)) + self.assertEqual( + cint(self.bin1_at_start.reserved_qty_for_production) + 1, + cint(bin1_on_start_production.reserved_qty_for_production), + ) # projected qty will now be 2 less (becuase of item movement) - self.assertEqual(cint(self.bin1_at_start.projected_qty), - cint(bin1_on_start_production.projected_qty) + 2) + self.assertEqual( + cint(self.bin1_at_start.projected_qty), cint(bin1_on_start_production.projected_qty) + 2 + ) # STOP stop_unstop(self.wo_order.name, "Stopped") @@ -284,19 +316,24 @@ class TestWorkOrder(FrappeTestCase): bin1_on_stop_production = get_bin(self.item, self.warehouse) # no change in reserved / projected - self.assertEqual(cint(bin1_on_stop_production.reserved_qty_for_production), - cint(self.bin1_at_start.reserved_qty_for_production)) - self.assertEqual(cint(bin1_on_stop_production.projected_qty) + 1, - cint(self.bin1_at_start.projected_qty)) + self.assertEqual( + cint(bin1_on_stop_production.reserved_qty_for_production), + cint(self.bin1_at_start.reserved_qty_for_production), + ) + self.assertEqual( + cint(bin1_on_stop_production.projected_qty) + 1, cint(self.bin1_at_start.projected_qty) + ) def test_scrap_material_qty(self): wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) # add raw materials to stores - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="Stores - _TC", qty=10, basic_rate=5000.0) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - _TC", qty=10, basic_rate=1000.0) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0 + ) s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) for d in s.get("items"): @@ -308,8 +345,9 @@ class TestWorkOrder(FrappeTestCase): s.insert() s.submit() - wo_order_details = frappe.db.get_value("Work Order", wo_order.name, - ["scrap_warehouse", "qty", "produced_qty", "bom_no"], as_dict=1) + wo_order_details = frappe.db.get_value( + "Work Order", wo_order.name, ["scrap_warehouse", "qty", "produced_qty", "bom_no"], as_dict=1 + ) scrap_item_details = get_scrap_item_details(wo_order_details.bom_no) @@ -318,15 +356,20 @@ class TestWorkOrder(FrappeTestCase): for item in s.items: if item.bom_no and item.item_code in scrap_item_details: self.assertEqual(wo_order_details.scrap_warehouse, item.t_warehouse) - self.assertEqual(flt(wo_order_details.qty)*flt(scrap_item_details[item.item_code]), item.qty) + self.assertEqual(flt(wo_order_details.qty) * flt(scrap_item_details[item.item_code]), item.qty) def test_allow_overproduction(self): allow_overproduction("overproduction_percentage_for_work_order", 0) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0) - test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=10, + basic_rate=1000.0, + ) s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 3)) s.insert() @@ -343,42 +386,47 @@ class TestWorkOrder(FrappeTestCase): so = make_sales_order(item_code="_Test FG Item", qty=2) allow_overproduction("overproduction_percentage_for_sales_order", 0) - wo_order = make_wo_order_test_record(planned_start_date=now(), - sales_order=so.name, qty=3, do_not_save=True) + wo_order = make_wo_order_test_record( + planned_start_date=now(), sales_order=so.name, qty=3, do_not_save=True + ) self.assertRaises(OverProductionError, wo_order.save) allow_overproduction("overproduction_percentage_for_sales_order", 50) - wo_order = make_wo_order_test_record(planned_start_date=now(), - sales_order=so.name, qty=3) + wo_order = make_wo_order_test_record(planned_start_date=now(), sales_order=so.name, qty=3) self.assertEqual(wo_order.docstatus, 1) allow_overproduction("overproduction_percentage_for_sales_order", 0) def test_work_order_with_non_stock_item(self): - items = {'Finished Good Test Item For non stock': 1, '_Test FG Item': 1, '_Test FG Non Stock Item': 0} + items = { + "Finished Good Test Item For non stock": 1, + "_Test FG Item": 1, + "_Test FG Non Stock Item": 0, + } for item, is_stock_item in items.items(): - make_item(item, { - 'is_stock_item': is_stock_item - }) + make_item(item, {"is_stock_item": is_stock_item}) - if not frappe.db.get_value('Item Price', {'item_code': '_Test FG Non Stock Item'}): - frappe.get_doc({ - 'doctype': 'Item Price', - 'item_code': '_Test FG Non Stock Item', - 'price_list_rate': 1000, - 'price_list': 'Standard Buying' - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Item Price", {"item_code": "_Test FG Non Stock Item"}): + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": "_Test FG Non Stock Item", + "price_list_rate": 1000, + "price_list": "Standard Buying", + } + ).insert(ignore_permissions=True) - fg_item = 'Finished Good Test Item For non stock' - test_stock_entry.make_stock_entry(item_code="_Test FG Item", - target="_Test Warehouse - _TC", qty=1, basic_rate=100) + fg_item = "Finished Good Test Item For non stock" + test_stock_entry.make_stock_entry( + item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) - if not frappe.db.get_value('BOM', {'item': fg_item}): - make_bom(item=fg_item, rate=1000, raw_materials = ['_Test FG Item', '_Test FG Non Stock Item']) + if not frappe.db.get_value("BOM", {"item": fg_item}): + make_bom(item=fg_item, rate=1000, raw_materials=["_Test FG Item", "_Test FG Non Stock Item"]) - wo = make_wo_order_test_record(production_item = fg_item) + wo = make_wo_order_test_record(production_item=fg_item) se = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1)) se.insert() @@ -392,25 +440,25 @@ class TestWorkOrder(FrappeTestCase): @timeout(seconds=60) def test_job_card(self): stock_entries = [] - bom = frappe.get_doc('BOM', { - 'docstatus': 1, - 'with_operations': 1, - 'company': '_Test Company' - }) + bom = frappe.get_doc("BOM", {"docstatus": 1, "with_operations": 1, "company": "_Test Company"}) - work_order = make_wo_order_test_record(item=bom.item, qty=1, - bom_no=bom.name, source_warehouse="_Test Warehouse - _TC") + work_order = make_wo_order_test_record( + item=bom.item, qty=1, bom_no=bom.name, source_warehouse="_Test Warehouse - _TC" + ) for row in work_order.required_items: - stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code, - target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100) + stock_entry_doc = test_stock_entry.make_stock_entry( + item_code=row.item_code, target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100 + ) stock_entries.append(stock_entry_doc) ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) ste.submit() stock_entries.append(ste) - job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc') + job_cards = frappe.get_all( + "Job Card", filters={"work_order": work_order.name}, order_by="creation asc" + ) self.assertEqual(len(job_cards), len(bom.operations)) for i, job_card in enumerate(job_cards): @@ -431,29 +479,33 @@ class TestWorkOrder(FrappeTestCase): stock_entry.cancel() def test_capcity_planning(self): - frappe.db.set_value("Manufacturing Settings", None, { - "disable_capacity_planning": 0, - "capacity_planning_for_days": 1 - }) + frappe.db.set_value( + "Manufacturing Settings", + None, + {"disable_capacity_planning": 0, "capacity_planning_for_days": 1}, + ) - data = frappe.get_cached_value('BOM', {'docstatus': 1, 'item': '_Test FG Item 2', - 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + data = frappe.get_cached_value( + "BOM", + {"docstatus": 1, "item": "_Test FG Item 2", "with_operations": 1, "company": "_Test Company"}, + ["name", "item"], + ) if data: bom, bom_item = data planned_start_date = add_months(today(), months=-1) - work_order = make_wo_order_test_record(item=bom_item, - qty=10, bom_no=bom, planned_start_date=planned_start_date) + work_order = make_wo_order_test_record( + item=bom_item, qty=10, bom_no=bom, planned_start_date=planned_start_date + ) - work_order1 = make_wo_order_test_record(item=bom_item, - qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1) + work_order1 = make_wo_order_test_record( + item=bom_item, qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1 + ) self.assertRaises(CapacityError, work_order1.submit) - frappe.db.set_value("Manufacturing Settings", None, { - "capacity_planning_for_days": 30 - }) + frappe.db.set_value("Manufacturing Settings", None, {"capacity_planning_for_days": 30}) work_order1.reload() work_order1.submit() @@ -463,22 +515,22 @@ class TestWorkOrder(FrappeTestCase): work_order.cancel() def test_work_order_with_non_transfer_item(self): - items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} + items = {"Finished Good Transfer Item": 1, "_Test FG Item": 1, "_Test FG Item 1": 0} for item, allow_transfer in items.items(): - make_item(item, { - 'include_item_in_manufacturing': allow_transfer - }) + make_item(item, {"include_item_in_manufacturing": allow_transfer}) - fg_item = 'Finished Good Transfer Item' - test_stock_entry.make_stock_entry(item_code="_Test FG Item", - target="_Test Warehouse - _TC", qty=1, basic_rate=100) - test_stock_entry.make_stock_entry(item_code="_Test FG Item 1", - target="_Test Warehouse - _TC", qty=1, basic_rate=100) + fg_item = "Finished Good Transfer Item" + test_stock_entry.make_stock_entry( + item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) + test_stock_entry.make_stock_entry( + item_code="_Test FG Item 1", target="_Test Warehouse - _TC", qty=1, basic_rate=100 + ) - if not frappe.db.get_value('BOM', {'item': fg_item}): - make_bom(item=fg_item, raw_materials = ['_Test FG Item', '_Test FG Item 1']) + if not frappe.db.get_value("BOM", {"item": fg_item}): + make_bom(item=fg_item, raw_materials=["_Test FG Item", "_Test FG Item 1"]) - wo = make_wo_order_test_record(production_item = fg_item) + wo = make_wo_order_test_record(production_item=fg_item) ste = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 1)) ste.insert() ste.submit() @@ -496,39 +548,42 @@ class TestWorkOrder(FrappeTestCase): rm1 = "Test Batch Size Item RM 1 For BOM" for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]: - make_item(item, { - "include_item_in_manufacturing": 1, - "is_stock_item": 1 - }) + make_item(item, {"include_item_in_manufacturing": 1, "is_stock_item": 1}) - bom_name = frappe.db.get_value("BOM", - {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + bom_name = frappe.db.get_value( + "BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name" + ) if not bom_name: - bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True) bom.with_operations = 1 - bom.append("operations", { - "operation": "_Test Operation 1", - "workstation": "_Test Workstation 1", - "description": "Test Data", - "operating_cost": 100, - "time_in_mins": 40, - "batch_size": 5 - }) + bom.append( + "operations", + { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "description": "Test Data", + "operating_cost": 100, + "time_in_mins": 40, + "batch_size": 5, + }, + ) bom.save() bom.submit() bom_name = bom.name - work_order = make_wo_order_test_record(item=fg_item, - planned_start_date=now(), qty=1, do_not_save=True) + work_order = make_wo_order_test_record( + item=fg_item, planned_start_date=now(), qty=1, do_not_save=True + ) work_order.set_work_order_operations() work_order.save() self.assertEqual(work_order.operations[0].time_in_mins, 8.0) - work_order1 = make_wo_order_test_record(item=fg_item, - planned_start_date=now(), qty=5, do_not_save=True) + work_order1 = make_wo_order_test_record( + item=fg_item, planned_start_date=now(), qty=5, do_not_save=True + ) work_order1.set_work_order_operations() work_order1.save() @@ -538,65 +593,73 @@ class TestWorkOrder(FrappeTestCase): fg_item = "Test Batch Size Item For BOM 3" rm1 = "Test Batch Size Item RM 1 For BOM 3" - frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 0) for item in ["Test Batch Size Item For BOM 3", "Test Batch Size Item RM 1 For BOM 3"]: - item_args = { - "include_item_in_manufacturing": 1, - "is_stock_item": 1 - } + item_args = {"include_item_in_manufacturing": 1, "is_stock_item": 1} if item == fg_item: - item_args['has_batch_no'] = 1 - item_args['create_new_batch'] = 1 - item_args['batch_number_series'] = 'TBSI3.#####' + item_args["has_batch_no"] = 1 + item_args["create_new_batch"] = 1 + item_args["batch_number_series"] = "TBSI3.#####" make_item(item, item_args) - bom_name = frappe.db.get_value("BOM", - {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + bom_name = frappe.db.get_value( + "BOM", {"item": fg_item, "is_active": 1, "with_operations": 1}, "name" + ) if not bom_name: - bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom = make_bom(item=fg_item, rate=1000, raw_materials=[rm1], do_not_save=True) bom.save() bom.submit() bom_name = bom.name - work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) + work_order = make_wo_order_test_record( + item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1 + ) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) - for row in ste1.get('items'): + for row in ste1.get("items"): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) - work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1) - frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 1) + work_order = make_wo_order_test_record( + item=fg_item, skip_transfer=True, planned_start_date=now(), qty=1 + ) + frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 1) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) - for row in ste1.get('items'): + for row in ste1.get("items"): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) - work_order = make_wo_order_test_record(item=fg_item, skip_transfer=True, planned_start_date=now(), - qty=30, do_not_save = True) + work_order = make_wo_order_test_record( + item=fg_item, skip_transfer=True, planned_start_date=now(), qty=30, do_not_save=True + ) work_order.batch_size = 10 work_order.insert() work_order.submit() self.assertEqual(work_order.has_batch_no, 1) ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 30)) - for row in ste1.get('items'): + for row in ste1.get("items"): if row.is_finished_item: self.assertEqual(row.item_code, fg_item) self.assertEqual(row.qty, 10) - frappe.db.set_value('Manufacturing Settings', None, 'make_serial_no_batch_from_work_order', 0) + frappe.db.set_value("Manufacturing Settings", None, "make_serial_no_batch_from_work_order", 0) def test_partial_material_consumption(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) ste_cancel_list = [] - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0 + ) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=20, + basic_rate=1000.0, + ) ste_cancel_list.extend([ste1, ste2]) @@ -622,16 +685,25 @@ class TestWorkOrder(FrappeTestCase): def test_extra_material_transfer(self): frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0) - frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", - "Material Transferred for Manufacture") + frappe.db.set_value( + "Manufacturing Settings", + None, + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4) ste_cancel_list = [] - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0 + ) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=20, + basic_rate=1000.0, + ) ste_cancel_list.extend([ste1, ste2]) @@ -663,30 +735,31 @@ class TestWorkOrder(FrappeTestCase): frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") def test_make_stock_entry_for_customer_provided_item(self): - finished_item = 'Test Item for Make Stock Entry 1' - make_item(finished_item, { + finished_item = "Test Item for Make Stock Entry 1" + make_item(finished_item, {"include_item_in_manufacturing": 1, "is_stock_item": 1}) + + customer_provided_item = "CUST-0987" + make_item( + customer_provided_item, + { + "is_purchase_item": 0, + "is_customer_provided_item": 1, + "is_stock_item": 1, "include_item_in_manufacturing": 1, - "is_stock_item": 1 - }) + "customer": "_Test Customer", + }, + ) - customer_provided_item = 'CUST-0987' - make_item(customer_provided_item, { - 'is_purchase_item': 0, - 'is_customer_provided_item': 1, - "is_stock_item": 1, - "include_item_in_manufacturing": 1, - 'customer': '_Test Customer' - }) - - if not frappe.db.exists('BOM', {'item': finished_item}): + if not frappe.db.exists("BOM", {"item": finished_item}): make_bom(item=finished_item, raw_materials=[customer_provided_item], rm_qty=1) company = "_Test Company with perpetual inventory" customer_warehouse = create_warehouse("Test Customer Provided Warehouse", company=company) - wo = make_wo_order_test_record(item=finished_item, qty=1, source_warehouse=customer_warehouse, - company=company) + wo = make_wo_order_test_record( + item=finished_item, qty=1, source_warehouse=customer_warehouse, company=company + ) - ste = frappe.get_doc(make_stock_entry(wo.name, purpose='Material Transfer for Manufacture')) + ste = frappe.get_doc(make_stock_entry(wo.name, purpose="Material Transfer for Manufacture")) ste.insert() self.assertEqual(len(ste.items), 1) @@ -695,26 +768,33 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(item.valuation_rate, 0) def test_valuation_rate_missing_on_make_stock_entry(self): - item_name = 'Test Valuation Rate Missing' - rm_item = '_Test raw material item' - make_item(item_name, { - "is_stock_item": 1, - "include_item_in_manufacturing": 1, - }) - make_item('_Test raw material item', { - "is_stock_item": 1, - "include_item_in_manufacturing": 1, - }) + item_name = "Test Valuation Rate Missing" + rm_item = "_Test raw material item" + make_item( + item_name, + { + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + }, + ) + make_item( + "_Test raw material item", + { + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + }, + ) - if not frappe.db.get_value('BOM', {'item': item_name}): + if not frappe.db.get_value("BOM", {"item": item_name}): make_bom(item=item_name, raw_materials=[rm_item], rm_qty=1) company = "_Test Company with perpetual inventory" source_warehouse = create_warehouse("Test Valuation Rate Missing Warehouse", company=company) - wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse, - company=company) + wo = make_wo_order_test_record( + item=item_name, qty=1, source_warehouse=source_warehouse, company=company + ) - stock_entry = frappe.get_doc(make_stock_entry(wo.name, 'Material Transfer for Manufacture')) + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture")) self.assertRaises(frappe.ValidationError, stock_entry.save) def test_wo_completion_with_pl_bom(self): @@ -724,19 +804,19 @@ class TestWorkOrder(FrappeTestCase): ) qty = 4 - scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG + scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" fg_item_non_whole, _, bom_item = create_process_loss_bom_items() - test_stock_entry.make_stock_entry(item_code=bom_item.item_code, - target=source_warehouse, qty=4, basic_rate=100) + test_stock_entry.make_stock_entry( + item_code=bom_item.item_code, target=source_warehouse, qty=4, basic_rate=100 + ) bom_no = f"BOM-{fg_item_non_whole.item_code}-001" if not frappe.db.exists("BOM", bom_no): bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=scrap_qty, - scrap_rate=0, fg_qty=1, is_process_loss=1 + fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1, is_process_loss=1 ) bom_doc.submit() @@ -749,16 +829,12 @@ class TestWorkOrder(FrappeTestCase): stock_uom=fg_item_non_whole.stock_uom, ) - se = frappe.get_doc( - make_stock_entry(wo.name, "Material Transfer for Manufacture", qty) - ) + se = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", qty)) se.get("items")[0].s_warehouse = "Stores - _TC" se.insert() se.submit() - se = frappe.get_doc( - make_stock_entry(wo.name, "Manufacture", qty) - ) + se = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", qty)) se.insert() se.submit() @@ -775,41 +851,52 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(fg_item.qty, actual_fg_qty) # Testing Work Order values - self.assertEqual( - frappe.db.get_value("Work Order", wo.name, "produced_qty"), - qty - ) - self.assertEqual( - frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), - total_pl_qty - ) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) + self.assertEqual(frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), total_pl_qty) @timeout(seconds=60) def test_job_card_scrap_item(self): - items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test', - 'Test RM Item 2 for Scrap Item Test'] + items = [ + "Test FG Item for Scrap Item Test", + "Test RM Item 1 for Scrap Item Test", + "Test RM Item 2 for Scrap Item Test", + ] - company = '_Test Company with perpetual inventory' + company = "_Test Company with perpetual inventory" for item_code in items: - create_item(item_code = item_code, is_stock_item = 1, - is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1') + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=100, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) - item = 'Test FG Item for Scrap Item Test' - raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test'] - if not frappe.db.get_value('BOM', {'item': item}): - bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True) + item = "Test FG Item for Scrap Item Test" + raw_materials = ["Test RM Item 1 for Scrap Item Test", "Test RM Item 2 for Scrap Item Test"] + if not frappe.db.get_value("BOM", {"item": item}): + bom = make_bom( + item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True + ) bom.with_operations = 1 - bom.append('operations', { - 'operation': '_Test Operation 1', - 'workstation': '_Test Workstation 1', - 'hour_rate': 20, - 'time_in_mins': 60 - }) + bom.append( + "operations", + { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "hour_rate": 20, + "time_in_mins": 60, + }, + ) bom.submit() - wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1) - job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + wo_order = make_wo_order_test_record( + item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1 + ) + job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name") update_job_card(job_card) stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) @@ -818,8 +905,10 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(row.qty, 1) # Partial Job Card 1 with qty 10 - wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1) - job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + wo_order = make_wo_order_test_record( + item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1 + ) + job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name") update_job_card(job_card, 10) stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) @@ -832,12 +921,12 @@ class TestWorkOrder(FrappeTestCase): wo_order.load_from_db() for row in wo_order.operations: n_dict = row.as_dict() - n_dict['qty'] = 10 - n_dict['pending_qty'] = 10 + n_dict["qty"] = 10 + n_dict["pending_qty"] = 10 operations.append(n_dict) make_job_card(wo_order.name, operations) - job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name') + job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name, "docstatus": 0}, "name") update_job_card(job_card, 10) stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) @@ -846,95 +935,119 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(row.qty, 2) def test_close_work_order(self): - items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO', - 'Test RM Item 2 for Closed WO'] + items = [ + "Test FG Item for Closed WO", + "Test RM Item 1 for Closed WO", + "Test RM Item 2 for Closed WO", + ] - company = '_Test Company with perpetual inventory' + company = "_Test Company with perpetual inventory" for item_code in items: - create_item(item_code = item_code, is_stock_item = 1, - is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1') + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=100, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) - item = 'Test FG Item for Closed WO' - raw_materials = ['Test RM Item 1 for Closed WO', 'Test RM Item 2 for Closed WO'] - if not frappe.db.get_value('BOM', {'item': item}): - bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True) + item = "Test FG Item for Closed WO" + raw_materials = ["Test RM Item 1 for Closed WO", "Test RM Item 2 for Closed WO"] + if not frappe.db.get_value("BOM", {"item": item}): + bom = make_bom( + item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True + ) bom.with_operations = 1 - bom.append('operations', { - 'operation': '_Test Operation 1', - 'workstation': '_Test Workstation 1', - 'hour_rate': 20, - 'time_in_mins': 60 - }) + bom.append( + "operations", + { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "hour_rate": 20, + "time_in_mins": 60, + }, + ) bom.submit() - wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1) - job_cards = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + wo_order = make_wo_order_test_record( + item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1 + ) + job_cards = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name") if len(job_cards) == len(bom.operations): for jc in job_cards: - job_card_doc = frappe.get_doc('Job Card', jc) - job_card_doc.append('time_logs', { - 'from_time': now(), - 'time_in_mins': 60, - 'completed_qty': job_card_doc.for_quantity - }) + job_card_doc = frappe.get_doc("Job Card", jc) + job_card_doc.append( + "time_logs", + {"from_time": now(), "time_in_mins": 60, "completed_qty": job_card_doc.for_quantity}, + ) job_card_doc.submit() close_work_order(wo_order, "Closed") - self.assertEqual(wo_order.get('status'), "Closed") + self.assertEqual(wo_order.get("status"), "Closed") def test_partial_manufacture_entries(self): cancel_stock_entry = [] - frappe.db.set_value("Manufacturing Settings", None, - "backflush_raw_materials_based_on", "Material Transferred for Manufacture") + frappe.db.set_value( + "Manufacturing Settings", + None, + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100) - ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item", - target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0) + ste1 = test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0 + ) - ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", - target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0) + ste2 = test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=240, + basic_rate=1000.0, + ) cancel_stock_entry.extend([ste1.name, ste2.name]) sm = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 100)) - for row in sm.get('items'): - if row.get('item_code') == '_Test Item': + for row in sm.get("items"): + if row.get("item_code") == "_Test Item": row.qty = 110 sm.submit() cancel_stock_entry.append(sm.name) s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 90)) - for row in s.get('items'): - if row.get('item_code') == '_Test Item': - self.assertEqual(row.get('qty'), 100) + for row in s.get("items"): + if row.get("item_code") == "_Test Item": + self.assertEqual(row.get("qty"), 100) s.submit() cancel_stock_entry.append(s.name) s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5)) - for row in s1.get('items'): - if row.get('item_code') == '_Test Item': - self.assertEqual(row.get('qty'), 5) + for row in s1.get("items"): + if row.get("item_code") == "_Test Item": + self.assertEqual(row.get("qty"), 5) s1.submit() cancel_stock_entry.append(s1.name) s2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5)) - for row in s2.get('items'): - if row.get('item_code') == '_Test Item': - self.assertEqual(row.get('qty'), 5) + for row in s2.get("items"): + if row.get("item_code") == "_Test Item": + self.assertEqual(row.get("qty"), 5) cancel_stock_entry.reverse() for ste in cancel_stock_entry: doc = frappe.get_doc("Stock Entry", ste) doc.cancel() - frappe.db.set_value("Manufacturing Settings", None, - "backflush_raw_materials_based_on", "BOM") + frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1}) def test_auto_batch_creation(self): @@ -943,7 +1056,7 @@ class TestWorkOrder(FrappeTestCase): fg_item = frappe.generate_hash(length=20) child_item = frappe.generate_hash(length=20) - bom_tree = {fg_item: {child_item: {}}} + bom_tree = {fg_item: {child_item: {}}} create_nested_bom(bom_tree, prefix="") @@ -959,54 +1072,56 @@ class TestWorkOrder(FrappeTestCase): def update_job_card(job_card, jc_qty=None): - employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') + employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") - job_card_doc = frappe.get_doc('Job Card', job_card) - job_card_doc.set('scrap_items', [ - { - 'item_code': 'Test RM Item 1 for Scrap Item Test', - 'stock_qty': 2 - }, - { - 'item_code': 'Test RM Item 2 for Scrap Item Test', - 'stock_qty': 2 - }, - ]) + job_card_doc = frappe.get_doc("Job Card", job_card) + job_card_doc.set( + "scrap_items", + [ + {"item_code": "Test RM Item 1 for Scrap Item Test", "stock_qty": 2}, + {"item_code": "Test RM Item 2 for Scrap Item Test", "stock_qty": 2}, + ], + ) if jc_qty: job_card_doc.for_quantity = jc_qty - job_card_doc.append('time_logs', { - 'from_time': now(), - 'employee': employee, - 'time_in_mins': 60, - 'completed_qty': job_card_doc.for_quantity - }) + job_card_doc.append( + "time_logs", + { + "from_time": now(), + "employee": employee, + "time_in_mins": 60, + "completed_qty": job_card_doc.for_quantity, + }, + ) job_card_doc.submit() + def get_scrap_item_details(bom_no): scrap_items = {} - for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` - where parent = %s""", bom_no, as_dict=1): + for item in frappe.db.sql( + """select item_code, stock_qty from `tabBOM Scrap Item` + where parent = %s""", + bom_no, + as_dict=1, + ): scrap_items[item.item_code] = item.stock_qty return scrap_items + def allow_overproduction(fieldname, percentage): doc = frappe.get_doc("Manufacturing Settings") - doc.update({ - fieldname: percentage - }) + doc.update({fieldname: percentage}) doc.save() + def make_wo_order_test_record(**args): args = frappe._dict(args) if args.company and args.company != "_Test Company": - warehouse_map = { - "fg_warehouse": "_Test FG Warehouse", - "wip_warehouse": "_Test WIP Warehouse" - } + warehouse_map = {"fg_warehouse": "_Test FG Warehouse", "wip_warehouse": "_Test WIP Warehouse"} for attr, wh_name in warehouse_map.items(): if not args.get(attr): @@ -1014,16 +1129,17 @@ def make_wo_order_test_record(**args): wo_order = frappe.new_doc("Work Order") wo_order.production_item = args.production_item or args.item or args.item_code or "_Test FG Item" - wo_order.bom_no = args.bom_no or frappe.db.get_value("BOM", {"item": wo_order.production_item, - "is_active": 1, "is_default": 1}) + wo_order.bom_no = args.bom_no or frappe.db.get_value( + "BOM", {"item": wo_order.production_item, "is_active": 1, "is_default": 1} + ) wo_order.qty = args.qty or 10 wo_order.wip_warehouse = args.wip_warehouse or "_Test Warehouse - _TC" wo_order.fg_warehouse = args.fg_warehouse or "_Test Warehouse 1 - _TC" wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC" wo_order.company = args.company or "_Test Company" wo_order.stock_uom = args.stock_uom or "_Test UOM" - wo_order.use_multi_level_bom=0 - wo_order.skip_transfer=args.skip_transfer or 0 + wo_order.use_multi_level_bom = 0 + wo_order.skip_transfer = args.skip_transfer or 0 wo_order.get_items_and_operations_from_bom() wo_order.sales_order = args.sales_order or None wo_order.planned_start_date = args.planned_start_date or now() @@ -1040,4 +1156,5 @@ def make_wo_order_test_record(**args): wo_order.submit() return wo_order -test_records = frappe.get_test_records('Work Order') + +test_records = frappe.get_test_records("Work Order") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index ee12597d24f..f2586825cc7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -42,11 +42,26 @@ from erpnext.stock.utils import get_bin, get_latest_stock_qty, validate_warehous from erpnext.utilities.transaction_base import validate_uom_is_integer -class OverProductionError(frappe.ValidationError): pass -class CapacityError(frappe.ValidationError): pass -class StockOverProductionError(frappe.ValidationError): pass -class OperationTooLongError(frappe.ValidationError): pass -class ItemHasVariantError(frappe.ValidationError): pass +class OverProductionError(frappe.ValidationError): + pass + + +class CapacityError(frappe.ValidationError): + pass + + +class StockOverProductionError(frappe.ValidationError): + pass + + +class OperationTooLongError(frappe.ValidationError): + pass + + +class ItemHasVariantError(frappe.ValidationError): + pass + + class SerialNoQtyError(frappe.ValidationError): pass @@ -74,12 +89,13 @@ class WorkOrder(Document): validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) - self.set_required_items(reset_only_qty = len(self.get("required_items"))) + self.set_required_items(reset_only_qty=len(self.get("required_items"))) def validate_sales_order(self): if self.sales_order: self.check_sales_order_on_hold_or_close() - so = frappe.db.sql(""" + so = frappe.db.sql( + """ select so.name, so_item.delivery_date, so.project from `tabSales Order` so inner join `tabSales Order Item` so_item on so_item.parent = so.name @@ -88,10 +104,14 @@ class WorkOrder(Document): and so.skip_delivery_note = 0 and ( so_item.item_code=%s or pk_item.item_code=%s ) - """, (self.sales_order, self.production_item, self.production_item), as_dict=1) + """, + (self.sales_order, self.production_item, self.production_item), + as_dict=1, + ) if not so: - so = frappe.db.sql(""" + so = frappe.db.sql( + """ select so.name, so_item.delivery_date, so.project from @@ -102,7 +122,10 @@ class WorkOrder(Document): and so.skip_delivery_note = 0 and so_item.item_code = packed_item.parent_item and so.docstatus = 1 and packed_item.item_code=%s - """, (self.sales_order, self.production_item), as_dict=1) + """, + (self.sales_order, self.production_item), + as_dict=1, + ) if len(so): if not self.expected_delivery_date: @@ -123,7 +146,9 @@ class WorkOrder(Document): def set_default_warehouse(self): if not self.wip_warehouse: - self.wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") + self.wip_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_wip_warehouse" + ) if not self.fg_warehouse: self.fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_fg_warehouse") @@ -145,40 +170,55 @@ class WorkOrder(Document): self.planned_operating_cost += flt(d.planned_operating_cost) self.actual_operating_cost += flt(d.actual_operating_cost) - variable_cost = self.actual_operating_cost if self.actual_operating_cost \ - else self.planned_operating_cost + variable_cost = ( + self.actual_operating_cost if self.actual_operating_cost else self.planned_operating_cost + ) - self.total_operating_cost = (flt(self.additional_operating_cost) - + flt(variable_cost) + flt(self.corrective_operation_cost)) + self.total_operating_cost = ( + flt(self.additional_operating_cost) + flt(variable_cost) + flt(self.corrective_operation_cost) + ) def validate_work_order_against_so(self): # already ordered qty - ordered_qty_against_so = frappe.db.sql("""select sum(qty) from `tabWork Order` + ordered_qty_against_so = frappe.db.sql( + """select sum(qty) from `tabWork Order` where production_item = %s and sales_order = %s and docstatus < 2 and name != %s""", - (self.production_item, self.sales_order, self.name))[0][0] + (self.production_item, self.sales_order, self.name), + )[0][0] total_qty = flt(ordered_qty_against_so) + flt(self.qty) # get qty from Sales Order Item table - so_item_qty = frappe.db.sql("""select sum(stock_qty) from `tabSales Order Item` + so_item_qty = frappe.db.sql( + """select sum(stock_qty) from `tabSales Order Item` where parent = %s and item_code = %s""", - (self.sales_order, self.production_item))[0][0] + (self.sales_order, self.production_item), + )[0][0] # get qty from Packing Item table - dnpi_qty = frappe.db.sql("""select sum(qty) from `tabPacked Item` + dnpi_qty = frappe.db.sql( + """select sum(qty) from `tabPacked Item` where parent = %s and parenttype = 'Sales Order' and item_code = %s""", - (self.sales_order, self.production_item))[0][0] + (self.sales_order, self.production_item), + )[0][0] # total qty in SO so_qty = flt(so_item_qty) + flt(dnpi_qty) - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_sales_order")) + allowance_percentage = flt( + frappe.db.get_single_value( + "Manufacturing Settings", "overproduction_percentage_for_sales_order" + ) + ) - if total_qty > so_qty + (allowance_percentage/100 * so_qty): - frappe.throw(_("Cannot produce more Item {0} than Sales Order quantity {1}") - .format(self.production_item, so_qty), OverProductionError) + if total_qty > so_qty + (allowance_percentage / 100 * so_qty): + frappe.throw( + _("Cannot produce more Item {0} than Sales Order quantity {1}").format( + self.production_item, so_qty + ), + OverProductionError, + ) def update_status(self, status=None): - '''Update status of work order if unknown''' + """Update status of work order if unknown""" if status != "Stopped" and status != "Closed": status = self.get_status(status) @@ -190,17 +230,22 @@ class WorkOrder(Document): return status def get_status(self, status=None): - '''Return the status based on stock entries against this work order''' + """Return the status based on stock entries against this work order""" if not status: status = self.status - if self.docstatus==0: - status = 'Draft' - elif self.docstatus==1: - if status != 'Stopped': - stock_entries = frappe._dict(frappe.db.sql("""select purpose, sum(fg_completed_qty) + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + if status != "Stopped": + stock_entries = frappe._dict( + frappe.db.sql( + """select purpose, sum(fg_completed_qty) from `tabStock Entry` where work_order=%s and docstatus=1 - group by purpose""", self.name)) + group by purpose""", + self.name, + ) + ) status = "Not Started" if stock_entries: @@ -209,31 +254,46 @@ class WorkOrder(Document): if flt(produced_qty) >= flt(self.qty): status = "Completed" else: - status = 'Cancelled' + status = "Cancelled" return status def update_work_order_qty(self): """Update **Manufactured Qty** and **Material Transferred for Qty** in Work Order - based on Stock Entry""" + based on Stock Entry""" - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) - for purpose, fieldname in (("Manufacture", "produced_qty"), - ("Material Transfer for Manufacture", "material_transferred_for_manufacturing")): - if (purpose == 'Material Transfer for Manufacture' and - self.operations and self.transfer_material_against == 'Job Card'): + for purpose, fieldname in ( + ("Manufacture", "produced_qty"), + ("Material Transfer for Manufacture", "material_transferred_for_manufacturing"), + ): + if ( + purpose == "Material Transfer for Manufacture" + and self.operations + and self.transfer_material_against == "Job Card" + ): continue - qty = flt(frappe.db.sql("""select sum(fg_completed_qty) + qty = flt( + frappe.db.sql( + """select sum(fg_completed_qty) from `tabStock Entry` where work_order=%s and docstatus=1 - and purpose=%s""", (self.name, purpose))[0][0]) + and purpose=%s""", + (self.name, purpose), + )[0][0] + ) - completed_qty = self.qty + (allowance_percentage/100 * self.qty) + completed_qty = self.qty + (allowance_percentage / 100 * self.qty) if qty > completed_qty: - frappe.throw(_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(\ - self.meta.get_label(fieldname), qty, completed_qty, self.name), StockOverProductionError) + frappe.throw( + _("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format( + self.meta.get_label(fieldname), qty, completed_qty, self.name + ), + StockOverProductionError, + ) self.db_set(fieldname, qty) self.set_process_loss_qty() @@ -247,7 +307,9 @@ class WorkOrder(Document): self.update_production_plan_status() def set_process_loss_qty(self): - process_loss_qty = flt(frappe.db.sql(""" + process_loss_qty = flt( + frappe.db.sql( + """ SELECT sum(qty) FROM `tabStock Entry Detail` WHERE is_process_loss=1 @@ -258,21 +320,33 @@ class WorkOrder(Document): AND purpose='Manufacture' AND docstatus=1 ) - """, (self.name, ))[0][0]) + """, + (self.name,), + )[0][0] + ) if process_loss_qty is not None: - self.db_set('process_loss_qty', process_loss_qty) + self.db_set("process_loss_qty", process_loss_qty) def update_production_plan_status(self): - production_plan = frappe.get_doc('Production Plan', self.production_plan) + production_plan = frappe.get_doc("Production Plan", self.production_plan) produced_qty = 0 if self.production_plan_item: - total_qty = frappe.get_all("Work Order", fields = "sum(produced_qty) as produced_qty", - filters = {'docstatus': 1, 'production_plan': self.production_plan, - 'production_plan_item': self.production_plan_item}, as_list=1) + total_qty = frappe.get_all( + "Work Order", + fields="sum(produced_qty) as produced_qty", + filters={ + "docstatus": 1, + "production_plan": self.production_plan, + "production_plan_item": self.production_plan_item, + }, + as_list=1, + ) produced_qty = total_qty[0][0] if total_qty else 0 - production_plan.run_method("update_produced_pending_qty", produced_qty, self.production_plan_item) + production_plan.run_method( + "update_produced_pending_qty", produced_qty, self.production_plan_item + ) def before_submit(self): self.create_serial_no_batch_no() @@ -283,7 +357,9 @@ class WorkOrder(Document): if not self.fg_warehouse: frappe.throw(_("For Warehouse is required before Submit")) - if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): + if self.production_plan and frappe.db.exists( + "Production Plan Item Reference", {"parent": self.production_plan} + ): self.update_work_order_qty_in_combined_so() else: self.update_work_order_qty_in_so() @@ -296,9 +372,11 @@ class WorkOrder(Document): def on_cancel(self): self.validate_cancel() - frappe.db.set(self,'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") - if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}): + if self.production_plan and frappe.db.exists( + "Production Plan Item Reference", {"parent": self.production_plan} + ): self.update_work_order_qty_in_combined_so() else: self.update_work_order_qty_in_so() @@ -314,16 +392,15 @@ class WorkOrder(Document): if not (self.has_serial_no or self.has_batch_no): return - if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + if not cint( + frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") + ): return if self.has_batch_no: self.create_batch_for_finished_good() - args = { - "item_code": self.production_item, - "work_order": self.name - } + args = {"item_code": self.production_item, "work_order": self.name} if self.has_serial_no: self.make_serial_nos(args) @@ -336,9 +413,12 @@ class WorkOrder(Document): batch_auto_creation = frappe.get_cached_value("Item", self.production_item, "create_new_batch") if not batch_auto_creation: frappe.msgprint( - _("Batch not created for item {} since it does not have a batch series.") - .format(frappe.bold(self.production_item)), - alert=True, indicator="orange") + _("Batch not created for item {} since it does not have a batch series.").format( + frappe.bold(self.production_item) + ), + alert=True, + indicator="orange", + ) return while total_qty > 0: @@ -352,19 +432,23 @@ class WorkOrder(Document): qty = total_qty total_qty = 0 - make_batch(frappe._dict({ - "item": self.production_item, - "qty_to_produce": qty, - "reference_doctype": self.doctype, - "reference_name": self.name - })) + make_batch( + frappe._dict( + { + "item": self.production_item, + "qty_to_produce": qty, + "reference_doctype": self.doctype, + "reference_name": self.name, + } + ) + ) def delete_auto_created_batch_and_serial_no(self): - for row in frappe.get_all("Serial No", filters = {"work_order": self.name}): + for row in frappe.get_all("Serial No", filters={"work_order": self.name}): frappe.delete_doc("Serial No", row.name) self.db_set("serial_no", "") - for row in frappe.get_all("Batch", filters = {"reference_name": self.name}): + for row in frappe.get_all("Batch", filters={"reference_name": self.name}): frappe.delete_doc("Batch", row.name) def make_serial_nos(self, args): @@ -379,8 +463,12 @@ class WorkOrder(Document): serial_nos_length = len(get_serial_nos(self.serial_no)) if serial_nos_length != self.qty: - frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.") - .format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError) + frappe.throw( + _("{0} Serial Numbers required for Item {1}. You have provided {2}.").format( + self.qty, self.production_item, serial_nos_length + ), + SerialNoQtyError, + ) def create_job_card(self): manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") @@ -393,8 +481,7 @@ class WorkOrder(Document): while qty > 0: qty = split_qty_based_on_batch_size(self, row, qty) if row.job_card_qty > 0: - self.prepare_data_for_job_card(row, index, - plan_days, enable_capacity_planning) + self.prepare_data_for_job_card(row, index, plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: @@ -404,12 +491,14 @@ class WorkOrder(Document): self.set_operation_start_end_time(index, row) if not row.workstation: - frappe.throw(_("Row {0}: select the workstation against the operation {1}") - .format(row.idx, row.operation)) + frappe.throw( + _("Row {0}: select the workstation against the operation {1}").format(row.idx, row.operation) + ) original_start_time = row.planned_start_time - job_card_doc = create_job_card(self, row, auto_create=True, - enable_capacity_planning=enable_capacity_planning) + job_card_doc = create_job_card( + self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning + ) if enable_capacity_planning and job_card_doc: row.planned_start_time = job_card_doc.time_logs[-1].from_time @@ -417,22 +506,29 @@ class WorkOrder(Document): if date_diff(row.planned_start_time, original_start_time) > plan_days: frappe.message_log.pop() - frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") - .format(plan_days, row.operation), CapacityError) + frappe.throw( + _("Unable to find the time slot in the next {0} days for the operation {1}.").format( + plan_days, row.operation + ), + CapacityError, + ) row.db_update() def set_operation_start_end_time(self, idx, row): """Set start and end time for given operation. If first operation, set start as `planned_start_date`, else add time diff to end time of earlier operation.""" - if idx==0: + if idx == 0: # first operation at planned_start date row.planned_start_time = self.planned_start_date else: - row.planned_start_time = get_datetime(self.operations[idx-1].planned_end_time)\ - + get_mins_between_operations() + row.planned_start_time = ( + get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations() + ) - row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta(minutes = row.time_in_mins) + row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta( + minutes=row.time_in_mins + ) if row.planned_start_time == row.planned_end_time: frappe.throw(_("Capacity Planning Error, planned start time can not be same as end time")) @@ -442,23 +538,35 @@ class WorkOrder(Document): frappe.throw(_("Stopped Work Order cannot be cancelled, Unstop it first to cancel")) # Check whether any stock entry exists against this Work Order - stock_entry = frappe.db.sql("""select name from `tabStock Entry` - where work_order = %s and docstatus = 1""", self.name) + stock_entry = frappe.db.sql( + """select name from `tabStock Entry` + where work_order = %s and docstatus = 1""", + self.name, + ) if stock_entry: - frappe.throw(_("Cannot cancel because submitted Stock Entry {0} exists").format(frappe.utils.get_link_to_form('Stock Entry', stock_entry[0][0]))) + frappe.throw( + _("Cannot cancel because submitted Stock Entry {0} exists").format( + frappe.utils.get_link_to_form("Stock Entry", stock_entry[0][0]) + ) + ) def update_planned_qty(self): - update_bin_qty(self.production_item, self.fg_warehouse, { - "planned_qty": get_planned_qty(self.production_item, self.fg_warehouse) - }) + update_bin_qty( + self.production_item, + self.fg_warehouse, + {"planned_qty": get_planned_qty(self.production_item, self.fg_warehouse)}, + ) if self.material_request: mr_obj = frappe.get_doc("Material Request", self.material_request) mr_obj.update_requested_qty([self.material_request_item]) def update_ordered_qty(self): - if self.production_plan and self.production_plan_item \ - and not self.production_plan_sub_assembly_item: + if ( + self.production_plan + and self.production_plan_item + and not self.production_plan_sub_assembly_item + ): qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0 if self.docstatus == 1: @@ -466,12 +574,11 @@ class WorkOrder(Document): elif self.docstatus == 2: qty -= self.qty - frappe.db.set_value('Production Plan Item', - self.production_plan_item, 'ordered_qty', qty) + frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) - doc = frappe.get_doc('Production Plan', self.production_plan) + doc = frappe.get_doc("Production Plan", self.production_plan) doc.set_status() - doc.db_set('status', doc.status) + doc.db_set("status", doc.status) def update_work_order_qty_in_so(self): if not self.sales_order and not self.sales_order_item: @@ -479,8 +586,11 @@ class WorkOrder(Document): total_bundle_qty = 1 if self.product_bundle_item: - total_bundle_qty = frappe.db.sql(""" select sum(qty) from - `tabProduct Bundle Item` where parent = %s""", (frappe.db.escape(self.product_bundle_item)))[0][0] + total_bundle_qty = frappe.db.sql( + """ select sum(qty) from + `tabProduct Bundle Item` where parent = %s""", + (frappe.db.escape(self.product_bundle_item)), + )[0][0] if not total_bundle_qty: # product bundle is 0 (product bundle allows 0 qty for items) @@ -488,45 +598,63 @@ class WorkOrder(Document): cond = "product_bundle_item = %s" if self.product_bundle_item else "production_item = %s" - qty = frappe.db.sql(""" select sum(qty) from + qty = frappe.db.sql( + """ select sum(qty) from `tabWork Order` where sales_order = %s and docstatus = 1 and {0} - """.format(cond), (self.sales_order, (self.product_bundle_item or self.production_item)), as_list=1) + """.format( + cond + ), + (self.sales_order, (self.product_bundle_item or self.production_item)), + as_list=1, + ) work_order_qty = qty[0][0] if qty and qty[0][0] else 0 - frappe.db.set_value('Sales Order Item', - self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2)) + frappe.db.set_value( + "Sales Order Item", + self.sales_order_item, + "work_order_qty", + flt(work_order_qty / total_bundle_qty, 2), + ) def update_work_order_qty_in_combined_so(self): total_bundle_qty = 1 if self.product_bundle_item: - total_bundle_qty = frappe.db.sql(""" select sum(qty) from - `tabProduct Bundle Item` where parent = %s""", (frappe.db.escape(self.product_bundle_item)))[0][0] + total_bundle_qty = frappe.db.sql( + """ select sum(qty) from + `tabProduct Bundle Item` where parent = %s""", + (frappe.db.escape(self.product_bundle_item)), + )[0][0] if not total_bundle_qty: # product bundle is 0 (product bundle allows 0 qty for items) total_bundle_qty = 1 - prod_plan = frappe.get_doc('Production Plan', self.production_plan) - item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item') + prod_plan = frappe.get_doc("Production Plan", self.production_plan) + item_reference = frappe.get_value( + "Production Plan Item", self.production_plan_item, "sales_order_item" + ) for plan_reference in prod_plan.prod_plan_references: work_order_qty = 0.0 if plan_reference.item_reference == item_reference: if self.docstatus == 1: work_order_qty = flt(plan_reference.qty) / total_bundle_qty - frappe.db.set_value('Sales Order Item', - plan_reference.sales_order_item, 'work_order_qty', work_order_qty) + frappe.db.set_value( + "Sales Order Item", plan_reference.sales_order_item, "work_order_qty", work_order_qty + ) def update_completed_qty_in_material_request(self): if self.material_request: - frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item]) + frappe.get_doc("Material Request", self.material_request).update_completed_qty( + [self.material_request_item] + ) def set_work_order_operations(self): """Fetch operations from BOM and set in 'Work Order'""" def _get_operations(bom_no, qty=1): return frappe.db.sql( - f"""select + f"""select operation, description, workstation, idx, base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins, "Pending" as status, parent as bom, batch_size, sequence_id @@ -534,11 +662,13 @@ class WorkOrder(Document): `tabBOM Operation` where parent = %s order by idx - """, bom_no, as_dict=1) + """, + bom_no, + as_dict=1, + ) - - self.set('operations', []) - if not self.bom_no or not frappe.get_cached_value('BOM', self.bom_no, 'with_operations'): + self.set("operations", []) + if not self.bom_no or not frappe.get_cached_value("BOM", self.bom_no, "with_operations"): return operations = [] @@ -552,12 +682,12 @@ class WorkOrder(Document): operations.extend(_get_operations(node.name, qty=node.exploded_qty)) bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity") - operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty)) + operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) for correct_index, operation in enumerate(operations, start=1): operation.idx = correct_index - self.set('operations', operations) + self.set("operations", operations) self.calculate_time() def calculate_time(self): @@ -572,16 +702,27 @@ class WorkOrder(Document): holidays = {} if holiday_list not in holidays: - holiday_list_days = [getdate(d[0]) for d in frappe.get_all("Holiday", fields=["holiday_date"], - filters={"parent": holiday_list}, order_by="holiday_date", limit_page_length=0, as_list=1)] + holiday_list_days = [ + getdate(d[0]) + for d in frappe.get_all( + "Holiday", + fields=["holiday_date"], + filters={"parent": holiday_list}, + order_by="holiday_date", + limit_page_length=0, + as_list=1, + ) + ] holidays[holiday_list] = holiday_list_days return holidays[holiday_list] def update_operation_status(self): - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")) - max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage/100 * flt(self.qty)) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) + max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty)) for d in self.get("operations"): if not d.completed_qty: @@ -597,7 +738,9 @@ class WorkOrder(Document): def set_actual_dates(self): if self.get("operations"): - actual_start_dates = [d.actual_start_time for d in self.get("operations") if d.actual_start_time] + actual_start_dates = [ + d.actual_start_time for d in self.get("operations") if d.actual_start_time + ] if actual_start_dates: self.actual_start_date = min(actual_start_dates) @@ -605,20 +748,21 @@ class WorkOrder(Document): if actual_end_dates: self.actual_end_date = max(actual_end_dates) else: - data = frappe.get_all("Stock Entry", - fields = ["timestamp(posting_date, posting_time) as posting_datetime"], - filters = { + data = frappe.get_all( + "Stock Entry", + fields=["timestamp(posting_date, posting_time) as posting_datetime"], + filters={ "work_order": self.name, - "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]) - } + "purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]), + }, ) if data and len(data): dates = [d.posting_datetime for d in data] - self.db_set('actual_start_date', min(dates)) + self.db_set("actual_start_date", min(dates)) if self.status == "Completed": - self.db_set('actual_end_date', max(dates)) + self.db_set("actual_end_date", max(dates)) self.set_lead_time() @@ -641,24 +785,39 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) - if self.production_plan and self.production_plan_item \ - and not self.production_plan_sub_assembly_item: - qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1) + if ( + self.production_plan + and self.production_plan_item + and not self.production_plan_sub_assembly_item + ): + qty_dict = frappe.db.get_value( + "Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1 + ) if not qty_dict: return - allowance_qty = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0) + allowance_qty = ( + flt( + frappe.db.get_single_value( + "Manufacturing Settings", "overproduction_percentage_for_work_order" + ) + ) + / 100 + * qty_dict.get("planned_qty", 0) + ) max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0) if not max_qty > 0: - frappe.throw(_("Cannot produce more item for {0}") - .format(self.production_item), OverProductionError) + frappe.throw( + _("Cannot produce more item for {0}").format(self.production_item), OverProductionError + ) elif self.qty > max_qty: - frappe.throw(_("Cannot produce more than {0} items for {1}") - .format(max_qty, self.production_item), OverProductionError) + frappe.throw( + _("Cannot produce more than {0} items for {1}").format(max_qty, self.production_item), + OverProductionError, + ) def validate_transfer_against(self): if not self.docstatus == 1: @@ -667,8 +826,10 @@ class WorkOrder(Document): if not self.operations: self.transfer_material_against = "Work Order" if not self.transfer_material_against: - frappe.throw(_("Setting {} is required").format(self.meta.get_label("transfer_material_against")), title=_("Missing value")) - + frappe.throw( + _("Setting {} is required").format(self.meta.get_label("transfer_material_against")), + title=_("Missing value"), + ) def validate_operation_time(self): for d in self.operations: @@ -676,14 +837,14 @@ class WorkOrder(Document): frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation)) def update_required_items(self): - ''' + """ update bin reserved_qty_for_production called from Stock Entry for production, after submit, cancel - ''' + """ # calculate consumed qty based on submitted stock entries self.update_consumed_qty_for_required_items() - if self.docstatus==1: + if self.docstatus == 1: # calculate transferred qty based on submitted stock entries self.update_transferred_qty_for_required_items() @@ -691,7 +852,7 @@ class WorkOrder(Document): self.update_reserved_qty_for_production() def update_reserved_qty_for_production(self, items=None): - '''update reserved_qty_for_production in bins''' + """update reserved_qty_for_production in bins""" for d in self.required_items: if d.source_warehouse: stock_bin = get_bin(d.item_code, d.source_warehouse) @@ -713,17 +874,18 @@ class WorkOrder(Document): d.available_qty_at_wip_warehouse = get_latest_stock_qty(d.item_code, self.wip_warehouse) def set_required_items(self, reset_only_qty=False): - '''set required_items for production to keep track of reserved qty''' + """set required_items for production to keep track of reserved qty""" if not reset_only_qty: self.required_items = [] operation = None - if self.get('operations') and len(self.operations) == 1: + if self.get("operations") and len(self.operations) == 1: operation = self.operations[0].operation if self.bom_no and self.qty: - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty, - fetch_exploded = self.use_multi_level_bom) + item_dict = get_bom_items_as_dict( + self.bom_no, self.company, qty=self.qty, fetch_exploded=self.use_multi_level_bom + ) if reset_only_qty: for d in self.get("required_items"): @@ -733,19 +895,22 @@ class WorkOrder(Document): if not d.operation: d.operation = operation else: - for item in sorted(item_dict.values(), key=lambda d: d['idx'] or float('inf')): - self.append('required_items', { - 'rate': item.rate, - 'amount': item.rate * item.qty, - 'operation': item.operation or operation, - 'item_code': item.item_code, - 'item_name': item.item_name, - 'description': item.description, - 'allow_alternative_item': item.allow_alternative_item, - 'required_qty': item.qty, - 'source_warehouse': item.source_warehouse or item.default_warehouse, - 'include_item_in_manufacturing': item.include_item_in_manufacturing - }) + for item in sorted(item_dict.values(), key=lambda d: d["idx"] or float("inf")): + self.append( + "required_items", + { + "rate": item.rate, + "amount": item.rate * item.qty, + "operation": item.operation or operation, + "item_code": item.item_code, + "item_name": item.item_name, + "description": item.description, + "allow_alternative_item": item.allow_alternative_item, + "required_qty": item.qty, + "source_warehouse": item.source_warehouse or item.default_warehouse, + "include_item_in_manufacturing": item.include_item_in_manufacturing, + }, + ) if not self.project: self.project = item.get("project") @@ -753,32 +918,33 @@ class WorkOrder(Document): self.set_available_qty() def update_transferred_qty_for_required_items(self): - '''update transferred qty from submitted stock entries for that item against - the work order''' + """update transferred qty from submitted stock entries for that item against + the work order""" for d in self.required_items: - transferred_qty = frappe.db.sql('''select sum(qty) + transferred_qty = frappe.db.sql( + """select sum(qty) from `tabStock Entry` entry, `tabStock Entry Detail` detail where entry.work_order = %(name)s and entry.purpose = "Material Transfer for Manufacture" and entry.docstatus = 1 and detail.parent = entry.name - and (detail.item_code = %(item)s or detail.original_item = %(item)s)''', { - 'name': self.name, - 'item': d.item_code - })[0][0] + and (detail.item_code = %(item)s or detail.original_item = %(item)s)""", + {"name": self.name, "item": d.item_code}, + )[0][0] - d.db_set('transferred_qty', flt(transferred_qty), update_modified = False) + d.db_set("transferred_qty", flt(transferred_qty), update_modified=False) def update_consumed_qty_for_required_items(self): - ''' - Update consumed qty from submitted stock entries - against a work order for each stock item - ''' + """ + Update consumed qty from submitted stock entries + against a work order for each stock item + """ for item in self.required_items: - consumed_qty = frappe.db.sql(''' + consumed_qty = frappe.db.sql( + """ SELECT SUM(qty) FROM @@ -793,85 +959,97 @@ class WorkOrder(Document): AND detail.s_warehouse IS NOT null AND (detail.item_code = %(item)s OR detail.original_item = %(item)s) - ''', { - 'name': self.name, - 'item': item.item_code - })[0][0] + """, + {"name": self.name, "item": item.item_code}, + )[0][0] - item.db_set('consumed_qty', flt(consumed_qty), update_modified=False) + item.db_set("consumed_qty", flt(consumed_qty), update_modified=False) @frappe.whitelist() def make_bom(self): - data = frappe.db.sql(""" select sed.item_code, sed.qty, sed.s_warehouse + data = frappe.db.sql( + """ select sed.item_code, sed.qty, sed.s_warehouse from `tabStock Entry Detail` sed, `tabStock Entry` se where se.name = sed.parent and se.purpose = 'Manufacture' and (sed.t_warehouse is null or sed.t_warehouse = '') and se.docstatus = 1 - and se.work_order = %s""", (self.name), as_dict=1) + and se.work_order = %s""", + (self.name), + as_dict=1, + ) bom = frappe.new_doc("BOM") bom.item = self.production_item bom.conversion_rate = 1 for d in data: - bom.append('items', { - 'item_code': d.item_code, - 'qty': d.qty, - 'source_warehouse': d.s_warehouse - }) + bom.append("items", {"item_code": d.item_code, "qty": d.qty, "source_warehouse": d.s_warehouse}) if self.operations: - bom.set('operations', self.operations) + bom.set("operations", self.operations) bom.with_operations = 1 bom.set_bom_material_details() return bom def update_batch_produced_qty(self, stock_entry_doc): - if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")): + if not cint( + frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") + ): return for row in stock_entry_doc.items: if row.batch_no and (row.is_finished_item or row.is_scrap_item): - qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1}, - or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0] + qty = frappe.get_all( + "Stock Entry Detail", + filters={"batch_no": row.batch_no, "docstatus": 1}, + or_filters={"is_finished_item": 1, "is_scrap_item": 1}, + fields=["sum(qty)"], + as_list=1, + )[0][0] frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty)) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): if txt: - filters['operation'] = ('like', '%%%s%%' % txt) + filters["operation"] = ("like", "%%%s%%" % txt) + + return frappe.get_all("BOM Operation", filters=filters, fields=["operation"], as_list=1) - return frappe.get_all('BOM Operation', - filters = filters, fields = ['operation'], as_list=1) @frappe.whitelist() -def get_item_details(item, project = None, skip_bom_info=False): - res = frappe.db.sql(""" +def get_item_details(item, project=None, skip_bom_info=False): + res = frappe.db.sql( + """ select stock_uom, description, item_name, allow_alternative_item, include_item_in_manufacturing from `tabItem` where disabled=0 and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %s) and name=%s - """, (nowdate(), item), as_dict=1) + """, + (nowdate(), item), + as_dict=1, + ) if not res: return {} res = res[0] - if skip_bom_info: return res + if skip_bom_info: + return res filters = {"item": item, "is_default": 1, "docstatus": 1} if project: filters = {"item": item, "project": project} - res["bom_no"] = frappe.db.get_value("BOM", filters = filters) + res["bom_no"] = frappe.db.get_value("BOM", filters=filters) if not res["bom_no"]: - variant_of= frappe.db.get_value("Item", item, "variant_of") + variant_of = frappe.db.get_value("Item", item, "variant_of") if variant_of: res["bom_no"] = frappe.db.get_value("BOM", filters={"item": variant_of, "is_default": 1}) @@ -879,19 +1057,26 @@ def get_item_details(item, project = None, skip_bom_info=False): if not res["bom_no"]: if project: res = get_item_details(item) - frappe.msgprint(_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1) + frappe.msgprint( + _("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1 + ) else: frappe.throw(_("Default BOM for {0} not found").format(item)) - bom_data = frappe.db.get_value('BOM', res['bom_no'], - ['project', 'allow_alternative_item', 'transfer_material_against', 'item_name'], as_dict=1) + bom_data = frappe.db.get_value( + "BOM", + res["bom_no"], + ["project", "allow_alternative_item", "transfer_material_against", "item_name"], + as_dict=1, + ) - res['project'] = project or bom_data.pop("project") + res["project"] = project or bom_data.pop("project") res.update(bom_data) res.update(check_if_scrap_warehouse_mandatory(res["bom_no"])) return res + @frappe.whitelist() def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): if not frappe.has_permission("Work Order", "write"): @@ -913,43 +1098,51 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): return wo_doc + def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): if isinstance(variant_items, str): variant_items = json.loads(variant_items) for item in variant_items: - args = frappe._dict({ - "item_code": item.get("variant_item_code"), - "required_qty": item.get("qty"), - "qty": item.get("qty"), # for bom - "source_warehouse": item.get("source_warehouse"), - "operation": item.get("operation") - }) + args = frappe._dict( + { + "item_code": item.get("variant_item_code"), + "required_qty": item.get("qty"), + "qty": item.get("qty"), # for bom + "source_warehouse": item.get("source_warehouse"), + "operation": item.get("operation"), + } + ) bom_doc = frappe.get_cached_doc("BOM", bom_no) item_data = get_item_details(args.item_code, skip_bom_info=True) args.update(item_data) - args["rate"] = get_bom_item_rate({ - "company": wo_doc.company, - "item_code": args.get("item_code"), - "qty": args.get("required_qty"), - "uom": args.get("stock_uom"), - "stock_uom": args.get("stock_uom"), - "conversion_factor": 1 - }, bom_doc) + args["rate"] = get_bom_item_rate( + { + "company": wo_doc.company, + "item_code": args.get("item_code"), + "qty": args.get("required_qty"), + "uom": args.get("stock_uom"), + "stock_uom": args.get("stock_uom"), + "conversion_factor": 1, + }, + bom_doc, + ) if not args.source_warehouse: - args["source_warehouse"] = get_item_defaults(item.get("variant_item_code"), - wo_doc.company).default_warehouse + args["source_warehouse"] = get_item_defaults( + item.get("variant_item_code"), wo_doc.company + ).default_warehouse args["amount"] = flt(args.get("required_qty")) * flt(args.get("rate")) args["uom"] = item_data.stock_uom wo_doc.append(table_name, args) + @frappe.whitelist() def check_if_scrap_warehouse_mandatory(bom_no): - res = {"set_scrap_wh_mandatory": False } + res = {"set_scrap_wh_mandatory": False} if bom_no: bom = frappe.get_doc("BOM", bom_no) @@ -958,12 +1151,14 @@ def check_if_scrap_warehouse_mandatory(bom_no): return res + @frappe.whitelist() def set_work_order_ops(name): - po = frappe.get_doc('Work Order', name) + po = frappe.get_doc("Work Order", name) po.set_work_order_operations() po.save() + @frappe.whitelist() def make_stock_entry(work_order_id, purpose, qty=None): work_order = frappe.get_doc("Work Order", work_order_id) @@ -981,10 +1176,11 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.use_multi_level_bom = work_order.use_multi_level_bom stock_entry.fg_completed_qty = qty or (flt(work_order.qty) - flt(work_order.produced_qty)) if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value('BOM', - work_order.bom_no, 'inspection_required') + stock_entry.inspection_required = frappe.db.get_value( + "BOM", work_order.bom_no, "inspection_required" + ) - if purpose=="Material Transfer for Manufacture": + if purpose == "Material Transfer for Manufacture": stock_entry.to_warehouse = wip_warehouse stock_entry.project = work_order.project else: @@ -997,6 +1193,7 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.set_serial_no_batch_for_finished_good() return stock_entry.as_dict() + @frappe.whitelist() def get_default_warehouse(): doc = frappe.get_cached_doc("Manufacturing Settings") @@ -1004,12 +1201,13 @@ def get_default_warehouse(): return { "wip_warehouse": doc.default_wip_warehouse, "fg_warehouse": doc.default_fg_warehouse, - "scrap_warehouse": doc.default_scrap_warehouse + "scrap_warehouse": doc.default_scrap_warehouse, } + @frappe.whitelist() def stop_unstop(work_order, status): - """ Called from client side on Stop/Unstop event""" + """Called from client side on Stop/Unstop event""" if not frappe.has_permission("Work Order", "write"): frappe.throw(_("Not permitted"), frappe.PermissionError) @@ -1026,24 +1224,29 @@ def stop_unstop(work_order, status): return pro_order.status + @frappe.whitelist() def query_sales_order(production_item): - out = frappe.db.sql_list(""" + out = frappe.db.sql_list( + """ select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1 union select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1 - """, (production_item, production_item)) + """, + (production_item, production_item), + ) return out + @frappe.whitelist() def make_job_card(work_order, operations): if isinstance(operations, str): operations = json.loads(operations) - work_order = frappe.get_doc('Work Order', work_order) + work_order = frappe.get_doc("Work Order", work_order) for row in operations: row = frappe._dict(row) validate_operation_data(row) @@ -1053,6 +1256,7 @@ def make_job_card(work_order, operations): if row.job_card_qty > 0: create_job_card(work_order, row, auto_create=True) + @frappe.whitelist() def close_work_order(work_order, status): if not frappe.has_permission("Work Order", "write"): @@ -1060,15 +1264,17 @@ def close_work_order(work_order, status): work_order = frappe.get_doc("Work Order", work_order) if work_order.get("operations"): - job_cards = frappe.get_list("Job Card", - filters={ - "work_order": work_order.name, - "status": "Work In Progress" - }, pluck='name') + job_cards = frappe.get_list( + "Job Card", filters={"work_order": work_order.name, "status": "Work In Progress"}, pluck="name" + ) if job_cards: job_cards = ", ".join(job_cards) - frappe.throw(_("Can not close Work Order. Since {0} Job Cards are in Work In Progress state.").format(job_cards)) + frappe.throw( + _("Can not close Work Order. Since {0} Job Cards are in Work In Progress state.").format( + job_cards + ) + ) work_order.update_status(status) work_order.update_planned_qty() @@ -1076,9 +1282,11 @@ def close_work_order(work_order, status): work_order.notify_update() return work_order.status + def split_qty_based_on_batch_size(wo_doc, row, qty): - if not cint(frappe.db.get_value("Operation", - row.operation, "create_job_card_based_on_batch_size")): + if not cint( + frappe.db.get_value("Operation", row.operation, "create_job_card_based_on_batch_size") + ): row.batch_size = row.get("qty") or wo_doc.qty row.job_card_qty = row.batch_size @@ -1092,55 +1300,63 @@ def split_qty_based_on_batch_size(wo_doc, row, qty): return qty + def get_serial_nos_for_job_card(row, wo_doc): if not wo_doc.serial_no: return serial_nos = get_serial_nos(wo_doc.serial_no) used_serial_nos = [] - for d in frappe.get_all('Job Card', fields=['serial_no'], - filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}): + for d in frappe.get_all( + "Job Card", + fields=["serial_no"], + filters={"docstatus": ("<", 2), "work_order": wo_doc.name, "operation_id": row.name}, + ): used_serial_nos.extend(get_serial_nos(d.serial_no)) serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) - row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty]) + row.serial_no = "\n".join(serial_nos[0 : row.job_card_qty]) + def validate_operation_data(row): if row.get("qty") <= 0: - frappe.throw(_("Quantity to Manufacture can not be zero for the operation {0}") - .format( + frappe.throw( + _("Quantity to Manufacture can not be zero for the operation {0}").format( frappe.bold(row.get("operation")) ) ) if row.get("qty") > row.get("pending_qty"): - frappe.throw(_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})") - .format( + frappe.throw( + _("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format( frappe.bold(row.get("operation")), frappe.bold(row.get("qty")), - frappe.bold(row.get("pending_qty")) + frappe.bold(row.get("pending_qty")), ) ) + def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") - doc.update({ - 'work_order': work_order.name, - 'operation': row.get("operation"), - 'workstation': row.get("workstation"), - 'posting_date': nowdate(), - 'for_quantity': row.job_card_qty or work_order.get('qty', 0), - 'operation_id': row.get("name"), - 'bom_no': work_order.bom_no, - 'project': work_order.project, - 'company': work_order.company, - 'sequence_id': row.get("sequence_id"), - 'wip_warehouse': work_order.wip_warehouse, - 'hour_rate': row.get("hour_rate"), - 'serial_no': row.get("serial_no") - }) + doc.update( + { + "work_order": work_order.name, + "operation": row.get("operation"), + "workstation": row.get("workstation"), + "posting_date": nowdate(), + "for_quantity": row.job_card_qty or work_order.get("qty", 0), + "operation_id": row.get("name"), + "bom_no": work_order.bom_no, + "project": work_order.project, + "company": work_order.company, + "sequence_id": row.get("sequence_id"), + "wip_warehouse": work_order.wip_warehouse, + "hour_rate": row.get("hour_rate"), + "serial_no": row.get("serial_no"), + } + ) - if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer: + if work_order.transfer_material_against == "Job Card" and not work_order.skip_transfer: doc.get_required_items() if auto_create: @@ -1149,7 +1365,9 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create doc.schedule_time_logs(row) doc.insert() - frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True) + frappe.msgprint( + _("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True + ) if enable_capacity_planning: # automatically added scheduling rows shouldn't change status to WIP @@ -1157,15 +1375,18 @@ def create_job_card(work_order, row, enable_capacity_planning=False, auto_create return doc + def get_work_order_operation_data(work_order, operation, workstation): for d in work_order.operations: if d.operation == operation and d.workstation == workstation: return d + @frappe.whitelist() def create_pick_list(source_name, target_doc=None, for_qty=None): - for_qty = for_qty or json.loads(target_doc).get('for_qty') - max_finished_goods_qty = frappe.db.get_value('Work Order', source_name, 'qty') + for_qty = for_qty or json.loads(target_doc).get("for_qty") + max_finished_goods_qty = frappe.db.get_value("Work Order", source_name, "qty") + def update_item_quantity(source, target, source_parent): pending_to_issue = flt(source.required_qty) - flt(source.transferred_qty) desire_to_transfer = flt(source.required_qty) / max_finished_goods_qty * flt(for_qty) @@ -1179,25 +1400,25 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): if qty: target.qty = qty target.stock_qty = qty - target.uom = frappe.get_value('Item', source.item_code, 'stock_uom') + target.uom = frappe.get_value("Item", source.item_code, "stock_uom") target.stock_uom = target.uom target.conversion_factor = 1 else: target.delete() - doc = get_mapped_doc('Work Order', source_name, { - 'Work Order': { - 'doctype': 'Pick List', - 'validation': { - 'docstatus': ['=', 1] - } + doc = get_mapped_doc( + "Work Order", + source_name, + { + "Work Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Work Order Item": { + "doctype": "Pick List Item", + "postprocess": update_item_quantity, + "condition": lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty), + }, }, - 'Work Order Item': { - 'doctype': 'Pick List Item', - 'postprocess': update_item_quantity, - 'condition': lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty) - }, - }, target_doc) + target_doc, + ) doc.for_qty = for_qty @@ -1205,26 +1426,31 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): return doc + def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: """Get total reserved quantity for any item in specified warehouse""" wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") return ( - frappe.qb - .from_(wo) + frappe.qb.from_(wo) .from_(wo_item) - .select(Sum(Case() + .select( + Sum( + Case() .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) - .else_(wo_item.required_qty - wo_item.consumed_qty)) + .else_(wo_item.required_qty - wo_item.consumed_qty) ) + ) .where( (wo_item.item_code == item_code) & (wo_item.parent == wo.name) & (wo.docstatus == 1) & (wo_item.source_warehouse == warehouse) & (wo.status.notin(["Stopped", "Completed", "Closed"])) - & ((wo_item.required_qty > wo_item.transferred_qty) - | (wo_item.required_qty > wo_item.consumed_qty)) + & ( + (wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty) + ) ) ).run()[0][0] or 0.0 diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py index 91279d8e616..465460f95d8 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py +++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'work_order', - 'non_standard_fieldnames': { - 'Batch': 'reference_name' - }, - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Stock Entry', 'Job Card', 'Pick List'] - }, - { - 'label': _('Reference'), - 'items': ['Serial No', 'Batch'] - } - ] + "fieldname": "work_order", + "non_standard_fieldnames": {"Batch": "reference_name"}, + "transactions": [ + {"label": _("Transactions"), "items": ["Stock Entry", "Job Card", "Pick List"]}, + {"label": _("Reference"), "items": ["Serial No", "Batch"]}, + ], } diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py index 4311d3bf17f..179274707e3 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class WorkOrderItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Work Order Item", ["item_code", "source_warehouse"]) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index dd51017bb75..6db985c8c2e 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -13,19 +13,42 @@ from erpnext.manufacturing.doctype.workstation.workstation import ( ) test_dependencies = ["Warehouse"] -test_records = frappe.get_test_records('Workstation') -make_test_records('Workstation') +test_records = frappe.get_test_records("Workstation") +make_test_records("Workstation") + class TestWorkstation(FrappeTestCase): def test_validate_timings(self): - check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00") - check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00") - self.assertRaises(NotInWorkingHoursError, check_if_within_operating_hours, - "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") - self.assertRaises(NotInWorkingHoursError, check_if_within_operating_hours, - "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") - self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, - "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + check_if_within_operating_hours( + "_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00" + ) + check_if_within_operating_hours( + "_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00" + ) + self.assertRaises( + NotInWorkingHoursError, + check_if_within_operating_hours, + "_Test Workstation 1", + "Operation 1", + "2013-02-02 05:00:00", + "2013-02-02 20:00:00", + ) + self.assertRaises( + NotInWorkingHoursError, + check_if_within_operating_hours, + "_Test Workstation 1", + "Operation 1", + "2013-02-02 05:00:00", + "2013-02-02 20:00:00", + ) + self.assertRaises( + WorkstationHolidayError, + check_if_within_operating_hours, + "_Test Workstation 1", + "Operation 1", + "2013-02-01 10:00:00", + "2013-02-02 20:00:00", + ) def test_update_bom_operation_rate(self): operations = [ @@ -33,14 +56,14 @@ class TestWorkstation(FrappeTestCase): "operation": "Test Operation A", "workstation": "_Test Workstation A", "hour_rate_rent": 300, - "time_in_mins": 60 + "time_in_mins": 60, }, { "operation": "Test Operation B", "workstation": "_Test Workstation B", "hour_rate_rent": 1000, - "time_in_mins": 60 - } + "time_in_mins": 60, + }, ] for row in operations: @@ -48,21 +71,13 @@ class TestWorkstation(FrappeTestCase): make_operation(row) test_routing_operations = [ - { - "operation": "Test Operation A", - "workstation": "_Test Workstation A", - "time_in_mins": 60 - }, - { - "operation": "Test Operation B", - "workstation": "_Test Workstation A", - "time_in_mins": 60 - } + {"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 60}, + {"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 60}, ] - routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations) + routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations) bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") w1 = frappe.get_doc("Workstation", "_Test Workstation A") - #resets values + # resets values w1.hour_rate_rent = 300 w1.hour_rate_labour = 0 w1.save() @@ -72,13 +87,14 @@ class TestWorkstation(FrappeTestCase): self.assertEqual(bom_doc.operations[0].hour_rate, 300) w1.hour_rate_rent = 250 w1.save() - #updating after setting new rates in workstations + # updating after setting new rates in workstations bom_doc.update_cost() bom_doc.reload() self.assertEqual(w1.hour_rate, 250) self.assertEqual(bom_doc.operations[0].hour_rate, 250) self.assertEqual(bom_doc.operations[1].hour_rate, 250) + def make_workstation(*args, **kwargs): args = args if args else kwargs if isinstance(args, tuple): @@ -88,10 +104,7 @@ def make_workstation(*args, **kwargs): workstation_name = args.workstation_name or args.workstation if not frappe.db.exists("Workstation", workstation_name): - doc = frappe.get_doc({ - "doctype": "Workstation", - "workstation_name": workstation_name - }) + doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name}) doc.hour_rate_rent = args.get("hour_rate_rent") doc.hour_rate_labour = args.get("hour_rate_labour") doc.insert() diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 4cfd410ac72..59e5318ab8d 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -19,14 +19,26 @@ from frappe.utils import ( from erpnext.support.doctype.issue.issue import get_holidays -class WorkstationHolidayError(frappe.ValidationError): pass -class NotInWorkingHoursError(frappe.ValidationError): pass -class OverlapError(frappe.ValidationError): pass +class WorkstationHolidayError(frappe.ValidationError): + pass + + +class NotInWorkingHoursError(frappe.ValidationError): + pass + + +class OverlapError(frappe.ValidationError): + pass + class Workstation(Document): def validate(self): - self.hour_rate = (flt(self.hour_rate_labour) + flt(self.hour_rate_electricity) + - flt(self.hour_rate_consumable) + flt(self.hour_rate_rent)) + self.hour_rate = ( + flt(self.hour_rate_labour) + + flt(self.hour_rate_electricity) + + flt(self.hour_rate_consumable) + + flt(self.hour_rate_rent) + ) def on_update(self): self.validate_overlap_for_operation_timings() @@ -35,29 +47,41 @@ class Workstation(Document): def validate_overlap_for_operation_timings(self): """Check if there is no overlap in setting Workstation Operating Hours""" for d in self.get("working_hours"): - existing = frappe.db.sql_list("""select idx from `tabWorkstation Working Hour` + existing = frappe.db.sql_list( + """select idx from `tabWorkstation Working Hour` where parent = %s and name != %s and ( (start_time between %s and %s) or (end_time between %s and %s) or (%s between start_time and end_time)) - """, (self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time)) + """, + (self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time), + ) if existing: - frappe.throw(_("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError) + frappe.throw( + _("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError + ) def update_bom_operation(self): - bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` - where workstation = %s and parenttype = 'routing' """, self.name) + bom_list = frappe.db.sql( + """select DISTINCT parent from `tabBOM Operation` + where workstation = %s and parenttype = 'routing' """, + self.name, + ) for bom_no in bom_list: - frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s + frappe.db.sql( + """update `tabBOM Operation` set hour_rate = %s where parent = %s and workstation = %s""", - (self.hour_rate, bom_no[0], self.name)) + (self.hour_rate, bom_no[0], self.name), + ) def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False): - if not skip_holiday_list_check and (not self.holiday_list or - cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))): + if not skip_holiday_list_check and ( + not self.holiday_list + or cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays")) + ): return schedule_date if schedule_date in tuple(get_holidays(self.holiday_list)): @@ -66,18 +90,25 @@ class Workstation(Document): return schedule_date + @frappe.whitelist() def get_default_holiday_list(): - return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list") + return frappe.get_cached_value( + "Company", frappe.defaults.get_user_default("Company"), "default_holiday_list" + ) + def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime): if from_datetime and to_datetime: - if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): + if not cint( + frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") + ): check_workstation_for_holiday(workstation, from_datetime, to_datetime) if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")): is_within_operating_hours(workstation, operation, from_datetime, to_datetime) + def is_within_operating_hours(workstation, operation, from_datetime, to_datetime): operation_length = time_diff_in_seconds(to_datetime, from_datetime) workstation = frappe.get_doc("Workstation", workstation) @@ -87,21 +118,35 @@ def is_within_operating_hours(workstation, operation, from_datetime, to_datetime for working_hour in workstation.working_hours: if working_hour.start_time and working_hour.end_time: - slot_length = (to_timedelta(working_hour.end_time or "") - to_timedelta(working_hour.start_time or "")).total_seconds() + slot_length = ( + to_timedelta(working_hour.end_time or "") - to_timedelta(working_hour.start_time or "") + ).total_seconds() if slot_length >= operation_length: return - frappe.throw(_("Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations").format(operation, workstation.name), NotInWorkingHoursError) + frappe.throw( + _( + "Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations" + ).format(operation, workstation.name), + NotInWorkingHoursError, + ) + def check_workstation_for_holiday(workstation, from_datetime, to_datetime): holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") if holiday_list and from_datetime and to_datetime: applicable_holidays = [] - for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s + for d in frappe.db.sql( + """select holiday_date from `tabHoliday` where parent = %s and holiday_date between %s and %s """, - (holiday_list, getdate(from_datetime), getdate(to_datetime))): - applicable_holidays.append(formatdate(d[0])) + (holiday_list, getdate(from_datetime), getdate(to_datetime)), + ): + applicable_holidays.append(formatdate(d[0])) if applicable_holidays: - frappe.throw(_("Workstation is closed on the following dates as per Holiday List: {0}") - .format(holiday_list) + "\n" + "\n".join(applicable_holidays), WorkstationHolidayError) + frappe.throw( + _("Workstation is closed on the following dates as per Holiday List: {0}").format(holiday_list) + + "\n" + + "\n".join(applicable_holidays), + WorkstationHolidayError, + ) diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py index 9c0f6b8b789..6d022216bd0 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py +++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py @@ -1,20 +1,24 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'workstation', - 'transactions': [ + "fieldname": "workstation", + "transactions": [ + {"label": _("Master"), "items": ["BOM", "Routing", "Operation"]}, { - 'label': _('Master'), - 'items': ['BOM', 'Routing', 'Operation'] + "label": _("Transaction"), + "items": [ + "Work Order", + "Job Card", + ], }, - { - 'label': _('Transaction'), - 'items': ['Work Order', 'Job Card',] - } ], - 'disable_create_buttons': ['BOM', 'Routing', 'Operation', - 'Work Order', 'Job Card',] + "disable_create_buttons": [ + "BOM", + "Routing", + "Operation", + "Work Order", + "Job Card", + ], } diff --git a/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py b/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py +++ b/erpnext/manufacturing/notification/material_request_receipt_notification/material_request_receipt_notification.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 19a80ab4076..c0affd9cada 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -11,30 +11,37 @@ def execute(filters=None): get_data(filters, data) return columns, data + def get_data(filters, data): get_exploded_items(filters.bom, data) + def get_exploded_items(bom, data, indent=0, qty=1): - exploded_items = frappe.get_all("BOM Item", + exploded_items = frappe.get_all( + "BOM Item", filters={"parent": bom}, - fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom']) + fields=["qty", "bom_no", "qty", "scrap", "item_code", "item_name", "description", "uom"], + ) for item in exploded_items: print(item.bom_no, indent) item["indent"] = indent - data.append({ - 'item_code': item.item_code, - 'item_name': item.item_name, - 'indent': indent, - 'bom_level': indent, - 'bom': item.bom_no, - 'qty': item.qty * qty, - 'uom': item.uom, - 'description': item.description, - 'scrap': item.scrap - }) + data.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "indent": indent, + "bom_level": indent, + "bom": item.bom_no, + "qty": item.qty * qty, + "uom": item.uom, + "description": item.description, + "scrap": item.scrap, + } + ) if item.bom_no: - get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty) + get_exploded_items(item.bom_no, data, indent=indent + 1, qty=item.qty) + def get_columns(): return [ @@ -43,49 +50,13 @@ def get_columns(): "fieldtype": "Link", "fieldname": "item_code", "width": 300, - "options": "Item" - }, - { - "label": "Item Name", - "fieldtype": "data", - "fieldname": "item_name", - "width": 100 - }, - { - "label": "BOM", - "fieldtype": "Link", - "fieldname": "bom", - "width": 150, - "options": "BOM" - }, - { - "label": "Qty", - "fieldtype": "data", - "fieldname": "qty", - "width": 100 - }, - { - "label": "UOM", - "fieldtype": "data", - "fieldname": "uom", - "width": 100 - }, - { - "label": "BOM Level", - "fieldtype": "Int", - "fieldname": "bom_level", - "width": 100 - }, - { - "label": "Standard Description", - "fieldtype": "data", - "fieldname": "description", - "width": 150 - }, - { - "label": "Scrap", - "fieldtype": "data", - "fieldname": "scrap", - "width": 100 + "options": "Item", }, + {"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100}, + {"label": "BOM", "fieldtype": "Link", "fieldname": "bom", "width": 150, "options": "BOM"}, + {"label": "Qty", "fieldtype": "data", "fieldname": "qty", "width": 100}, + {"label": "UOM", "fieldtype": "data", "fieldname": "uom", "width": 100}, + {"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100}, + {"label": "Standard Description", "fieldtype": "data", "fieldname": "description", "width": 150}, + {"label": "Scrap", "fieldtype": "data", "fieldname": "scrap", "width": 100}, ] diff --git a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py index eda9eb9d701..92c69cf3e0a 100644 --- a/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py +++ b/erpnext/manufacturing/report/bom_operations_time/bom_operations_time.py @@ -11,6 +11,7 @@ def execute(filters=None): columns = get_columns(filters) return columns, data + def get_data(filters): bom_wise_data = {} bom_data, report_data = [], [] @@ -24,11 +25,9 @@ def get_data(filters): bom_data.append(d.name) row.update(d) else: - row.update({ - "operation": d.operation, - "workstation": d.workstation, - "time_in_mins": d.time_in_mins - }) + row.update( + {"operation": d.operation, "workstation": d.workstation, "time_in_mins": d.time_in_mins} + ) # maintain BOM wise data for grouping such as: # {"BOM A": [{Row1}, {Row2}], "BOM B": ...} @@ -43,20 +42,25 @@ def get_data(filters): return report_data + def get_filtered_data(filters): bom = frappe.qb.DocType("BOM") bom_ops = frappe.qb.DocType("BOM Operation") bom_ops_query = ( frappe.qb.from_(bom) - .join(bom_ops).on(bom.name == bom_ops.parent) + .join(bom_ops) + .on(bom.name == bom_ops.parent) .select( - bom.name, bom.item, bom.item_name, bom.uom, - bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins - ).where( - (bom.docstatus == 1) - & (bom.is_active == 1) + bom.name, + bom.item, + bom.item_name, + bom.uom, + bom_ops.operation, + bom_ops.workstation, + bom_ops.time_in_mins, ) + .where((bom.docstatus == 1) & (bom.is_active == 1)) ) if filters.get("item_code"): @@ -66,18 +70,20 @@ def get_filtered_data(filters): bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id"))) if filters.get("workstation"): - bom_ops_query = bom_ops_query.where( - bom_ops.workstation == filters.get("workstation") - ) + bom_ops_query = bom_ops_query.where(bom_ops.workstation == filters.get("workstation")) bom_operation_data = bom_ops_query.run(as_dict=True) return bom_operation_data + def get_bom_count(bom_data): - data = frappe.get_all("BOM Item", + data = frappe.get_all( + "BOM Item", fields=["count(name) as count", "bom_no"], - filters= {"bom_no": ("in", bom_data)}, group_by = "bom_no") + filters={"bom_no": ("in", bom_data)}, + group_by="bom_no", + ) bom_count = {} for d in data: @@ -85,58 +91,42 @@ def get_bom_count(bom_data): return bom_count + def get_args(): - return frappe._dict({ - "name": "", - "item": "", - "item_name": "", - "uom": "" - }) + return frappe._dict({"name": "", "item": "", "item_name": "", "uom": ""}) + def get_columns(filters): - return [{ - "label": _("BOM ID"), - "options": "BOM", - "fieldname": "name", - "fieldtype": "Link", - "width": 220 - }, { - "label": _("Item Code"), - "options": "Item", - "fieldname": "item", - "fieldtype": "Link", - "width": 150 - }, { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 110 - }, { - "label": _("UOM"), - "options": "UOM", - "fieldname": "uom", - "fieldtype": "Link", - "width": 100 - }, { - "label": _("Operation"), - "options": "Operation", - "fieldname": "operation", - "fieldtype": "Link", - "width": 140 - }, { - "label": _("Workstation"), - "options": "Workstation", - "fieldname": "workstation", - "fieldtype": "Link", - "width": 110 - }, { - "label": _("Time (In Mins)"), - "fieldname": "time_in_mins", - "fieldtype": "Float", - "width": 120 - }, { - "label": _("Sub-assembly BOM Count"), - "fieldname": "used_as_subassembly_items", - "fieldtype": "Int", - "width": 200 - }] + return [ + {"label": _("BOM ID"), "options": "BOM", "fieldname": "name", "fieldtype": "Link", "width": 220}, + { + "label": _("Item Code"), + "options": "Item", + "fieldname": "item", + "fieldtype": "Link", + "width": 150, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 110}, + {"label": _("UOM"), "options": "UOM", "fieldname": "uom", "fieldtype": "Link", "width": 100}, + { + "label": _("Operation"), + "options": "Operation", + "fieldname": "operation", + "fieldtype": "Link", + "width": 140, + }, + { + "label": _("Workstation"), + "options": "Workstation", + "fieldname": "workstation", + "fieldtype": "Link", + "width": 110, + }, + {"label": _("Time (In Mins)"), "fieldname": "time_in_mins", "fieldtype": "Float", "width": 120}, + { + "label": _("Sub-assembly BOM Count"), + "fieldname": "used_as_subassembly_items", + "fieldtype": "Int", + "width": 200, + }, + ] diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py index 26933523246..933be3e0140 100644 --- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py +++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py @@ -23,14 +23,24 @@ def execute(filters=None): summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details)) return columns, summ_data + def get_report_data(last_pur_price, reqd_qty, row, manufacture_details): to_build = row.to_build if row.to_build > 0 else 0 diff_qty = to_build - reqd_qty - return [row.item_code, row.description, - comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer', []), add_quotes=False), - comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer_part', []), add_quotes=False), - row.actual_qty, str(to_build), - reqd_qty, diff_qty, last_pur_price] + return [ + row.item_code, + row.description, + comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False), + comma_and( + manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False + ), + row.actual_qty, + str(to_build), + reqd_qty, + diff_qty, + last_pur_price, + ] + def get_columns(): """return columns""" @@ -41,12 +51,13 @@ def get_columns(): _("Manufacturer Part Number") + "::250", _("Qty") + ":Float:50", _("Stock Qty") + ":Float:100", - _("Reqd Qty")+ ":Float:100", - _("Diff Qty")+ ":Float:100", - _("Last Purchase Price")+ ":Float:100", + _("Reqd Qty") + ":Float:100", + _("Diff Qty") + ":Float:100", + _("Last Purchase Price") + ":Float:100", ] return columns + def get_bom_stock(filters): conditions = "" bom = filters.get("bom") @@ -59,18 +70,23 @@ def get_bom_stock(filters): qty_field = "stock_qty" if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" % (warehouse_details.lft, - warehouse_details.rgt) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) else: conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse")) else: conditions += "" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT bom_item.item_code, bom_item.description, @@ -86,14 +102,21 @@ def get_bom_stock(filters): WHERE bom_item.parent = '{bom}' and bom_item.parenttype='BOM' - GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) + GROUP BY bom_item.item_code""".format( + qty_field=qty_field, table=table, conditions=conditions, bom=bom + ), + as_dict=1, + ) + def get_manufacturer_records(): - details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"]) + details = frappe.get_all( + "Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"] + ) manufacture_details = frappe._dict() for detail in details: - dic = manufacture_details.setdefault(detail.get('item_code'), {}) - dic.setdefault('manufacturer', []).append(detail.get('manufacturer')) - dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no')) + dic = manufacture_details.setdefault(detail.get("item_code"), {}) + dic.setdefault("manufacturer", []).append(detail.get("manufacturer")) + dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no")) return manufacture_details diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index fa943912617..34e9826305e 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -7,7 +7,8 @@ from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() @@ -15,6 +16,7 @@ def execute(filters=None): return columns, data + def get_columns(): """return columns""" columns = [ @@ -29,6 +31,7 @@ def get_columns(): return columns + def get_bom_stock(filters): conditions = "" bom = filters.get("bom") @@ -37,25 +40,30 @@ def get_bom_stock(filters): qty_field = "stock_qty" qty_to_produce = filters.get("qty_to_produce", 1) - if int(qty_to_produce) <= 0: + if int(qty_to_produce) <= 0: frappe.throw(_("Quantity to Produce can not be less than Zero")) if filters.get("show_exploded_view"): table = "`tabBOM Explosion Item`" if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" % (warehouse_details.lft, - warehouse_details.rgt) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) else: conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse")) else: conditions += "" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT bom_item.item_code, bom_item.description , @@ -74,9 +82,10 @@ def get_bom_stock(filters): bom_item.parent = {bom} and bom_item.parenttype='BOM' GROUP BY bom_item.item_code""".format( - qty_field=qty_field, - table=table, - conditions=conditions, - bom=frappe.db.escape(bom), - qty_to_produce=qty_to_produce or 1) - ) + qty_field=qty_field, + table=table, + conditions=conditions, + bom=frappe.db.escape(bom), + qty_to_produce=qty_to_produce or 1, + ) + ) diff --git a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py index a5ae43e9add..3fe2198966c 100644 --- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py @@ -12,98 +12,99 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): - columns = [{ + columns = [ + { "label": _("Work Order"), "fieldname": "work_order", "fieldtype": "Link", "options": "Work Order", - "width": 120 - }] - - if not filters.get('bom_no'): - columns.extend([ - { - "label": _("BOM No"), - "fieldname": "bom_no", - "fieldtype": "Link", - "options": "BOM", - "width": 180 - } - ]) - - columns.extend([ - { - "label": _("Finished Good"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, - { - "label": _("Ordered Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Produced Qty"), - "fieldname": "produced_qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Raw Material"), - "fieldname": "raw_material_code", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, - { - "label": _("Required Qty"), - "fieldname": "required_qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Consumed Qty"), - "fieldname": "consumed_qty", - "fieldtype": "Float", - "width": 120 + "width": 120, } - ]) + ] + + if not filters.get("bom_no"): + columns.extend( + [ + { + "label": _("BOM No"), + "fieldname": "bom_no", + "fieldtype": "Link", + "options": "BOM", + "width": 180, + } + ] + ) + + columns.extend( + [ + { + "label": _("Finished Good"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + {"label": _("Ordered Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + {"label": _("Produced Qty"), "fieldname": "produced_qty", "fieldtype": "Float", "width": 120}, + { + "label": _("Raw Material"), + "fieldname": "raw_material_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + {"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 120}, + {"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120}, + ] + ) return columns + def get_data(filters): cond = "1=1" - if filters.get('bom_no') and not filters.get('work_order'): - cond += " and bom_no = '%s'" % filters.get('bom_no') + if filters.get("bom_no") and not filters.get("work_order"): + cond += " and bom_no = '%s'" % filters.get("bom_no") - if filters.get('work_order'): - cond += " and name = '%s'" % filters.get('work_order') + if filters.get("work_order"): + cond += " and name = '%s'" % filters.get("work_order") results = [] - for d in frappe.db.sql(""" select name as work_order, qty, produced_qty, production_item, bom_no - from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(cond), as_dict=1): + for d in frappe.db.sql( + """ select name as work_order, qty, produced_qty, production_item, bom_no + from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format( + cond + ), + as_dict=1, + ): results.append(d) - for data in frappe.get_all('Work Order Item', fields=["item_code as raw_material_code", - "required_qty", "consumed_qty"], filters={'parent': d.work_order, 'parenttype': 'Work Order'}): + for data in frappe.get_all( + "Work Order Item", + fields=["item_code as raw_material_code", "required_qty", "consumed_qty"], + filters={"parent": d.work_order, "parenttype": "Work Order"}, + ): results.append(data) return results + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_work_orders(doctype, txt, searchfield, start, page_len, filters): cond = "1=1" - if filters.get('bom_no'): - cond += " and bom_no = '%s'" % filters.get('bom_no') + if filters.get("bom_no"): + cond += " and bom_no = '%s'" % filters.get("bom_no") - return frappe.db.sql("""select name from `tabWork Order` + return frappe.db.sql( + """select name from `tabWork Order` where name like %(name)s and {0} and produced_qty > qty and docstatus = 1 - order by name limit {1}, {2}""".format(cond, start, page_len),{ - 'name': "%%%s%%" % txt - }, as_list=1) + order by name limit {1}, {2}""".format( + cond, start, page_len + ), + {"name": "%%%s%%" % txt}, + as_list=1, + ) diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py index 88b21170e8b..481fe51d739 100644 --- a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py +++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py @@ -11,58 +11,77 @@ def execute(filters=None): def get_data(report_filters): data = [] - operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1}) + operations = frappe.get_all("Operation", filters={"is_corrective_operation": 1}) if operations: - if report_filters.get('operation'): - operations = [report_filters.get('operation')] + if report_filters.get("operation"): + operations = [report_filters.get("operation")] else: operations = [d.name for d in operations] job_card = frappe.qb.DocType("Job Card") - operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost') - item_code = (job_card.production_item).as_('item_code') + operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_( + "operating_cost" + ) + item_code = (job_card.production_item).as_("item_code") - query = (frappe.qb - .from_(job_card) - .select(job_card.name, job_card.work_order, item_code, job_card.item_name, - job_card.operation, job_card.serial_no, job_card.batch_no, - job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate, - operating_cost) - .where( - (job_card.docstatus == 1) - & (job_card.is_corrective_job_card == 1)) - .groupby(job_card.name) - ) + query = ( + frappe.qb.from_(job_card) + .select( + job_card.name, + job_card.work_order, + item_code, + job_card.item_name, + job_card.operation, + job_card.serial_no, + job_card.batch_no, + job_card.workstation, + job_card.total_time_in_mins, + job_card.hour_rate, + operating_cost, + ) + .where((job_card.docstatus == 1) & (job_card.is_corrective_job_card == 1)) + .groupby(job_card.name) + ) query = append_filters(query, report_filters, operations, job_card) data = query.run(as_dict=True) return data -def append_filters(query, report_filters, operations, job_card): - """Append optional filters to query builder. """ - for field in ("name", "work_order", "operation", "workstation", - "company", "serial_no", "batch_no", "production_item"): +def append_filters(query, report_filters, operations, job_card): + """Append optional filters to query builder.""" + + for field in ( + "name", + "work_order", + "operation", + "workstation", + "company", + "serial_no", + "batch_no", + "production_item", + ): if report_filters.get(field): - if field == 'serial_no': - query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field)))) - elif field == 'operation': + if field == "serial_no": + query = query.where(job_card[field].like("%{}%".format(report_filters.get(field)))) + elif field == "operation": query = query.where(job_card[field].isin(operations)) else: query = query.where(job_card[field] == report_filters.get(field)) - if report_filters.get('from_date') or report_filters.get('to_date'): + if report_filters.get("from_date") or report_filters.get("to_date"): job_card_time_log = frappe.qb.DocType("Job Card Time Log") query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent) - if report_filters.get('from_date'): - query = query.where(job_card_time_log.from_time >= report_filters.get('from_date')) - if report_filters.get('to_date'): - query = query.where(job_card_time_log.to_time <= report_filters.get('to_date')) + if report_filters.get("from_date"): + query = query.where(job_card_time_log.from_time >= report_filters.get("from_date")) + if report_filters.get("to_date"): + query = query.where(job_card_time_log.to_time <= report_filters.get("to_date")) return query + def get_columns(filters): return [ { @@ -70,64 +89,49 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "name", "options": "Job Card", - "width": "120" + "width": "120", }, { "label": _("Work Order"), "fieldtype": "Link", "fieldname": "work_order", "options": "Work Order", - "width": "100" + "width": "100", }, { "label": _("Item Code"), "fieldtype": "Link", "fieldname": "item_code", "options": "Item", - "width": "100" - }, - { - "label": _("Item Name"), - "fieldtype": "Data", - "fieldname": "item_name", - "width": "100" + "width": "100", }, + {"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": "100"}, { "label": _("Operation"), "fieldtype": "Link", "fieldname": "operation", "options": "Operation", - "width": "100" - }, - { - "label": _("Serial No"), - "fieldtype": "Data", - "fieldname": "serial_no", - "width": "100" - }, - { - "label": _("Batch No"), - "fieldtype": "Data", - "fieldname": "batch_no", - "width": "100" + "width": "100", }, + {"label": _("Serial No"), "fieldtype": "Data", "fieldname": "serial_no", "width": "100"}, + {"label": _("Batch No"), "fieldtype": "Data", "fieldname": "batch_no", "width": "100"}, { "label": _("Workstation"), "fieldtype": "Link", "fieldname": "workstation", "options": "Workstation", - "width": "100" + "width": "100", }, { "label": _("Operating Cost"), "fieldtype": "Currency", "fieldname": "operating_cost", - "width": "150" + "width": "150", }, { "label": _("Total Time (in Mins)"), "fieldtype": "Float", "fieldname": "total_time_in_mins", - "width": "150" - } + "width": "150", + }, ] diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py index 2c515d1b36f..80a15648670 100644 --- a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.py @@ -14,10 +14,20 @@ def execute(filters=None): chart_data = get_chart_data(data, filters) return columns, data, None, chart_data + def get_data(filters): query_filters = {} - fields = ["name", "workstation", "operator", "from_time", "to_time", "downtime", "stop_reason", "remarks"] + fields = [ + "name", + "workstation", + "operator", + "from_time", + "to_time", + "downtime", + "stop_reason", + "remarks", + ] query_filters["from_time"] = (">=", filters.get("from_date")) query_filters["to_time"] = ("<=", filters.get("to_date")) @@ -25,13 +35,14 @@ def get_data(filters): if filters.get("workstation"): query_filters["workstation"] = filters.get("workstation") - data = frappe.get_all("Downtime Entry", fields= fields, filters=query_filters) or [] + data = frappe.get_all("Downtime Entry", fields=fields, filters=query_filters) or [] for d in data: if d.downtime: d.downtime = d.downtime / 60 return data + def get_chart_data(data, columns): labels = sorted(list(set([d.workstation for d in data]))) @@ -47,17 +58,13 @@ def get_chart_data(data, columns): datasets.append(workstation_wise_data.get(label, 0)) chart = { - "data": { - "labels": labels, - "datasets": [ - {"name": "Machine Downtime", "values": datasets} - ] - }, - "type": "bar" + "data": {"labels": labels, "datasets": [{"name": "Machine Downtime", "values": datasets}]}, + "type": "bar", } return chart + def get_columns(filters): return [ { @@ -65,50 +72,25 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Downtime Entry", - "width": 100 + "width": 100, }, { "label": _("Machine"), "fieldname": "workstation", "fieldtype": "Link", "options": "Workstation", - "width": 100 + "width": 100, }, { "label": _("Operator"), "fieldname": "operator", "fieldtype": "Link", "options": "Employee", - "width": 130 + "width": 130, }, - { - "label": _("From Time"), - "fieldname": "from_time", - "fieldtype": "Datetime", - "width": 160 - }, - { - "label": _("To Time"), - "fieldname": "to_time", - "fieldtype": "Datetime", - "width": 160 - }, - { - "label": _("Downtime (In Hours)"), - "fieldname": "downtime", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Stop Reason"), - "fieldname": "stop_reason", - "fieldtype": "Data", - "width": 220 - }, - { - "label": _("Remarks"), - "fieldname": "remarks", - "fieldtype": "Text", - "width": 100 - } + {"label": _("From Time"), "fieldname": "from_time", "fieldtype": "Datetime", "width": 160}, + {"label": _("To Time"), "fieldname": "to_time", "fieldtype": "Datetime", "width": 160}, + {"label": _("Downtime (In Hours)"), "fieldname": "downtime", "fieldtype": "Float", "width": 150}, + {"label": _("Stop Reason"), "fieldname": "stop_reason", "fieldtype": "Data", "width": 220}, + {"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Text", "width": 100}, ] diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index 26b3359dee1..7500744c228 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses def execute(filters=None): return ForecastingReport(filters).execute_report() + class ExponentialSmoothingForecast(object): def forecast_future_data(self): for key, value in self.period_wise_data.items(): @@ -26,24 +27,22 @@ class ExponentialSmoothingForecast(object): elif forecast_data: previous_period_data = forecast_data[-1] - value[forecast_key] = (previous_period_data[1] + - flt(self.filters.smoothing_constant) * ( - flt(previous_period_data[0]) - flt(previous_period_data[1]) - ) + value[forecast_key] = previous_period_data[1] + flt(self.filters.smoothing_constant) * ( + flt(previous_period_data[0]) - flt(previous_period_data[1]) ) if value.get(forecast_key): # will be use to forecaset next period forecast_data.append([value.get(period.key), value.get(forecast_key)]) + class ForecastingReport(ExponentialSmoothingForecast): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.data = [] self.doctype = self.filters.based_on_document self.child_doctype = self.doctype + " Item" - self.based_on_field = ("qty" - if self.filters.based_on_field == "Qty" else "amount") + self.based_on_field = "qty" if self.filters.based_on_field == "Qty" else "amount" self.fieldtype = "Float" if self.based_on_field == "qty" else "Currency" self.company_currency = erpnext.get_company_currency(self.filters.company) @@ -63,8 +62,15 @@ class ForecastingReport(ExponentialSmoothingForecast): self.period_wise_data = {} from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1) - self.period_list = get_period_list(from_date, self.filters.to_date, - from_date, self.filters.to_date, "Date Range", self.filters.periodicity, ignore_fiscal_year=True) + self.period_list = get_period_list( + from_date, + self.filters.to_date, + from_date, + self.filters.to_date, + "Date Range", + self.filters.periodicity, + ignore_fiscal_year=True, + ) order_data = self.get_data_for_forecast() or [] @@ -76,8 +82,10 @@ class ForecastingReport(ExponentialSmoothingForecast): period_data = self.period_wise_data[key] for period in self.period_list: # check if posting date is within the period - if (entry.posting_date >= period.from_date and entry.posting_date <= period.to_date): - period_data[period.key] = period_data.get(period.key, 0.0) + flt(entry.get(self.based_on_field)) + if entry.posting_date >= period.from_date and entry.posting_date <= period.to_date: + period_data[period.key] = period_data.get(period.key, 0.0) + flt( + entry.get(self.based_on_field) + ) for key, value in self.period_wise_data.items(): list_of_period_value = [value.get(p.key, 0) for p in self.period_list] @@ -90,12 +98,12 @@ class ForecastingReport(ExponentialSmoothingForecast): def get_data_for_forecast(self): cond = "" if self.filters.item_code: - cond = " AND soi.item_code = %s" %(frappe.db.escape(self.filters.item_code)) + cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code)) warehouses = [] if self.filters.warehouse: warehouses = get_child_warehouses(self.filters.warehouse) - cond += " AND soi.warehouse in ({})".format(','.join(['%s'] * len(warehouses))) + cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses))) input_data = [self.filters.from_date, self.filters.company] if warehouses: @@ -103,7 +111,8 @@ class ForecastingReport(ExponentialSmoothingForecast): date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so.{date_field} as posting_date, soi.item_code, soi.warehouse, soi.item_name, soi.stock_qty as qty, soi.base_amount as amount @@ -112,23 +121,27 @@ class ForecastingReport(ExponentialSmoothingForecast): WHERE so.docstatus = 1 AND so.name = soi.parent AND so.{date_field} < %s AND so.company = %s {cond} - """.format(doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond), - tuple(input_data), as_dict=1) + """.format( + doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond + ), + tuple(input_data), + as_dict=1, + ) def prepare_final_data(self): self.data = [] - if not self.period_wise_data: return + if not self.period_wise_data: + return for key in self.period_wise_data: self.data.append(self.period_wise_data.get(key)) def add_total(self): - if not self.data: return + if not self.data: + return - total_row = { - "item_code": _(frappe.bold("Total Quantity")) - } + total_row = {"item_code": _(frappe.bold("Total Quantity"))} for value in self.data: for period in self.period_list: @@ -145,43 +158,52 @@ class ForecastingReport(ExponentialSmoothingForecast): self.data.append(total_row) def get_columns(self): - columns = [{ - "label": _("Item Code"), - "options": "Item", - "fieldname": "item_code", - "fieldtype": "Link", - "width": 130 - }, { - "label": _("Warehouse"), - "options": "Warehouse", - "fieldname": "warehouse", - "fieldtype": "Link", - "width": 130 - }] + columns = [ + { + "label": _("Item Code"), + "options": "Item", + "fieldname": "item_code", + "fieldtype": "Link", + "width": 130, + }, + { + "label": _("Warehouse"), + "options": "Warehouse", + "fieldname": "warehouse", + "fieldtype": "Link", + "width": 130, + }, + ] - width = 180 if self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"] else 100 + width = 180 if self.filters.periodicity in ["Yearly", "Half-Yearly", "Quarterly"] else 100 for period in self.period_list: - if (self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"] - or period.from_date >= getdate(self.filters.from_date)): + if self.filters.periodicity in [ + "Yearly", + "Half-Yearly", + "Quarterly", + ] or period.from_date >= getdate(self.filters.from_date): forecast_key = period.key label = _(period.label) if period.from_date >= getdate(self.filters.from_date): - forecast_key = 'forecast_' + period.key + forecast_key = "forecast_" + period.key label = _(period.label) + " " + _("(Forecast)") - columns.append({ - "label": label, - "fieldname": forecast_key, - "fieldtype": self.fieldtype, - "width": width, - "default": 0.0 - }) + columns.append( + { + "label": label, + "fieldname": forecast_key, + "fieldtype": self.fieldtype, + "width": width, + "default": 0.0, + } + ) return columns def get_chart_data(self): - if not self.data: return + if not self.data: + return labels = [] self.total_demand = [] @@ -206,40 +228,35 @@ class ForecastingReport(ExponentialSmoothingForecast): "data": { "labels": labels, "datasets": [ - { - "name": "Demand", - "values": self.total_demand - }, - { - "name": "Forecast", - "values": self.total_forecast - } - ] + {"name": "Demand", "values": self.total_demand}, + {"name": "Forecast", "values": self.total_forecast}, + ], }, - "type": "line" + "type": "line", } def get_summary_data(self): - if not self.data: return + if not self.data: + return return [ { "value": sum(self.total_demand), "label": _("Total Demand (Past Data)"), "currency": self.company_currency, - "datatype": self.fieldtype + "datatype": self.fieldtype, }, { "value": sum(self.total_history_forecast), "label": _("Total Forecast (Past Data)"), "currency": self.company_currency, - "datatype": self.fieldtype + "datatype": self.fieldtype, }, { "value": sum(self.total_future_forecast), "indicator": "Green", "label": _("Total Forecast (Future Data)"), "currency": self.company_currency, - "datatype": self.fieldtype - } + "datatype": self.fieldtype, + }, ] diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py index 4046bb12b86..a86c7a47c36 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -16,23 +16,34 @@ def execute(filters=None): chart_data = get_chart_data(data, filters) return columns, data, None, chart_data + def get_data(filters): query_filters = { "docstatus": ("<", 2), - "posting_date": ("between", [filters.from_date, filters.to_date]) + "posting_date": ("between", [filters.from_date, filters.to_date]), } - fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date", - "total_completed_qty", "workstation", "operation", "total_time_in_mins"] + fields = [ + "name", + "status", + "work_order", + "production_item", + "item_name", + "posting_date", + "total_completed_qty", + "workstation", + "operation", + "total_time_in_mins", + ] for field in ["work_order", "workstation", "operation", "company"]: if filters.get(field): query_filters[field] = ("in", filters.get(field)) - data = frappe.get_all("Job Card", - fields= fields, filters=query_filters) + data = frappe.get_all("Job Card", fields=fields, filters=query_filters) - if not data: return [] + if not data: + return [] job_cards = [d.name for d in data] @@ -42,9 +53,12 @@ def get_data(filters): } job_card_time_details = {} - for job_card_data in frappe.get_all("Job Card Time Log", + for job_card_data in frappe.get_all( + "Job Card Time Log", fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"], - filters=job_card_time_filter, group_by="parent"): + filters=job_card_time_filter, + group_by="parent", + ): job_card_time_details[job_card_data.parent] = job_card_data res = [] @@ -60,6 +74,7 @@ def get_data(filters): return res + def get_chart_data(job_card_details, filters): labels, periodic_data = prepare_chart_data(job_card_details, filters) @@ -73,23 +88,15 @@ def get_chart_data(job_card_details, filters): datasets.append({"name": "Open", "values": open_job_cards}) datasets.append({"name": "Completed", "values": completed}) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, - "type": "bar" - } + chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} return chart + def prepare_chart_data(job_card_details, filters): labels = [] - periodic_data = { - "Open": {}, - "Completed": {} - } + periodic_data = {"Open": {}, "Completed": {}} filters.range = "Monthly" @@ -110,6 +117,7 @@ def prepare_chart_data(job_card_details, filters): return labels, periodic_data + def get_columns(filters): columns = [ { @@ -117,84 +125,62 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Job Card", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, ] if not filters.get("status"): columns.append( - { - "label": _("Status"), - "fieldname": "status", - "width": 100 - }, + {"label": _("Status"), "fieldname": "status", "width": 100}, ) - columns.extend([ - { - "label": _("Work Order"), - "fieldname": "work_order", - "fieldtype": "Link", - "options": "Work Order", - "width": 100 - }, - { - "label": _("Production Item"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 110 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Workstation"), - "fieldname": "workstation", - "fieldtype": "Link", - "options": "Workstation", - "width": 110 - }, - { - "label": _("Operation"), - "fieldname": "operation", - "fieldtype": "Link", - "options": "Operation", - "width": 110 - }, - { - "label": _("Total Completed Qty"), - "fieldname": "total_completed_qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("From Time"), - "fieldname": "from_time", - "fieldtype": "Datetime", - "width": 120 - }, - { - "label": _("To Time"), - "fieldname": "to_time", - "fieldtype": "Datetime", - "width": 120 - }, - { - "label": _("Time Required (In Mins)"), - "fieldname": "total_time_in_mins", - "fieldtype": "Float", - "width": 100 - } - ]) + columns.extend( + [ + { + "label": _("Work Order"), + "fieldname": "work_order", + "fieldtype": "Link", + "options": "Work Order", + "width": 100, + }, + { + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 110, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + { + "label": _("Workstation"), + "fieldname": "workstation", + "fieldtype": "Link", + "options": "Workstation", + "width": 110, + }, + { + "label": _("Operation"), + "fieldname": "operation", + "fieldtype": "Link", + "options": "Operation", + "width": 110, + }, + { + "label": _("Total Completed Qty"), + "fieldname": "total_completed_qty", + "fieldtype": "Float", + "width": 120, + }, + {"label": _("From Time"), "fieldname": "from_time", "fieldtype": "Datetime", "width": 120}, + {"label": _("To Time"), "fieldname": "to_time", "fieldtype": "Datetime", "width": 120}, + { + "label": _("Time Required (In Mins)"), + "fieldname": "total_time_in_mins", + "fieldtype": "Float", + "width": 100, + }, + ] + ) return columns diff --git a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py index d3dfd52b773..b10e6434223 100644 --- a/erpnext/manufacturing/report/process_loss_report/process_loss_report.py +++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py @@ -12,87 +12,71 @@ Data = List[Row] Columns = List[Dict[str, str]] QueryArgs = Dict[str, str] + def execute(filters: Filters) -> Tuple[Columns, Data]: columns = get_columns() data = get_data(filters) return columns, data + def get_data(filters: Filters) -> Data: query_args = get_query_args(filters) data = run_query(query_args) update_data_with_total_pl_value(data) return data + def get_columns() -> Columns: return [ { - 'label': _('Work Order'), - 'fieldname': 'name', - 'fieldtype': 'Link', - 'options': 'Work Order', - 'width': '200' + "label": _("Work Order"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Work Order", + "width": "200", }, { - 'label': _('Item'), - 'fieldname': 'production_item', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': '100' + "label": _("Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": "100", }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": "100"}, { - 'label': _('Status'), - 'fieldname': 'status', - 'fieldtype': 'Data', - 'width': '100' + "label": _("Manufactured Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": "150", }, + {"label": _("Loss Qty"), "fieldname": "process_loss_qty", "fieldtype": "Float", "width": "150"}, { - 'label': _('Manufactured Qty'), - 'fieldname': 'produced_qty', - 'fieldtype': 'Float', - 'width': '150' + "label": _("Actual Manufactured Qty"), + "fieldname": "actual_produced_qty", + "fieldtype": "Float", + "width": "150", }, + {"label": _("Loss Value"), "fieldname": "total_pl_value", "fieldtype": "Float", "width": "150"}, + {"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"}, { - 'label': _('Loss Qty'), - 'fieldname': 'process_loss_qty', - 'fieldtype': 'Float', - 'width': '150' + "label": _("Raw Material Value"), + "fieldname": "total_rm_value", + "fieldtype": "Float", + "width": "150", }, - { - 'label': _('Actual Manufactured Qty'), - 'fieldname': 'actual_produced_qty', - 'fieldtype': 'Float', - 'width': '150' - }, - { - 'label': _('Loss Value'), - 'fieldname': 'total_pl_value', - 'fieldtype': 'Float', - 'width': '150' - }, - { - 'label': _('FG Value'), - 'fieldname': 'total_fg_value', - 'fieldtype': 'Float', - 'width': '150' - }, - { - 'label': _('Raw Material Value'), - 'fieldname': 'total_rm_value', - 'fieldtype': 'Float', - 'width': '150' - } ] + def get_query_args(filters: Filters) -> QueryArgs: query_args = {} query_args.update(filters) - query_args.update( - get_filter_conditions(filters) - ) + query_args.update(get_filter_conditions(filters)) return query_args + def run_query(query_args: QueryArgs) -> Data: - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT wo.name, wo.status, wo.production_item, wo.qty, wo.produced_qty, wo.process_loss_qty, @@ -111,24 +95,26 @@ def run_query(query_args: QueryArgs) -> Data: {work_order_filter} GROUP BY se.work_order - """.format(**query_args), query_args, as_dict=1) + """.format( + **query_args + ), + query_args, + as_dict=1, + ) + def update_data_with_total_pl_value(data: Data) -> None: for row in data: - value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty'] - row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg + value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"] + row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg + def get_filter_conditions(filters: Filters) -> QueryArgs: filter_conditions = dict(item_filter="", work_order_filter="") if "item" in filters: production_item = filters.get("item") - filter_conditions.update( - {"item_filter": f"AND wo.production_item='{production_item}'"} - ) + filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"}) if "work_order" in filters: work_order_name = filters.get("work_order") - filter_conditions.update( - {"work_order_filter": f"AND wo.name='{work_order_name}'"} - ) + filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"}) return filter_conditions - diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index d4743d3a8ef..12b5d19ba87 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -12,16 +12,11 @@ from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get def execute(filters=None): columns = get_columns(filters) data, chart = get_data(filters, columns) - return columns, data, None , chart + return columns, data, None, chart + def get_columns(filters): - columns =[ - { - "label": _("Status"), - "fieldname": "Status", - "fieldtype": "Data", - "width": 140 - }] + columns = [{"label": _("Status"), "fieldname": "Status", "fieldtype": "Data", "width": 140}] ranges = get_period_date_ranges(filters) @@ -29,22 +24,20 @@ def get_columns(filters): period = get_period(end_date, filters) - columns.append({ - "label": _(period), - "fieldname": scrub(period), - "fieldtype": "Float", - "width": 120 - }) + columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) return columns + def get_periodic_data(filters, entry): periodic_data = { "All Work Orders": {}, "Not Started": {}, "Overdue": {}, "Pending": {}, - "Completed": {} + "Completed": {}, } ranges = get_period_date_ranges(filters) @@ -52,34 +45,37 @@ def get_periodic_data(filters, entry): for from_date, end_date in ranges: period = get_period(end_date, filters) for d in entry: - if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date) : + if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date): periodic_data = update_periodic_data(periodic_data, "All Work Orders", period) - if d.status == 'Completed': - if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate(from_date): + if d.status == "Completed": + if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate( + from_date + ): periodic_data = update_periodic_data(periodic_data, "Completed", period) - elif getdate(d.actual_start_date) < getdate(from_date) : + elif getdate(d.actual_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Pending", period) - elif getdate(d.planned_start_date) < getdate(from_date) : + elif getdate(d.planned_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) - elif d.status == 'In Process': - if getdate(d.actual_start_date) < getdate(from_date) : + elif d.status == "In Process": + if getdate(d.actual_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Pending", period) - elif getdate(d.planned_start_date) < getdate(from_date) : + elif getdate(d.planned_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) - elif d.status == 'Not Started': - if getdate(d.planned_start_date) < getdate(from_date) : + elif d.status == "Not Started": + if getdate(d.planned_start_date) < getdate(from_date): periodic_data = update_periodic_data(periodic_data, "Overdue", period) else: periodic_data = update_periodic_data(periodic_data, "Not Started", period) return periodic_data + def update_periodic_data(periodic_data, status, period): if periodic_data.get(status).get(period): periodic_data[status][period] += 1 @@ -88,22 +84,33 @@ def update_periodic_data(periodic_data, status, period): return periodic_data + def get_data(filters, columns): data = [] - entry = frappe.get_all("Work Order", - fields=["creation", "modified", "actual_start_date", "actual_end_date", "planned_start_date", "planned_end_date", "status"], - filters={"docstatus": 1, "company": filters["company"] }) + entry = frappe.get_all( + "Work Order", + fields=[ + "creation", + "modified", + "actual_start_date", + "actual_end_date", + "planned_start_date", + "planned_end_date", + "status", + ], + filters={"docstatus": 1, "company": filters["company"]}, + ) - periodic_data = get_periodic_data(filters,entry) + periodic_data = get_periodic_data(filters, entry) labels = ["All Work Orders", "Not Started", "Overdue", "Pending", "Completed"] - chart_data = get_chart_data(periodic_data,columns) + chart_data = get_chart_data(periodic_data, columns) ranges = get_period_date_ranges(filters) for label in labels: work = {} work["Status"] = label - for dummy,end_date in ranges: + for dummy, end_date in ranges: period = get_period(end_date, filters) if periodic_data.get(label).get(period): work[scrub(period)] = periodic_data.get(label).get(period) @@ -113,10 +120,11 @@ def get_data(filters, columns): return data, chart_data + def get_chart_data(periodic_data, columns): labels = [d.get("label") for d in columns[1:]] - all_data, not_start, overdue, pending, completed = [], [], [] , [], [] + all_data, not_start, overdue, pending, completed = [], [], [], [], [] datasets = [] for d in labels: @@ -126,18 +134,13 @@ def get_chart_data(periodic_data, columns): pending.append(periodic_data.get("Pending").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({'name':'All Work Orders', 'values': all_data}) - datasets.append({'name':'Not Started', 'values': not_start}) - datasets.append({'name':'Overdue', 'values': overdue}) - datasets.append({'name':'Pending', 'values': pending}) - datasets.append({'name':'Completed', 'values': completed}) + datasets.append({"name": "All Work Orders", "values": all_data}) + datasets.append({"name": "Not Started", "values": not_start}) + datasets.append({"name": "Overdue", "values": overdue}) + datasets.append({"name": "Pending", "values": pending}) + datasets.append({"name": "Completed", "values": completed}) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } - } + chart = {"data": {"labels": labels, "datasets": datasets}} chart["type"] = "line" return chart diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index aaa231466fd..17f7f5e51fa 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -13,6 +13,7 @@ def execute(filters=None): return columns, data + def get_data(filters): data = [] @@ -23,6 +24,7 @@ def get_data(filters): return data + def get_production_plan_item_details(filters, data, order_details): itemwise_indent = {} @@ -30,77 +32,85 @@ def get_production_plan_item_details(filters, data, order_details): for row in production_plan_doc.po_items: work_order = frappe.get_value( "Work Order", - { - "production_plan_item": row.name, - "bom_no": row.bom_no, - "production_item": row.item_code - }, - "name" + {"production_plan_item": row.name, "bom_no": row.bom_no, "production_item": row.item_code}, + "name", ) if row.item_code not in itemwise_indent: itemwise_indent.setdefault(row.item_code, {}) - data.append({ - "indent": 0, - "item_code": row.item_code, - "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), - "qty": row.planned_qty, - "document_type": "Work Order", - "document_name": work_order or "", - "bom_level": 0, - "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), - "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) - }) + data.append( + { + "indent": 0, + "item_code": row.item_code, + "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "qty": row.planned_qty, + "document_type": "Work Order", + "document_name": work_order or "", + "bom_level": 0, + "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), + "pending_qty": flt(row.planned_qty) + - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)), + } + ) - get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details) + get_production_plan_sub_assembly_item_details( + filters, row, production_plan_doc, data, order_details + ) -def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details): + +def get_production_plan_sub_assembly_item_details( + filters, row, production_plan_doc, data, order_details +): for item in production_plan_doc.sub_assembly_items: if row.name == item.production_plan_item: - subcontracted_item = (item.type_of_manufacturing == 'Subcontract') + subcontracted_item = item.type_of_manufacturing == "Subcontract" if subcontracted_item: docname = frappe.get_value( "Purchase Order Item", - { - "production_plan_sub_assembly_item": item.name, - "docstatus": ("<", 2) - }, - "parent" + {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, + "parent", ) else: docname = frappe.get_value( - "Work Order", - { - "production_plan_sub_assembly_item": item.name, - "docstatus": ("<", 2) - }, - "name" + "Work Order", {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name" ) - data.append({ - "indent": 1, - "item_code": item.production_item, - "item_name": item.item_name, - "qty": item.qty, - "document_type": "Work Order" if not subcontracted_item else "Purchase Order", - "document_name": docname or "", - "bom_level": item.bom_level, - "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0), - "pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)) - }) + data.append( + { + "indent": 1, + "item_code": item.production_item, + "item_name": item.item_name, + "qty": item.qty, + "document_type": "Work Order" if not subcontracted_item else "Purchase Order", + "document_name": docname or "", + "bom_level": item.bom_level, + "produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0), + "pending_qty": flt(item.qty) + - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)), + } + ) + def get_work_order_details(filters, order_details): - for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")}, - fields=["name", "produced_qty", "production_plan", "production_item"]): + for row in frappe.get_all( + "Work Order", + filters={"production_plan": filters.get("production_plan")}, + fields=["name", "produced_qty", "production_plan", "production_item"], + ): order_details.setdefault((row.name, row.production_item), row) + def get_purchase_order_details(filters, order_details): - for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")}, - fields=["parent", "received_qty as produced_qty", "item_code"]): + for row in frappe.get_all( + "Purchase Order Item", + filters={"production_plan": filters.get("production_plan")}, + fields=["parent", "received_qty as produced_qty", "item_code"], + ): order_details.setdefault((row.parent, row.item_code), row) + def get_column(filters): return [ { @@ -108,49 +118,24 @@ def get_column(filters): "fieldtype": "Link", "fieldname": "item_code", "width": 300, - "options": "Item" - }, - { - "label": "Item Name", - "fieldtype": "data", - "fieldname": "item_name", - "width": 100 + "options": "Item", }, + {"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100}, { "label": "Document Type", "fieldtype": "Link", "fieldname": "document_type", "width": 150, - "options": "DocType" + "options": "DocType", }, { "label": "Document Name", "fieldtype": "Dynamic Link", "fieldname": "document_name", - "width": 150 + "width": 150, }, - { - "label": "BOM Level", - "fieldtype": "Int", - "fieldname": "bom_level", - "width": 100 - }, - { - "label": "Order Qty", - "fieldtype": "Float", - "fieldname": "qty", - "width": 120 - }, - { - "label": "Received Qty", - "fieldtype": "Float", - "fieldname": "produced_qty", - "width": 160 - }, - { - "label": "Pending Qty", - "fieldtype": "Float", - "fieldname": "pending_qty", - "width": 110 - } + {"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100}, + {"label": "Order Qty", "fieldtype": "Float", "fieldname": "qty", "width": 120}, + {"label": "Received Qty", "fieldtype": "Float", "fieldname": "produced_qty", "width": 160}, + {"label": "Pending Qty", "fieldtype": "Float", "fieldname": "pending_qty", "width": 110}, ] diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index e1e7225e057..140488820a5 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -15,38 +15,36 @@ mapper = { stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse, `tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """, "filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty - and `tabSales Order`.per_delivered < 100.0""" + and `tabSales Order`.per_delivered < 100.0""", }, "Material Request": { "fields": """ item_code as production_item, item_name as production_item_name, stock_uom, stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse, `tabMaterial Request Item`.schedule_date """, "filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100 - and `tabMaterial Request`.material_request_type = 'Manufacture' """ + and `tabMaterial Request`.material_request_type = 'Manufacture' """, }, "Work Order": { "fields": """ production_item, item_name as production_item_name, planned_start_date, stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """, - "filters": "docstatus = 1 and status not in ('Completed', 'Stopped')" + "filters": "docstatus = 1 and status not in ('Completed', 'Stopped')", }, } order_mapper = { "Sales Order": { "Delivery Date": "`tabSales Order Item`.delivery_date asc", - "Total Amount": "`tabSales Order`.base_grand_total desc" + "Total Amount": "`tabSales Order`.base_grand_total desc", }, - "Material Request": { - "Required Date": "`tabMaterial Request Item`.schedule_date asc" - }, - "Work Order": { - "Planned Start Date": "planned_start_date asc" - } + "Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"}, + "Work Order": {"Planned Start Date": "planned_start_date asc"}, } + def execute(filters=None): return ProductionPlanReport(filters).execute_report() + class ProductionPlanReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -65,46 +63,64 @@ class ProductionPlanReport(object): return self.columns, self.data def get_open_orders(self): - doctype = ("`tabWork Order`" if self.filters.based_on == "Work Order" - else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)) + doctype = ( + "`tabWork Order`" + if self.filters.based_on == "Work Order" + else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on) + ) filters = mapper.get(self.filters.based_on)["filters"] filters = self.prepare_other_conditions(filters, self.filters.based_on) order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by]) - self.orders = frappe.db.sql(""" SELECT {fields} from {doctype} + self.orders = frappe.db.sql( + """ SELECT {fields} from {doctype} WHERE {filters} {order_by}""".format( - doctype = doctype, - filters = filters, - order_by = order_by, - fields = mapper.get(self.filters.based_on)["fields"] - ), tuple(self.filters.docnames), as_dict=1) + doctype=doctype, + filters=filters, + order_by=order_by, + fields=mapper.get(self.filters.based_on)["fields"], + ), + tuple(self.filters.docnames), + as_dict=1, + ) def prepare_other_conditions(self, filters, doctype): if self.filters.docnames: field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype) - filters += " and %s in (%s)" % (field, ','.join(['%s'] * len(self.filters.docnames))) + filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames))) if doctype != "Work Order": filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype) if self.filters.company: - filters += " and `tab%s`.company = %s" %(doctype, frappe.db.escape(self.filters.company)) + filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company)) return filters def get_raw_materials(self): - if not self.orders: return + if not self.orders: + return self.warehouses = [d.warehouse for d in self.orders] self.item_codes = [d.production_item for d in self.orders] if self.filters.based_on == "Work Order": work_orders = [d.name for d in self.orders] - raw_materials = frappe.get_all("Work Order Item", - fields=["parent", "item_code", "item_name as raw_material_name", - "source_warehouse as warehouse", "required_qty"], - filters = {"docstatus": 1, "parent": ("in", work_orders), "source_warehouse": ("!=", "")}) or [] + raw_materials = ( + frappe.get_all( + "Work Order Item", + fields=[ + "parent", + "item_code", + "item_name as raw_material_name", + "source_warehouse as warehouse", + "required_qty", + ], + filters={"docstatus": 1, "parent": ("in", work_orders), "source_warehouse": ("!=", "")}, + ) + or [] + ) self.warehouses.extend([d.source_warehouse for d in raw_materials]) else: @@ -118,21 +134,32 @@ class ProductionPlanReport(object): bom_nos.append(bom_no) - bom_doctype = ("BOM Explosion Item" - if self.filters.include_subassembly_raw_materials else "BOM Item") + bom_doctype = ( + "BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item" + ) - qty_field = ("qty_consumed_per_unit" - if self.filters.include_subassembly_raw_materials else "(bom_item.qty / bom.quantity)") + qty_field = ( + "qty_consumed_per_unit" + if self.filters.include_subassembly_raw_materials + else "(bom_item.qty / bom.quantity)" + ) - raw_materials = frappe.db.sql(""" SELECT bom_item.parent, bom_item.item_code, + raw_materials = frappe.db.sql( + """ SELECT bom_item.parent, bom_item.item_code, bom_item.item_name as raw_material_name, {0} as required_qty_per_unit FROM `tabBOM` as bom, `tab{1}` as bom_item WHERE bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1 - """.format(qty_field, bom_doctype, ','.join(["%s"] * len(bom_nos))), tuple(bom_nos), as_dict=1) + """.format( + qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos)) + ), + tuple(bom_nos), + as_dict=1, + ) - if not raw_materials: return + if not raw_materials: + return self.item_codes.extend([d.item_code for d in raw_materials]) @@ -144,15 +171,20 @@ class ProductionPlanReport(object): rows.append(d) def get_item_details(self): - if not (self.orders and self.item_codes): return + if not (self.orders and self.item_codes): + return self.item_details = {} - for d in frappe.get_all("Item Default", fields = ["parent", "default_warehouse"], - filters = {"company": self.filters.company, "parent": ("in", self.item_codes)}): + for d in frappe.get_all( + "Item Default", + fields=["parent", "default_warehouse"], + filters={"company": self.filters.company, "parent": ("in", self.item_codes)}, + ): self.item_details[d.parent] = d def get_bin_details(self): - if not (self.orders and self.raw_materials_dict): return + if not (self.orders and self.raw_materials_dict): + return self.bin_details = {} self.mrp_warehouses = [] @@ -160,48 +192,55 @@ class ProductionPlanReport(object): self.mrp_warehouses.extend(get_child_warehouses(self.filters.raw_material_warehouse)) self.warehouses.extend(self.mrp_warehouses) - for d in frappe.get_all("Bin", + for d in frappe.get_all( + "Bin", fields=["warehouse", "item_code", "actual_qty", "ordered_qty", "projected_qty"], - filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}): + filters={"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}, + ): key = (d.item_code, d.warehouse) if key not in self.bin_details: self.bin_details.setdefault(key, d) def get_purchase_details(self): - if not (self.orders and self.raw_materials_dict): return + if not (self.orders and self.raw_materials_dict): + return self.purchase_details = {} - purchased_items = frappe.get_all("Purchase Order Item", + purchased_items = frappe.get_all( + "Purchase Order Item", fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"], filters={ "item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses), "docstatus": 1, }, - group_by = "item_code, warehouse") + group_by="item_code, warehouse", + ) for d in purchased_items: key = (d.item_code, d.warehouse) if key not in self.purchase_details: self.purchase_details.setdefault(key, d) def prepare_data(self): - if not self.orders: return + if not self.orders: + return for d in self.orders: key = d.name if self.filters.based_on == "Work Order" else d.bom_no - if not self.raw_materials_dict.get(key): continue + if not self.raw_materials_dict.get(key): + continue bin_data = self.bin_details.get((d.production_item, d.warehouse)) or {} - d.update({ - "for_warehouse": d.warehouse, - "available_qty": 0 - }) + d.update({"for_warehouse": d.warehouse, "available_qty": 0}) if bin_data and bin_data.get("actual_qty") > 0 and d.qty_to_manufacture: - d.available_qty = (bin_data.get("actual_qty") - if (d.qty_to_manufacture > bin_data.get("actual_qty")) else d.qty_to_manufacture) + d.available_qty = ( + bin_data.get("actual_qty") + if (d.qty_to_manufacture > bin_data.get("actual_qty")) + else d.qty_to_manufacture + ) bin_data["actual_qty"] -= d.available_qty @@ -232,8 +271,9 @@ class ProductionPlanReport(object): d.remaining_qty = d.required_qty self.pick_materials_from_warehouses(d, data, warehouses) - if (d.remaining_qty and self.filters.raw_material_warehouse - and d.remaining_qty != d.required_qty): + if ( + d.remaining_qty and self.filters.raw_material_warehouse and d.remaining_qty != d.required_qty + ): row = self.get_args() d.warehouse = self.filters.raw_material_warehouse d.required_qty = d.remaining_qty @@ -243,7 +283,8 @@ class ProductionPlanReport(object): def pick_materials_from_warehouses(self, args, order_data, warehouses): for index, warehouse in enumerate(warehouses): - if not args.remaining_qty: return + if not args.remaining_qty: + return row = self.get_args() @@ -255,14 +296,18 @@ class ProductionPlanReport(object): args.allotted_qty = 0 if bin_data and bin_data.get("actual_qty") > 0: - args.allotted_qty = (bin_data.get("actual_qty") - if (args.required_qty > bin_data.get("actual_qty")) else args.required_qty) + args.allotted_qty = ( + bin_data.get("actual_qty") + if (args.required_qty > bin_data.get("actual_qty")) + else args.required_qty + ) args.remaining_qty -= args.allotted_qty bin_data["actual_qty"] -= args.allotted_qty - if ((self.mrp_warehouses and (args.allotted_qty or index == len(warehouses) - 1)) - or not self.mrp_warehouses): + if ( + self.mrp_warehouses and (args.allotted_qty or index == len(warehouses) - 1) + ) or not self.mrp_warehouses: if not self.index: row.update(order_data) self.index += 1 @@ -275,52 +320,45 @@ class ProductionPlanReport(object): self.data.append(row) def get_args(self): - return frappe._dict({ - "work_order": "", - "sales_order": "", - "production_item": "", - "production_item_name": "", - "qty_to_manufacture": "", - "produced_qty": "" - }) + return frappe._dict( + { + "work_order": "", + "sales_order": "", + "production_item": "", + "production_item_name": "", + "qty_to_manufacture": "", + "produced_qty": "", + } + ) def get_columns(self): based_on = self.filters.based_on - self.columns = [{ - "label": _("ID"), - "options": based_on, - "fieldname": "name", - "fieldtype": "Link", - "width": 100 - }, { - "label": _("Item Code"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, { - "label": _("Item Name"), - "fieldname": "production_item_name", - "fieldtype": "Data", - "width": 130 - }, { - "label": _("Warehouse"), - "options": "Warehouse", - "fieldname": "for_warehouse", - "fieldtype": "Link", - "width": 100 - }, { - "label": _("Order Qty"), - "fieldname": "qty_to_manufacture", - "fieldtype": "Float", - "width": 80 - }, { - "label": _("Available"), - "fieldname": "available_qty", - "fieldtype": "Float", - "width": 80 - }] + self.columns = [ + {"label": _("ID"), "options": based_on, "fieldname": "name", "fieldtype": "Link", "width": 100}, + { + "label": _("Item Code"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + { + "label": _("Item Name"), + "fieldname": "production_item_name", + "fieldtype": "Data", + "width": 130, + }, + { + "label": _("Warehouse"), + "options": "Warehouse", + "fieldname": "for_warehouse", + "fieldtype": "Link", + "width": 100, + }, + {"label": _("Order Qty"), "fieldname": "qty_to_manufacture", "fieldtype": "Float", "width": 80}, + {"label": _("Available"), "fieldname": "available_qty", "fieldtype": "Float", "width": 80}, + ] fieldname, fieldtype = "delivery_date", "Date" if self.filters.based_on == "Sales Order" and self.filters.order_by == "Total Amount": @@ -330,48 +368,50 @@ class ProductionPlanReport(object): elif self.filters.based_on == "Work Order": fieldname = "planned_start_date" - self.columns.append({ - "label": _(self.filters.order_by), - "fieldname": fieldname, - "fieldtype": fieldtype, - "width": 100 - }) + self.columns.append( + { + "label": _(self.filters.order_by), + "fieldname": fieldname, + "fieldtype": fieldtype, + "width": 100, + } + ) - self.columns.extend([{ - "label": _("Raw Material Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 120 - }, { - "label": _("Raw Material Name"), - "fieldname": "raw_material_name", - "fieldtype": "Data", - "width": 130 - }, { - "label": _("Warehouse"), - "options": "Warehouse", - "fieldname": "warehouse", - "fieldtype": "Link", - "width": 110 - }, { - "label": _("Required Qty"), - "fieldname": "required_qty", - "fieldtype": "Float", - "width": 100 - }, { - "label": _("Allotted Qty"), - "fieldname": "allotted_qty", - "fieldtype": "Float", - "width": 100 - }, { - "label": _("Expected Arrival Date"), - "fieldname": "arrival_date", - "fieldtype": "Date", - "width": 160 - }, { - "label": _("Arrival Quantity"), - "fieldname": "arrival_qty", - "fieldtype": "Float", - "width": 140 - }]) + self.columns.extend( + [ + { + "label": _("Raw Material Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, + }, + { + "label": _("Raw Material Name"), + "fieldname": "raw_material_name", + "fieldtype": "Data", + "width": 130, + }, + { + "label": _("Warehouse"), + "options": "Warehouse", + "fieldname": "warehouse", + "fieldtype": "Link", + "width": 110, + }, + {"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Allotted Qty"), "fieldname": "allotted_qty", "fieldtype": "Float", "width": 100}, + { + "label": _("Expected Arrival Date"), + "fieldname": "arrival_date", + "fieldtype": "Date", + "width": 160, + }, + { + "label": _("Arrival Quantity"), + "fieldname": "arrival_qty", + "fieldtype": "Float", + "width": 140, + }, + ] + ) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index a0c4a43e90f..0a79130f1b2 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -11,13 +11,24 @@ def execute(filters=None): data = get_data(filters) columns = get_columns(filters) chart_data = get_chart_data(data, filters) - return columns, data , None, chart_data + return columns, data, None, chart_data + def get_data(filters): query_filters = {"docstatus": ("<", 2)} - fields = ["name", "status", "report_date", "item_code", "item_name", "sample_size", - "inspection_type", "reference_type", "reference_name", "inspected_by"] + fields = [ + "name", + "status", + "report_date", + "item_code", + "item_name", + "sample_size", + "inspection_type", + "reference_type", + "reference_name", + "inspected_by", + ] for field in ["status", "item_code", "status", "inspected_by"]: if filters.get(field): @@ -26,36 +37,33 @@ def get_data(filters): query_filters["report_date"] = (">=", filters.get("from_date")) query_filters["report_date"] = ("<=", filters.get("to_date")) - return frappe.get_all("Quality Inspection", - fields= fields, filters=query_filters, order_by="report_date asc") + return frappe.get_all( + "Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc" + ) + def get_chart_data(periodic_data, columns): labels = ["Rejected", "Accepted"] - status_wise_data = { - "Accepted": 0, - "Rejected": 0 - } + status_wise_data = {"Accepted": 0, "Rejected": 0} datasets = [] for d in periodic_data: status_wise_data[d.status] += 1 - datasets.append({'name':'Qty Wise Chart', - 'values': [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")]}) + datasets.append( + { + "name": "Qty Wise Chart", + "values": [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")], + } + ) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, - "type": "donut", - "height": 300 - } + chart = {"data": {"labels": labels, "datasets": datasets}, "type": "donut", "height": 300} return chart + def get_columns(filters): columns = [ { @@ -63,71 +71,49 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Work Order", - "width": 100 + "width": 100, }, - { - "label": _("Report Date"), - "fieldname": "report_date", - "fieldtype": "Date", - "width": 150 - } + {"label": _("Report Date"), "fieldname": "report_date", "fieldtype": "Date", "width": 150}, ] if not filters.get("status"): columns.append( - { - "label": _("Status"), - "fieldname": "status", - "width": 100 - }, + {"label": _("Status"), "fieldname": "status", "width": 100}, ) - columns.extend([ - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 130 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 130 - }, - { - "label": _("Sample Size"), - "fieldname": "sample_size", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Inspection Type"), - "fieldname": "inspection_type", - "fieldtype": "Data", - "width": 110 - }, - { - "label": _("Document Type"), - "fieldname": "reference_type", - "fieldtype": "Data", - "width": 90 - }, - { - "label": _("Document Name"), - "fieldname": "reference_name", - "fieldtype": "Dynamic Link", - "options": "reference_type", - "width": 150 - }, - { - "label": _("Inspected By"), - "fieldname": "inspected_by", - "fieldtype": "Link", - "options": "User", - "width": 150 - } - ]) + columns.extend( + [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 130, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 130}, + {"label": _("Sample Size"), "fieldname": "sample_size", "fieldtype": "Float", "width": 110}, + { + "label": _("Inspection Type"), + "fieldname": "inspection_type", + "fieldtype": "Data", + "width": 110, + }, + {"label": _("Document Type"), "fieldname": "reference_type", "fieldtype": "Data", "width": 90}, + { + "label": _("Document Name"), + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "options": "reference_type", + "width": 150, + }, + { + "label": _("Inspected By"), + "fieldname": "inspected_by", + "fieldtype": "Link", + "options": "User", + "width": 150, + }, + ] + ) return columns diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py index db0b239ae20..c6b7e58d656 100644 --- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py +++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py @@ -12,17 +12,20 @@ def execute(filters=None): columns = get_columns() return columns, data + def get_item_list(wo_list, filters): out = [] - #Add a row for each item/qty + # Add a row for each item/qty for wo_details in wo_list: desc = frappe.db.get_value("BOM", wo_details.bom_no, "description") - for wo_item_details in frappe.db.get_values("Work Order Item", - {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1): + for wo_item_details in frappe.db.get_values( + "Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1 + ): - item_list = frappe.db.sql("""SELECT + item_list = frappe.db.sql( + """SELECT bom_item.item_code as item_code, ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty FROM @@ -36,8 +39,14 @@ def get_item_list(wo_list, filters): and bom.name = %(bom)s GROUP BY bom_item.item_code""", - {"bom": wo_details.bom_no, "warehouse": wo_item_details.source_warehouse, - "filterhouse": filters.warehouse, "item_code": wo_item_details.item_code}, as_dict=1) + { + "bom": wo_details.bom_no, + "warehouse": wo_item_details.source_warehouse, + "filterhouse": filters.warehouse, + "item_code": wo_item_details.item_code, + }, + as_dict=1, + ) stock_qty = 0 count = 0 @@ -54,97 +63,99 @@ def get_item_list(wo_list, filters): else: build = "N" - row = frappe._dict({ - "work_order": wo_details.name, - "status": wo_details.status, - "req_items": cint(count), - "instock": stock_qty, - "description": desc, - "source_warehouse": wo_item_details.source_warehouse, - "item_code": wo_item_details.item_code, - "bom_no": wo_details.bom_no, - "qty": wo_details.qty, - "buildable_qty": buildable_qty, - "ready_to_build": build - }) + row = frappe._dict( + { + "work_order": wo_details.name, + "status": wo_details.status, + "req_items": cint(count), + "instock": stock_qty, + "description": desc, + "source_warehouse": wo_item_details.source_warehouse, + "item_code": wo_item_details.item_code, + "bom_no": wo_details.bom_no, + "qty": wo_details.qty, + "buildable_qty": buildable_qty, + "ready_to_build": build, + } + ) out.append(row) return out + def get_work_orders(): - out = frappe.get_all("Work Order", filters={"docstatus": 1, "status": ( "!=","Completed")}, - fields=["name","status", "bom_no", "qty", "produced_qty"], order_by='name') + out = frappe.get_all( + "Work Order", + filters={"docstatus": 1, "status": ("!=", "Completed")}, + fields=["name", "status", "bom_no", "qty", "produced_qty"], + order_by="name", + ) return out + def get_columns(): - columns = [{ - "fieldname": "work_order", - "label": "Work Order", - "fieldtype": "Link", - "options": "Work Order", - "width": 110 - }, { - "fieldname": "bom_no", - "label": "BOM", - "fieldtype": "Link", - "options": "BOM", - "width": 120 - }, { - "fieldname": "description", - "label": "Description", - "fieldtype": "Data", - "options": "", - "width": 230 - }, { - "fieldname": "item_code", - "label": "Item Code", - "fieldtype": "Link", - "options": "Item", - "width": 110 - },{ - "fieldname": "source_warehouse", - "label": "Source Warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 110 - },{ - "fieldname": "qty", - "label": "Qty to Build", - "fieldtype": "Data", - "options": "", - "width": 110 - }, { - "fieldname": "status", - "label": "Status", - "fieldtype": "Data", - "options": "", - "width": 100 - }, { - "fieldname": "req_items", - "label": "# Req'd Items", - "fieldtype": "Data", - "options": "", - "width": 105 - }, { - "fieldname": "instock", - "label": "# In Stock", - "fieldtype": "Data", - "options": "", - "width": 105 - }, { - "fieldname": "buildable_qty", - "label": "Buildable Qty", - "fieldtype": "Data", - "options": "", - "width": 100 - }, { - "fieldname": "ready_to_build", - "label": "Build All?", - "fieldtype": "Data", - "options": "", - "width": 90 - }] + columns = [ + { + "fieldname": "work_order", + "label": "Work Order", + "fieldtype": "Link", + "options": "Work Order", + "width": 110, + }, + {"fieldname": "bom_no", "label": "BOM", "fieldtype": "Link", "options": "BOM", "width": 120}, + { + "fieldname": "description", + "label": "Description", + "fieldtype": "Data", + "options": "", + "width": 230, + }, + { + "fieldname": "item_code", + "label": "Item Code", + "fieldtype": "Link", + "options": "Item", + "width": 110, + }, + { + "fieldname": "source_warehouse", + "label": "Source Warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 110, + }, + {"fieldname": "qty", "label": "Qty to Build", "fieldtype": "Data", "options": "", "width": 110}, + {"fieldname": "status", "label": "Status", "fieldtype": "Data", "options": "", "width": 100}, + { + "fieldname": "req_items", + "label": "# Req'd Items", + "fieldtype": "Data", + "options": "", + "width": 105, + }, + { + "fieldname": "instock", + "label": "# In Stock", + "fieldtype": "Data", + "options": "", + "width": 105, + }, + { + "fieldname": "buildable_qty", + "label": "Buildable Qty", + "fieldtype": "Data", + "options": "", + "width": 100, + }, + { + "fieldname": "ready_to_build", + "label": "Build All?", + "fieldtype": "Data", + "options": "", + "width": 90, + }, + ] return columns diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index d7469ddfdd6..2368bfdf2c6 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -21,11 +21,23 @@ def execute(filters=None): chart_data = get_chart_data(data, filters) return columns, data, None, chart_data + def get_data(filters): query_filters = {"docstatus": ("<", 2)} - fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty", - "planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"] + fields = [ + "name", + "status", + "sales_order", + "production_item", + "qty", + "produced_qty", + "planned_start_date", + "planned_end_date", + "actual_start_date", + "actual_end_date", + "lead_time", + ] for field in ["sales_order", "production_item", "status", "company"]: if filters.get(field): @@ -34,15 +46,16 @@ def get_data(filters): query_filters["planned_start_date"] = (">=", filters.get("from_date")) query_filters["planned_end_date"] = ("<=", filters.get("to_date")) - data = frappe.get_all("Work Order", - fields= fields, filters=query_filters, order_by="planned_start_date asc") + data = frappe.get_all( + "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" + ) res = [] for d in data: start_date = d.actual_start_date or d.planned_start_date d.age = 0 - if d.status != 'Completed': + if d.status != "Completed": d.age = date_diff(today(), start_date) if filters.get("age") <= d.age: @@ -50,6 +63,7 @@ def get_data(filters): return res + def get_chart_data(data, filters): if filters.get("charts_based_on") == "Status": return get_chart_based_on_status(data) @@ -58,6 +72,7 @@ def get_chart_data(data, filters): else: return get_chart_based_on_qty(data, filters) + def get_chart_based_on_status(data): labels = frappe.get_meta("Work Order").get_options("status").split("\n") if "" in labels: @@ -71,25 +86,18 @@ def get_chart_based_on_status(data): values = [status_wise_data[label] for label in labels] chart = { - "data": { - 'labels': labels, - 'datasets': [{'name':'Qty Wise Chart', 'values': values}] - }, + "data": {"labels": labels, "datasets": [{"name": "Qty Wise Chart", "values": values}]}, "type": "donut", - "height": 300 + "height": 300, } return chart + def get_chart_based_on_age(data): labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"] - age_wise_data = { - "0-30 Days": 0, - "30-60 Days": 0, - "60-90 Days": 0, - "90 Above": 0 - } + age_wise_data = {"0-30 Days": 0, "30-60 Days": 0, "60-90 Days": 0, "90 Above": 0} for d in data: if d.age > 0 and d.age <= 30: @@ -101,20 +109,22 @@ def get_chart_based_on_age(data): else: age_wise_data["90 Above"] += 1 - values = [age_wise_data["0-30 Days"], age_wise_data["30-60 Days"], - age_wise_data["60-90 Days"], age_wise_data["90 Above"]] + values = [ + age_wise_data["0-30 Days"], + age_wise_data["30-60 Days"], + age_wise_data["60-90 Days"], + age_wise_data["90 Above"], + ] chart = { - "data": { - 'labels': labels, - 'datasets': [{'name':'Qty Wise Chart', 'values': values}] - }, + "data": {"labels": labels, "datasets": [{"name": "Qty Wise Chart", "values": values}]}, "type": "donut", - "height": 300 + "height": 300, } return chart + def get_chart_based_on_qty(data, filters): labels, periodic_data = prepare_chart_data(data, filters) @@ -129,25 +139,18 @@ def get_chart_based_on_qty(data, filters): datasets.append({"name": "Completed", "values": completed}) chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, + "data": {"labels": labels, "datasets": datasets}, "type": "bar", - "barOptions": { - "stacked": 1 - } + "barOptions": {"stacked": 1}, } return chart + def prepare_chart_data(data, filters): labels = [] - periodic_data = { - "Pending": {}, - "Completed": {} - } + periodic_data = {"Pending": {}, "Completed": {}} filters.range = "Monthly" @@ -165,11 +168,12 @@ def prepare_chart_data(data, filters): for d in data: if getdate(d.planned_start_date) >= from_date and getdate(d.planned_start_date) <= end_date: - periodic_data["Pending"][period] += (flt(d.qty) - flt(d.produced_qty)) + periodic_data["Pending"][period] += flt(d.qty) - flt(d.produced_qty) periodic_data["Completed"][period] += flt(d.produced_qty) return labels, periodic_data + def get_columns(filters): columns = [ { @@ -177,90 +181,77 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Work Order", - "width": 100 + "width": 100, }, ] if not filters.get("status"): columns.append( - { - "label": _("Status"), - "fieldname": "status", - "width": 100 - }, + {"label": _("Status"), "fieldname": "status", "width": 100}, ) - columns.extend([ - { - "label": _("Production Item"), - "fieldname": "production_item", - "fieldtype": "Link", - "options": "Item", - "width": 130 - }, - { - "label": _("Produce Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Produced Qty"), - "fieldname": "produced_qty", - "fieldtype": "Float", - "width": 110 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 90 - }, - { - "label": _("Planned Start Date"), - "fieldname": "planned_start_date", - "fieldtype": "Date", - "width": 150 - }, - { - "label": _("Planned End Date"), - "fieldname": "planned_end_date", - "fieldtype": "Date", - "width": 150 - } - ]) - - if filters.get("status") != 'Not Started': - columns.extend([ + columns.extend( + [ { - "label": _("Actual Start Date"), - "fieldname": "actual_start_date", + "label": _("Production Item"), + "fieldname": "production_item", + "fieldtype": "Link", + "options": "Item", + "width": 130, + }, + {"label": _("Produce Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 110}, + {"label": _("Produced Qty"), "fieldname": "produced_qty", "fieldtype": "Float", "width": 110}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 90, + }, + { + "label": _("Planned Start Date"), + "fieldname": "planned_start_date", "fieldtype": "Date", - "width": 100 + "width": 150, }, { - "label": _("Actual End Date"), - "fieldname": "actual_end_date", + "label": _("Planned End Date"), + "fieldname": "planned_end_date", "fieldtype": "Date", - "width": 100 + "width": 150, }, - { - "label": _("Age"), - "fieldname": "age", - "fieldtype": "Float", - "width": 110 - }, - ]) + ] + ) - if filters.get("status") == 'Completed': - columns.extend([ - { - "label": _("Lead Time (in mins)"), - "fieldname": "lead_time", - "fieldtype": "Float", - "width": 110 - }, - ]) + if filters.get("status") != "Not Started": + columns.extend( + [ + { + "label": _("Actual Start Date"), + "fieldname": "actual_start_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual End Date"), + "fieldname": "actual_end_date", + "fieldtype": "Date", + "width": 100, + }, + {"label": _("Age"), "fieldname": "age", "fieldtype": "Float", "width": 110}, + ] + ) + + if filters.get("status") == "Completed": + columns.extend( + [ + { + "label": _("Lead Time (in mins)"), + "fieldname": "lead_time", + "fieldtype": "Float", + "width": 110, + }, + ] + ) return columns diff --git a/erpnext/non_profit/doctype/chapter/chapter.py b/erpnext/non_profit/doctype/chapter/chapter.py index c01b1ef3e42..ae2f75824d4 100644 --- a/erpnext/non_profit/doctype/chapter/chapter.py +++ b/erpnext/non_profit/doctype/chapter/chapter.py @@ -8,22 +8,21 @@ from frappe.website.website_generator import WebsiteGenerator class Chapter(WebsiteGenerator): _website = frappe._dict( - condition_field = "published", + condition_field="published", ) def get_context(self, context): context.no_cache = True context.show_sidebar = True - context.parents = [dict(label='View All Chapters', - route='chapters', title='View Chapters')] + context.parents = [dict(label="View All Chapters", route="chapters", title="View Chapters")] def validate(self): - if not self.route: #pylint: disable=E0203 - self.route = 'chapters/' + self.scrub(self.name) + if not self.route: # pylint: disable=E0203 + self.route = "chapters/" + self.scrub(self.name) def enable(self): - chapter = frappe.get_doc('Chapter', frappe.form_dict.name) - chapter.append('members', dict(enable=self.value)) + chapter = frappe.get_doc("Chapter", frappe.form_dict.name) + chapter.append("members", dict(enable=self.value)) chapter.save(ignore_permissions=1) frappe.db.commit() @@ -32,9 +31,9 @@ def get_list_context(context): context.allow_guest = True context.no_cache = True context.show_sidebar = True - context.title = 'All Chapters' + context.title = "All Chapters" context.no_breadcrumbs = True - context.order_by = 'creation desc' + context.order_by = "creation desc" @frappe.whitelist() diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index 85f5a2652e9..8e5ac5b61bf 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -16,28 +16,30 @@ from erpnext.non_profit.doctype.membership.membership import verify_signature class Donation(Document): def validate(self): - if not self.donor or not frappe.db.exists('Donor', self.donor): + if not self.donor or not frappe.db.exists("Donor", self.donor): # for web forms - user_type = frappe.db.get_value('User', frappe.session.user, 'user_type') - if user_type == 'Website User': + user_type = frappe.db.get_value("User", frappe.session.user, "user_type") + if user_type == "Website User": self.create_donor_for_website_user() else: - frappe.throw(_('Please select a Member')) + frappe.throw(_("Please select a Member")) def create_donor_for_website_user(self): - donor_name = frappe.get_value('Donor', dict(email=frappe.session.user)) + donor_name = frappe.get_value("Donor", dict(email=frappe.session.user)) if not donor_name: - user = frappe.get_doc('User', frappe.session.user) - donor = frappe.get_doc(dict( - doctype='Donor', - donor_type=self.get('donor_type'), - email=frappe.session.user, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) + user = frappe.get_doc("User", frappe.session.user) + donor = frappe.get_doc( + dict( + doctype="Donor", + donor_type=self.get("donor_type"), + email=frappe.session.user, + member_name=user.get_fullname(), + ) + ).insert(ignore_permissions=True) donor_name = donor.name - if self.get('__islocal'): + if self.get("__islocal"): self.donor = donor_name def on_payment_authorized(self, *args, **kwargs): @@ -45,13 +47,16 @@ class Donation(Document): self.create_payment_entry() def create_payment_entry(self, date=None): - settings = frappe.get_doc('Non Profit Settings') + settings = frappe.get_doc("Non Profit Settings") if not settings.automate_donation_payment_entries: return if not settings.donation_payment_account: - frappe.throw(_('You need to set Payment Account for Donation in {0}').format( - get_link_to_form('Non Profit Settings', 'Non Profit Settings'))) + frappe.throw( + _("You need to set Payment Account for Donation in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings") + ) + ) from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -71,31 +76,31 @@ class Donation(Document): @frappe.whitelist(allow_guest=True) def capture_razorpay_donations(*args, **kwargs): """ - Creates Donation from Razorpay Webhook Request Data on payment.captured event - Creates Donor from email if not found + Creates Donation from Razorpay Webhook Request Data on payment.captured event + Creates Donor from email if not found """ data = frappe.request.get_data(as_text=True) try: - verify_signature(data, endpoint='Donation') + verify_signature(data, endpoint="Donation") except Exception as e: - log = frappe.log_error(e, 'Donation Webhook Verification Error') + log = frappe.log_error(e, "Donation Webhook Verification Error") notify_failure(log) - return { 'status': 'Failed', 'reason': e } + return {"status": "Failed", "reason": e} if isinstance(data, six.string_types): data = json.loads(data) data = frappe._dict(data) - payment = data.payload.get('payment', {}).get('entity', {}) + payment = data.payload.get("payment", {}).get("entity", {}) payment = frappe._dict(payment) try: - if not data.event == 'payment.captured': + if not data.event == "payment.captured": return # to avoid capturing subscription payments as donations - if payment.description and 'subscription' in str(payment.description).lower(): + if payment.description and "subscription" in str(payment.description).lower(): return donor = get_donor(payment.email) @@ -103,45 +108,45 @@ def capture_razorpay_donations(*args, **kwargs): donor = create_donor(payment) donation = create_razorpay_donation(donor, payment) - donation.run_method('create_payment_entry') + donation.run_method("create_payment_entry") except Exception as e: - message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id) - log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name)) + message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) + log = frappe.log_error(message, _("Error creating donation entry for {0}").format(donor.name)) notify_failure(log) - return { 'status': 'Failed', 'reason': e } + return {"status": "Failed", "reason": e} - return { 'status': 'Success' } + return {"status": "Success"} def create_razorpay_donation(donor, payment): - if not frappe.db.exists('Mode of Payment', payment.method): + if not frappe.db.exists("Mode of Payment", payment.method): create_mode_of_payment(payment.method) company = get_company_for_donations() - donation = frappe.get_doc({ - 'doctype': 'Donation', - 'company': company, - 'donor': donor.name, - 'donor_name': donor.donor_name, - 'email': donor.email, - 'date': getdate(), - 'amount': flt(payment.amount) / 100, # Convert to rupees from paise - 'mode_of_payment': payment.method, - 'payment_id': payment.id - }).insert(ignore_mandatory=True) + donation = frappe.get_doc( + { + "doctype": "Donation", + "company": company, + "donor": donor.name, + "donor_name": donor.donor_name, + "email": donor.email, + "date": getdate(), + "amount": flt(payment.amount) / 100, # Convert to rupees from paise + "mode_of_payment": payment.method, + "payment_id": payment.id, + } + ).insert(ignore_mandatory=True) donation.submit() return donation def get_donor(email): - donors = frappe.get_all('Donor', - filters={'email': email}, - order_by='creation desc') + donors = frappe.get_all("Donor", filters={"email": email}, order_by="creation desc") try: - return frappe.get_doc('Donor', donors[0]['name']) + return frappe.get_doc("Donor", donors[0]["name"]) except Exception: return None @@ -149,17 +154,19 @@ def get_donor(email): @frappe.whitelist() def create_donor(payment): donor_details = frappe._dict(payment) - donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type') + donor_type = frappe.db.get_single_value("Non Profit Settings", "default_donor_type") - donor = frappe.new_doc('Donor') - donor.update({ - 'donor_name': donor_details.email, - 'donor_type': donor_type, - 'email': donor_details.email, - 'contact': donor_details.contact - }) + donor = frappe.new_doc("Donor") + donor.update( + { + "donor_name": donor_details.email, + "donor_type": donor_type, + "email": donor_details.email, + "contact": donor_details.contact, + } + ) - if donor_details.get('notes'): + if donor_details.get("notes"): donor = get_additional_notes(donor, donor_details) donor.insert(ignore_mandatory=True) @@ -167,9 +174,10 @@ def create_donor(payment): def get_company_for_donations(): - company = frappe.db.get_single_value('Non Profit Settings', 'donation_company') + company = frappe.db.get_single_value("Non Profit Settings", "donation_company") if not company: from erpnext.healthcare.setup import get_company + company = get_company() return company @@ -177,45 +185,44 @@ def get_company_for_donations(): def get_additional_notes(donor, donor_details): if type(donor_details.notes) == dict: for k, v in donor_details.notes.items(): - notes = '\n'.join('{}: {}'.format(k, v)) + notes = "\n".join("{}: {}".format(k, v)) # extract donor name from notes - if 'name' in k.lower(): - donor.update({ - 'donor_name': donor_details.notes.get(k) - }) + if "name" in k.lower(): + donor.update({"donor_name": donor_details.notes.get(k)}) # extract pan from notes - if 'pan' in k.lower(): - donor.update({ - 'pan_number': donor_details.notes.get(k) - }) + if "pan" in k.lower(): + donor.update({"pan_number": donor_details.notes.get(k)}) - donor.add_comment('Comment', notes) + donor.add_comment("Comment", notes) elif type(donor_details.notes) == str: - donor.add_comment('Comment', donor_details.notes) + donor.add_comment("Comment", donor_details.notes) return donor def create_mode_of_payment(method): - frappe.get_doc({ - 'doctype': 'Mode of Payment', - 'mode_of_payment': method - }).insert(ignore_mandatory=True) + frappe.get_doc({"doctype": "Mode of Payment", "mode_of_payment": method}).insert( + ignore_mandatory=True + ) def notify_failure(log): try: - content = ''' + content = """ Dear System Manager, Razorpay webhook for creating donation failed due to some reason. Please check the error log linked below Error Log: {0} Regards, Administrator - '''.format(get_link_to_form('Error Log', log.name)) + """.format( + get_link_to_form("Error Log", log.name) + ) - sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content) + sendmail_to_system_managers( + _("[Important] [ERPNext] Razorpay donation webhook failed, please check."), content + ) except Exception: pass diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py index 1d43ba23dcf..53700ca1ecb 100644 --- a/erpnext/non_profit/doctype/donation/donation_dashboard.py +++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py @@ -1,17 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'donation', - 'non_standard_fieldnames': { - 'Payment Entry': 'reference_name' - }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry'] - } - ] + "fieldname": "donation", + "non_standard_fieldnames": {"Payment Entry": "reference_name"}, + "transactions": [{"label": _("Payment"), "items": ["Payment Entry"]}], } diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py index 4e39adbcf26..f24d0555b2a 100644 --- a/erpnext/non_profit/doctype/donation/test_donation.py +++ b/erpnext/non_profit/doctype/donation/test_donation.py @@ -11,25 +11,21 @@ from erpnext.non_profit.doctype.donation.donation import create_razorpay_donatio class TestDonation(unittest.TestCase): def setUp(self): create_donor_type() - settings = frappe.get_doc('Non Profit Settings') - settings.company = '_Test Company' - settings.donation_company = '_Test Company' - settings.default_donor_type = '_Test Donor' + settings = frappe.get_doc("Non Profit Settings") + settings.company = "_Test Company" + settings.donation_company = "_Test Company" + settings.default_donor_type = "_Test Donor" settings.automate_donation_payment_entries = 1 - settings.donation_debit_account = 'Debtors - _TC' - settings.donation_payment_account = 'Cash - _TC' - settings.creation_user = 'Administrator' + settings.donation_debit_account = "Debtors - _TC" + settings.donation_payment_account = "Cash - _TC" + settings.creation_user = "Administrator" settings.flags.ignore_permissions = True settings.save() def test_payment_entry_for_donations(self): donor = create_donor() create_mode_of_payment() - payment = frappe._dict({ - 'amount': 100, - 'method': 'Debit Card', - 'id': 'pay_MeXAmsgeKOhq7O' - }) + payment = frappe._dict({"amount": 100, "method": "Debit Card", "id": "pay_MeXAmsgeKOhq7O"}) donation = create_razorpay_donation(donor, payment) self.assertTrue(donation.name) @@ -41,37 +37,35 @@ class TestDonation(unittest.TestCase): donation.reload() self.assertEqual(donation.paid, 1) - self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name})) + self.assertTrue(frappe.db.exists("Payment Entry", {"reference_no": donation.name})) def create_donor_type(): - if not frappe.db.exists('Donor Type', '_Test Donor'): - frappe.get_doc({ - 'doctype': 'Donor Type', - 'donor_type': '_Test Donor' - }).insert() + if not frappe.db.exists("Donor Type", "_Test Donor"): + frappe.get_doc({"doctype": "Donor Type", "donor_type": "_Test Donor"}).insert() def create_donor(): - donor = frappe.db.exists('Donor', 'donor@test.com') + donor = frappe.db.exists("Donor", "donor@test.com") if donor: - return frappe.get_doc('Donor', 'donor@test.com') + return frappe.get_doc("Donor", "donor@test.com") else: - return frappe.get_doc({ - 'doctype': 'Donor', - 'donor_name': '_Test Donor', - 'donor_type': '_Test Donor', - 'email': 'donor@test.com' - }).insert() + return frappe.get_doc( + { + "doctype": "Donor", + "donor_name": "_Test Donor", + "donor_type": "_Test Donor", + "email": "donor@test.com", + } + ).insert() def create_mode_of_payment(): - if not frappe.db.exists('Mode of Payment', 'Debit Card'): - frappe.get_doc({ - 'doctype': 'Mode of Payment', - 'mode_of_payment': 'Debit Card', - 'accounts': [{ - 'company': '_Test Company', - 'default_account': 'Cash - _TC' - }] - }).insert() + if not frappe.db.exists("Mode of Payment", "Debit Card"): + frappe.get_doc( + { + "doctype": "Mode of Payment", + "mode_of_payment": "Debit Card", + "accounts": [{"company": "_Test Company", "default_account": "Cash - _TC"}], + } + ).insert() diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py index 058321b1591..77ec17e823f 100644 --- a/erpnext/non_profit/doctype/donor/donor.py +++ b/erpnext/non_profit/doctype/donor/donor.py @@ -13,5 +13,6 @@ class Donor(Document): def validate(self): from frappe.utils import validate_email_address + if self.email: validate_email_address(self.email.strip(), True) diff --git a/erpnext/non_profit/doctype/grant_application/grant_application.py b/erpnext/non_profit/doctype/grant_application/grant_application.py index cc5e1b1442d..03db783696a 100644 --- a/erpnext/non_profit/doctype/grant_application/grant_application.py +++ b/erpnext/non_profit/doctype/grant_application/grant_application.py @@ -11,12 +11,12 @@ from frappe.website.website_generator import WebsiteGenerator class GrantApplication(WebsiteGenerator): _website = frappe._dict( - condition_field = "published", + condition_field="published", ) def validate(self): - if not self.route: #pylint: disable=E0203 - self.route = 'grant-application/' + self.scrub(self.name) + if not self.route: # pylint: disable=E0203 + self.route = "grant-application/" + self.scrub(self.name) def onload(self): """Load address and contacts in `__onload`""" @@ -25,32 +25,35 @@ class GrantApplication(WebsiteGenerator): def get_context(self, context): context.no_cache = True context.show_sidebar = True - context.parents = [dict(label='View All Grant Applications', - route='grant-application', title='View Grants')] + context.parents = [ + dict(label="View All Grant Applications", route="grant-application", title="View Grants") + ] + def get_list_context(context): context.allow_guest = True context.no_cache = True context.no_breadcrumbs = True context.show_sidebar = True - context.order_by = 'creation desc' - context.introduction =''' - Apply for new Grant Application''' + context.order_by = "creation desc" + context.introduction = """ + Apply for new Grant Application""" + @frappe.whitelist() def send_grant_review_emails(grant_application): grant = frappe.get_doc("Grant Application", grant_application) - url = get_url('grant-application/{0}'.format(grant_application)) + url = get_url("grant-application/{0}".format(grant_application)) frappe.sendmail( - recipients= grant.assessment_manager, + recipients=grant.assessment_manager, sender=frappe.session.user, - subject='Grant Application for {0}'.format(grant.applicant_name), - message='

    Please Review this grant application


    ' + url, + subject="Grant Application for {0}".format(grant.applicant_name), + message="

    Please Review this grant application


    " + url, reference_doctype=grant.doctype, - reference_name=grant.name + reference_name=grant.name, ) - grant.status = 'In Progress' + grant.status = "In Progress" grant.email_notification_sent = 1 grant.save() frappe.db.commit() diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 7639c2de68f..d2e4ae45fb0 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -17,20 +17,21 @@ class Member(Document): """Load address and contacts in `__onload`""" load_address_and_contact(self) - def validate(self): if self.email_id: self.validate_email_type(self.email_id) def validate_email_type(self, email): from frappe.utils import validate_email_address + validate_email_address(email.strip(), True) def setup_subscription(self): - non_profit_settings = frappe.get_doc('Non Profit Settings') + non_profit_settings = frappe.get_doc("Non Profit Settings") if not non_profit_settings.enable_razorpay_for_memberships: - frappe.throw(_('Please check Enable Razorpay for Memberships in {0} to setup subscription')).format( - get_link_to_form('Non Profit Settings', 'Non Profit Settings')) + frappe.throw( + _("Please check Enable Razorpay for Memberships in {0} to setup subscription") + ).format(get_link_to_form("Non Profit Settings", "Non Profit Settings")) controller = get_payment_gateway_controller("Razorpay") settings = controller.get_settings({}) @@ -43,12 +44,10 @@ class Member(Document): subscription_details = { "plan_id": plan_id, "billing_frequency": cint(non_profit_settings.billing_frequency), - "customer_notify": 1 + "customer_notify": 1, } - args = { - 'subscription_details': subscription_details - } + args = {"subscription_details": subscription_details} subscription = controller.setup_subscription(settings, **args) @@ -59,11 +58,9 @@ class Member(Document): if self.customer: frappe.msgprint(_("A customer is already linked to this Member")) - customer = create_customer(frappe._dict({ - 'fullname': self.member_name, - 'email': self.email_id, - 'phone': None - })) + customer = create_customer( + frappe._dict({"fullname": self.member_name, "email": self.email_id, "phone": None}) + ) self.customer = customer self.save() @@ -71,24 +68,29 @@ class Member(Document): def get_or_create_member(user_details): - member_list = frappe.get_all("Member", filters={'email': user_details.email, 'membership_type': user_details.plan_id}) + member_list = frappe.get_all( + "Member", filters={"email": user_details.email, "membership_type": user_details.plan_id} + ) if member_list and member_list[0]: - return member_list[0]['name'] + return member_list[0]["name"] else: return create_member(user_details) + def create_member(user_details): user_details = frappe._dict(user_details) member = frappe.new_doc("Member") - member.update({ - "member_name": user_details.fullname, - "email_id": user_details.email, - "pan_number": user_details.pan or None, - "membership_type": user_details.plan_id, - "customer_id": user_details.customer_id or None, - "subscription_id": user_details.subscription_id or None, - "subscription_status": user_details.subscription_status or "" - }) + member.update( + { + "member_name": user_details.fullname, + "email_id": user_details.email, + "pan_number": user_details.pan or None, + "membership_type": user_details.plan_id, + "customer_id": user_details.customer_id or None, + "subscription_id": user_details.subscription_id or None, + "subscription_status": user_details.subscription_status or "", + } + ) member.insert(ignore_permissions=True) member.customer = create_customer(user_details, member.name) @@ -96,6 +98,7 @@ def create_member(user_details): return member + def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname @@ -115,16 +118,10 @@ def create_customer(user_details, member=None): contact.add_email(user_details.email, is_primary=1) contact.insert(ignore_permissions=True) - contact.append("links", { - "link_doctype": "Customer", - "link_name": customer.name - }) + contact.append("links", {"link_doctype": "Customer", "link_name": customer.name}) if member: - contact.append("links", { - "link_doctype": "Member", - "link_name": member - }) + contact.append("links", {"link_doctype": "Member", "link_name": member}) contact.save(ignore_permissions=True) @@ -138,23 +135,24 @@ def create_customer(user_details, member=None): return customer.name + @frappe.whitelist(allow_guest=True) def create_member_subscription_order(user_details): """Create Member subscription and order for payment Args: - user_details (TYPE): Description + user_details (TYPE): Description Returns: - Dictionary: Dictionary with subscription details - { - 'subscription_details': { - 'plan_id': 'plan_EXwyxDYDCj3X4v', - 'billing_frequency': 24, - 'customer_notify': 1 - }, - 'subscription_id': 'sub_EZycCvXFvqnC6p' - } + Dictionary: Dictionary with subscription details + { + 'subscription_details': { + 'plan_id': 'plan_EXwyxDYDCj3X4v', + 'billing_frequency': 24, + 'customer_notify': 1 + }, + 'subscription_id': 'sub_EZycCvXFvqnC6p' + } """ user_details = frappe._dict(user_details) @@ -162,28 +160,31 @@ def create_member_subscription_order(user_details): subscription = member.setup_subscription() - member.subscription_id = subscription.get('subscription_id') + member.subscription_id = subscription.get("subscription_id") member.save(ignore_permissions=True) return subscription + @frappe.whitelist() def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): plan = get_membership_type(rzpay_plan_id) if not plan: raise frappe.DoesNotExistError - member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) + member = frappe.db.exists("Member", {"email": email, "subscription_id": subscription_id}) if member: return member else: - member = create_member(dict( - fullname=fullname, - email=email, - plan_id=plan, - subscription_id=subscription_id, - pan=pan, - mobile=mobile - )) + member = create_member( + dict( + fullname=fullname, + email=email, + plan_id=plan, + subscription_id=subscription_id, + pan=pan, + mobile=mobile, + ) + ) return member.name diff --git a/erpnext/non_profit/doctype/member/member_dashboard.py b/erpnext/non_profit/doctype/member/member_dashboard.py index 80bb9e3250d..19d9c388e19 100644 --- a/erpnext/non_profit/doctype/member/member_dashboard.py +++ b/erpnext/non_profit/doctype/member/member_dashboard.py @@ -1,23 +1,14 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('Member Activity'), - 'fieldname': 'member', - 'non_standard_fieldnames': { - 'Bank Account': 'party' - }, - 'transactions': [ - { - 'label': _('Membership Details'), - 'items': ['Membership'] - }, - { - 'label': _('Fee'), - 'items': ['Bank Account'] - } - ] + "heatmap": True, + "heatmap_message": _("Member Activity"), + "fieldname": "member", + "non_standard_fieldnames": {"Bank Account": "party"}, + "transactions": [ + {"label": _("Membership Details"), "items": ["Membership"]}, + {"label": _("Fee"), "items": ["Bank Account"]}, + ], } diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 2809c8da1a7..f29005a6d4b 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -33,12 +33,14 @@ class Membership(Document): if not member_name: user = frappe.get_doc("User", frappe.session.user) - member = frappe.get_doc(dict( - doctype="Member", - email_id=frappe.session.user, - membership_type=self.membership_type, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) + member = frappe.get_doc( + dict( + doctype="Member", + email_id=frappe.session.user, + membership_type=self.membership_type, + member_name=user.get_fullname(), + ) + ).insert(ignore_permissions=True) member_name = member.name if self.get("__islocal"): @@ -49,9 +51,13 @@ class Membership(Document): last_membership = erpnext.get_last_membership(self.member) # if person applied for offline membership - if last_membership and last_membership.name != self.name and not frappe.session.user == "Administrator": + if ( + last_membership + and last_membership.name != self.name + and not frappe.session.user == "Administrator" + ): # if last membership does not expire in 30 days, then do not allow to renew - if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : + if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()): frappe.throw(_("You can only renew if your membership expires within 30 days")) self.from_date = add_days(last_membership.to_date, 1) @@ -72,13 +78,16 @@ class Membership(Document): self.db_set("paid", 1) settings = frappe.get_doc("Non Profit Settings") if settings.allow_invoicing and settings.automate_membership_invoicing: - self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) - + self.generate_invoice( + with_payment_entry=settings.automate_membership_payment_entries, save=True + ) @frappe.whitelist() def generate_invoice(self, save=True, with_payment_entry=False): if not (self.paid or self.currency or self.amount): - frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) + frappe.throw( + _("The payment for this membership is not paid. To generate invoice fill the payment details") + ) if self.invoice: frappe.throw(_("An invoice is already linked to this document")) @@ -110,21 +119,30 @@ class Membership(Document): frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) if not settings.company: - frappe.throw(_("You need to set Default Company for invoicing in {0}").format(settings_link)) + frappe.throw( + _("You need to set Default Company for invoicing in {0}").format(settings_link) + ) if not plan.linked_item: - frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format( - get_link_to_form("Membership Type", self.membership_type))) + frappe.throw( + _("Please set a Linked Item for the Membership Type {0}").format( + get_link_to_form("Membership Type", self.membership_type) + ) + ) def make_payment_entry(self, settings, invoice): if not settings.membership_payment_account: - frappe.throw(_("You need to set Payment Account for Membership in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit Settings"))) + frappe.throw( + _("You need to set Payment Account for Membership in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings") + ) + ) from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + frappe.flags.ignore_account_permission = True pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) - frappe.flags.ignore_account_permission=False + frappe.flags.ignore_account_permission = False pe.paid_to = settings.membership_payment_account pe.reference_no = self.name pe.reference_date = getdate() @@ -136,22 +154,33 @@ class Membership(Document): def send_acknowlement(self): settings = frappe.get_doc("Non Profit Settings") if not settings.send_email: - frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit Settings"))) + frappe.throw( + _("You need to enable Send Acknowledge Email in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings") + ) + ) member = frappe.get_doc("Member", self.member) if not member.email_id: - frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member))) + frappe.throw( + _("Email address of member {0} is missing").format( + frappe.utils.get_link_to_form("Member", self.member) + ) + ) plan = frappe.get_doc("Membership Type", self.membership_type) email = member.email_id - attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] + attachments = [ + frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format) + ] if self.invoice and settings.send_invoice: - attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + attachments.append( + frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format) + ) email_template = frappe.get_doc("Email Template", settings.email_template) - context = { "doc": self, "member": member} + context = {"doc": self, "member": member} email_args = { "recipients": [email], @@ -159,7 +188,7 @@ class Membership(Document): "subject": frappe.render_template(email_template.get("subject"), context), "attachments": attachments, "reference_doctype": self.doctype, - "reference_name": self.name + "reference_name": self.name, } if not frappe.flags.in_test: @@ -173,21 +202,17 @@ class Membership(Document): def make_invoice(membership, member, plan, settings): - invoice = frappe.get_doc({ - "doctype": "Sales Invoice", - "customer": member.customer, - "debit_to": settings.membership_debit_account, - "currency": membership.currency, - "company": settings.company, - "is_pos": 0, - "items": [ - { - "item_code": plan.linked_item, - "rate": membership.amount, - "qty": 1 - } - ] - }) + invoice = frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": member.customer, + "debit_to": settings.membership_debit_account, + "currency": membership.currency, + "company": settings.company, + "is_pos": 0, + "items": [{"item_code": plan.linked_item, "rate": membership.amount, "qty": 1}], + } + ) invoice.set_missing_values() invoice.insert() invoice.submit() @@ -241,11 +266,15 @@ def trigger_razorpay_subscription(*args, **kwargs): member = get_member_based_on_subscription(subscription.id, payment.email) if not member: - member = create_member(frappe._dict({ - "fullname": payment.email, - "email": payment.email, - "plan_id": get_plan_from_razorpay_id(subscription.plan_id) - })) + member = create_member( + frappe._dict( + { + "fullname": payment.email, + "email": payment.email, + "plan_id": get_plan_from_razorpay_id(subscription.plan_id), + } + ) + ) member.subscription_id = subscription.id member.customer_id = payment.customer_id @@ -256,18 +285,20 @@ def trigger_razorpay_subscription(*args, **kwargs): company = get_company_for_memberships() # Update Membership membership = frappe.new_doc("Membership") - membership.update({ - "company": company, - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) + membership.update( + { + "company": company, + "member": member.name, + "membership_status": "Current", + "membership_type": member.membership_type, + "currency": "INR", + "paid": 1, + "payment_id": payment.id, + "from_date": datetime.fromtimestamp(subscription.current_start), + "to_date": datetime.fromtimestamp(subscription.current_end), + "amount": payment.amount / 100, # Convert to rupees from paise + } + ) membership.flags.ignore_mandatory = True membership.insert() @@ -281,7 +312,9 @@ def trigger_razorpay_subscription(*args, **kwargs): settings = frappe.get_doc("Non Profit Settings") if settings.allow_invoicing and settings.automate_membership_invoicing: membership.reload() - membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) + membership.generate_invoice( + with_payment_entry=settings.automate_membership_payment_entries, save=True + ) except Exception as e: message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) @@ -328,7 +361,9 @@ def update_halted_razorpay_subscription(*args, **kwargs): except Exception as e: message = "{0}\n\n{1}".format(e, frappe.get_traceback()) - log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name)) + log = frappe.log_error( + message, _("Error updating halted status for member {0}").format(member.name) + ) notify_failure(log) return {"status": "Failed", "reason": e} @@ -354,6 +389,7 @@ def get_company_for_memberships(): company = frappe.db.get_single_value("Non Profit Settings", "company") if not company: from erpnext.healthcare.setup import get_company + company = get_company() return company @@ -365,15 +401,11 @@ def get_additional_notes(member, subscription): # extract member name from notes if "name" in k.lower(): - member.update({ - "member_name": subscription.notes.get(k) - }) + member.update({"member_name": subscription.notes.get(k)}) # extract pan number from notes if "pan" in k.lower(): - member.update({ - "pan_number": subscription.notes.get(k) - }) + member.update({"pan_number": subscription.notes.get(k)}) member.add_comment("Comment", notes) @@ -391,15 +423,21 @@ def notify_failure(log): Please check the following error log linked below Error Log: {0} Regards, Administrator - """.format(get_link_to_form("Error Log", log.name)) + """.format( + get_link_to_form("Error Log", log.name) + ) - sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) + sendmail_to_system_managers( + "[Important] [ERPNext] Razorpay membership webhook failed , please check.", content + ) except Exception: pass def get_plan_from_razorpay_id(plan_id): - plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc") + plan = frappe.get_all( + "Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc" + ) try: return plan[0]["name"] @@ -408,9 +446,12 @@ def get_plan_from_razorpay_id(plan_id): def set_expired_status(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabMembership` SET `membership_status` = 'Expired' WHERE `membership_status` not in ('Cancelled') AND `to_date` < %s - """, (nowdate())) + """, + (nowdate()), + ) diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index fbe344c6a15..aef34a69606 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -17,14 +17,16 @@ class TestMembership(unittest.TestCase): # make test member self.member_doc = create_member( - frappe._dict({ - "fullname": "_Test_Member", - "email": "_test_member_erpnext@example.com", - "plan_id": plan.name, - "subscription_id": "sub_DEX6xcJ1HSW4CR", - "customer_id": "cust_C0WlbKhp3aLA7W", - "subscription_status": "Active" - }) + frappe._dict( + { + "fullname": "_Test_Member", + "email": "_test_member_erpnext@example.com", + "plan_id": plan.name, + "subscription_id": "sub_DEX6xcJ1HSW4CR", + "customer_id": "cust_C0WlbKhp3aLA7W", + "subscription_status": "Active", + } + ) ) self.member_doc.make_customer_and_link() self.member = self.member_doc.name @@ -40,30 +42,38 @@ class TestMembership(unittest.TestCase): def test_renew_within_30_days(self): # create a membership for two months # Should work fine - make_membership(self.member, { "from_date": nowdate() }) - make_membership(self.member, { "from_date": add_months(nowdate(), 1) }) + make_membership(self.member, {"from_date": nowdate()}) + make_membership(self.member, {"from_date": add_months(nowdate(), 1)}) from frappe.utils.user import add_role + add_role("test@example.com", "Non Profit Manager") frappe.set_user("test@example.com") # create next membership with expiry not within 30 days - self.assertRaises(frappe.ValidationError, make_membership, self.member, { - "from_date": add_months(nowdate(), 2), - }) + self.assertRaises( + frappe.ValidationError, + make_membership, + self.member, + { + "from_date": add_months(nowdate(), 2), + }, + ) frappe.set_user("Administrator") # create the same membership but as administrator - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3), - }) + make_membership( + self.member, + { + "from_date": add_months(nowdate(), 2), + "to_date": add_months(nowdate(), 3), + }, + ) def test_halted_memberships(self): - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3) - }) + make_membership( + self.member, {"from_date": add_months(nowdate(), 2), "to_date": add_months(nowdate(), 3)} + ) self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active") payload = get_subscription_payload() @@ -73,9 +83,11 @@ class TestMembership(unittest.TestCase): def tearDown(self): frappe.db.rollback() + def set_config(key, value): frappe.db.set_value("Non Profit Settings", None, key, value) + def make_membership(member, payload={}): data = { "doctype": "Membership", @@ -85,13 +97,14 @@ def make_membership(member, payload={}): "currency": "INR", "paid": 1, "from_date": nowdate(), - "amount": 100 + "amount": 100, } data.update(payload) membership = frappe.get_doc(data) membership.insert(ignore_permissions=True, ignore_if_duplicate=True) return membership + def create_item(item_code): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") @@ -106,6 +119,7 @@ def create_item(item_code): item = frappe.get_doc("Item", item_code) return item + def setup_membership(): # Get default company company = frappe.get_doc("Company", erpnext.get_default_company()) @@ -139,14 +153,13 @@ def setup_membership(): return plan + def get_subscription_payload(): return { "entity": "event", "account_id": "acc_BFQ7uQEaa7j2z7", "event": "subscription.halted", - "contains": [ - "subscription" - ], + "contains": ["subscription"], "payload": { "subscription": { "entity": { @@ -155,10 +168,8 @@ def get_subscription_payload(): "plan_id": "_rzpy_test_milythm", "customer_id": "cust_C0WlbKhp3aLA7W", "status": "halted", - "notes": { - "Important": "Notes for Internal Reference" - }, + "notes": {"Important": "Notes for Internal Reference"}, } } - } + }, } diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index b4464215715..33a7ffd75bd 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -14,5 +14,6 @@ class MembershipType(Document): if is_stock_item: frappe.throw(_("The Linked Item should be a service item")) + def get_membership_type(razorpay_id): return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py index ace66055427..ae36a335198 100644 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py @@ -18,8 +18,12 @@ class NonProfitSettings(Document): secret_for = "Membership" if field == "membership_webhook_secret" else "Donation" frappe.msgprint( - _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "

    " + key, - _("Webhook Secret") + _("Here is your webhook secret for {0} API, this will be shown to you only once.").format( + secret_for + ) + + "

    " + + key, + _("Webhook Secret"), ) @frappe.whitelist() @@ -28,9 +32,12 @@ class NonProfitSettings(Document): self.save() def get_webhook_secret(self, endpoint="Membership"): - fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret" + fieldname = ( + "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret" + ) return self.get_password(fieldname=fieldname, raise_exception=False) + @frappe.whitelist() def get_plans_for_membership(*args, **kwargs): controller = get_payment_gateway_controller("Razorpay") diff --git a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py index 3ddbfdc3b0d..58f5bfec9dc 100644 --- a/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py +++ b/erpnext/non_profit/report/expiring_memberships/expiring_memberships.py @@ -11,18 +11,37 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): return [ - _("Membership Type") + ":Link/Membership Type:100", _("Membership ID") + ":Link/Membership:140", - _("Member ID") + ":Link/Member:140", _("Member Name") + ":Data:140", _("Email") + ":Data:140", - _("Expiring On") + ":Date:120" + _("Membership Type") + ":Link/Membership Type:100", + _("Membership ID") + ":Link/Membership:140", + _("Member ID") + ":Link/Member:140", + _("Member Name") + ":Data:140", + _("Email") + ":Data:140", + _("Expiring On") + ":Date:120", ] + def get_data(filters): - filters["month"] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].index(filters.month) + 1 + filters["month"] = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ].index(filters.month) + 1 - return frappe.db.sql(""" + return frappe.db.sql( + """ select ms.membership_type,ms.name,m.name,m.member_name,m.email,ms.max_membership_date from `tabMember` m inner join (select name,membership_type,max(to_date) as max_membership_date,member @@ -31,4 +50,6 @@ def get_data(filters): group by member order by max_membership_date asc) ms on m.name = ms.member - where month(max_membership_date) = %(month)s and year(max_membership_date) = %(year)s """,{'month': filters.get('month'),'year':filters.get('fiscal_year')}) + where month(max_membership_date) = %(month)s and year(max_membership_date) = %(year)s """, + {"month": filters.get("month"), "year": filters.get("fiscal_year")}, + ) diff --git a/erpnext/non_profit/web_form/certification_application/certification_application.py b/erpnext/non_profit/web_form/certification_application/certification_application.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/non_profit/web_form/certification_application/certification_application.py +++ b/erpnext/non_profit/web_form/certification_application/certification_application.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py +++ b/erpnext/non_profit/web_form/certification_application_usd/certification_application_usd.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/non_profit/web_form/grant_application/grant_application.py b/erpnext/non_profit/web_form/grant_application/grant_application.py index 1f828940922..e14a613cd2a 100644 --- a/erpnext/non_profit/web_form/grant_application/grant_application.py +++ b/erpnext/non_profit/web_form/grant_application/grant_application.py @@ -1,6 +1,3 @@ - - def get_context(context): context.no_cache = True - context.parents = [dict(label='View All ', - route='grant-application', title='View All')] + context.parents = [dict(label="View All ", route="grant-application", title="View All")] diff --git a/erpnext/patches/v10_0/add_default_cash_flow_mappers.py b/erpnext/patches/v10_0/add_default_cash_flow_mappers.py index 165ca0243bf..5493258e3de 100644 --- a/erpnext/patches/v10_0/add_default_cash_flow_mappers.py +++ b/erpnext/patches/v10_0/add_default_cash_flow_mappers.py @@ -8,8 +8,8 @@ from erpnext.setup.install import create_default_cash_flow_mapper_templates def execute(): - frappe.reload_doc('accounts', 'doctype', frappe.scrub('Cash Flow Mapping')) - frappe.reload_doc('accounts', 'doctype', frappe.scrub('Cash Flow Mapper')) - frappe.reload_doc('accounts', 'doctype', frappe.scrub('Cash Flow Mapping Template Details')) + frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapping")) + frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapper")) + frappe.reload_doc("accounts", "doctype", frappe.scrub("Cash Flow Mapping Template Details")) - create_default_cash_flow_mapper_templates() + create_default_cash_flow_mapper_templates() diff --git a/erpnext/patches/v10_0/delete_hub_documents.py b/erpnext/patches/v10_0/delete_hub_documents.py index 706d1d2b68a..986300ceffa 100644 --- a/erpnext/patches/v10_0/delete_hub_documents.py +++ b/erpnext/patches/v10_0/delete_hub_documents.py @@ -1,4 +1,3 @@ - import frappe @@ -7,7 +6,7 @@ def execute(): frappe.delete_doc(dt, dn, ignore_missing=True) if frappe.db.exists("DocType", "Data Migration Plan"): - data_migration_plans = frappe.get_all("Data Migration Plan", filters={"module": 'Hub Node'}) + data_migration_plans = frappe.get_all("Data Migration Plan", filters={"module": "Hub Node"}) for plan in data_migration_plans: plan_doc = frappe.get_doc("Data Migration Plan", plan.name) for m in plan_doc.get("mappings"): diff --git a/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py b/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py index cdf5ba29141..44497299c47 100644 --- a/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py +++ b/erpnext/patches/v10_0/fichier_des_ecritures_comptables_for_france.py @@ -8,6 +8,6 @@ from erpnext.setup.doctype.company.company import install_country_fixtures def execute(): - frappe.reload_doc('regional', 'report', 'fichier_des_ecritures_comptables_[fec]') - for d in frappe.get_all('Company', filters = {'country': 'France'}): + frappe.reload_doc("regional", "report", "fichier_des_ecritures_comptables_[fec]") + for d in frappe.get_all("Company", filters={"country": "France"}): install_country_fixtures(d.name) diff --git a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py index ffff95d223c..e2d0943d724 100644 --- a/erpnext/patches/v10_0/item_barcode_childtable_migrate.py +++ b/erpnext/patches/v10_0/item_barcode_childtable_migrate.py @@ -7,26 +7,30 @@ import frappe def execute(): frappe.reload_doc("stock", "doctype", "item_barcode") - if frappe.get_all("Item Barcode", limit=1): return - if "barcode" not in frappe.db.get_table_columns("Item"): return + if frappe.get_all("Item Barcode", limit=1): + return + if "barcode" not in frappe.db.get_table_columns("Item"): + return - items_barcode = frappe.db.sql("select name, barcode from tabItem where barcode is not null", as_dict=True) + items_barcode = frappe.db.sql( + "select name, barcode from tabItem where barcode is not null", as_dict=True + ) frappe.reload_doc("stock", "doctype", "item") - - for item in items_barcode: barcode = item.barcode.strip() - if barcode and '<' not in barcode: + if barcode and "<" not in barcode: try: - frappe.get_doc({ - 'idx': 0, - 'doctype': 'Item Barcode', - 'barcode': barcode, - 'parenttype': 'Item', - 'parent': item.name, - 'parentfield': 'barcodes' - }).insert() + frappe.get_doc( + { + "idx": 0, + "doctype": "Item Barcode", + "barcode": barcode, + "parenttype": "Item", + "parent": item.name, + "parentfield": "barcodes", + } + ).insert() except (frappe.DuplicateEntryError, frappe.UniqueValidationError): continue diff --git a/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py b/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py index fd511849b2b..2cbbe055f67 100644 --- a/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py +++ b/erpnext/patches/v10_0/migrate_daily_work_summary_settings_to_daily_work_summary_group.py @@ -6,13 +6,13 @@ import frappe def execute(): - if not frappe.db.table_exists('Daily Work Summary Group'): + if not frappe.db.table_exists("Daily Work Summary Group"): frappe.reload_doc("hr", "doctype", "daily_work_summary_group") frappe.reload_doc("hr", "doctype", "daily_work_summary_group_user") # check if Daily Work Summary Settings Company table exists try: - frappe.db.sql('DESC `tabDaily Work Summary Settings Company`') + frappe.db.sql("DESC `tabDaily Work Summary Settings Company`") except Exception: return @@ -20,19 +20,24 @@ def execute(): previous_setting = get_previous_setting() if previous_setting["companies"]: for d in previous_setting["companies"]: - users = frappe.get_list("Employee", dict( - company=d.company, user_id=("!=", " ")), "user_id as user") - if(len(users)): + users = frappe.get_list( + "Employee", dict(company=d.company, user_id=("!=", " ")), "user_id as user" + ) + if len(users): # create new group entry for each company entry - new_group = frappe.get_doc(dict(doctype="Daily Work Summary Group", - name="Daily Work Summary for " + d.company, - users=users, - send_emails_at=d.send_emails_at, - subject=previous_setting["subject"], - message=previous_setting["message"])) + new_group = frappe.get_doc( + dict( + doctype="Daily Work Summary Group", + name="Daily Work Summary for " + d.company, + users=users, + send_emails_at=d.send_emails_at, + subject=previous_setting["subject"], + message=previous_setting["message"], + ) + ) new_group.flags.ignore_permissions = True new_group.flags.ignore_validate = True - new_group.insert(ignore_if_duplicate = True) + new_group.insert(ignore_if_duplicate=True) frappe.delete_doc("DocType", "Daily Work Summary Settings") frappe.delete_doc("DocType", "Daily Work Summary Settings Company") @@ -41,11 +46,13 @@ def execute(): def get_previous_setting(): obj = {} setting_data = frappe.db.sql( - "select field, value from tabSingles where doctype='Daily Work Summary Settings'") + "select field, value from tabSingles where doctype='Daily Work Summary Settings'" + ) for field, value in setting_data: obj[field] = value obj["companies"] = get_setting_companies() return obj + def get_setting_companies(): return frappe.db.sql("select * from `tabDaily Work Summary Settings Company`", as_dict=True) diff --git a/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py b/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py index 00d0dd75e4c..a2deab62258 100644 --- a/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py +++ b/erpnext/patches/v10_0/rename_offer_letter_to_job_offer.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py b/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py index 152c5b3ec48..1d5518f0728 100644 --- a/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py +++ b/erpnext/patches/v10_0/rename_price_to_rate_in_pricing_rule.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -11,5 +10,5 @@ def execute(): rename_field("Pricing Rule", "price", "rate") except Exception as e: - if e.args[0]!=1054: + if e.args[0] != 1054: raise diff --git a/erpnext/patches/v10_0/set_currency_in_pricing_rule.py b/erpnext/patches/v10_0/set_currency_in_pricing_rule.py index 374df2a4dc7..d68148eec1c 100644 --- a/erpnext/patches/v10_0/set_currency_in_pricing_rule.py +++ b/erpnext/patches/v10_0/set_currency_in_pricing_rule.py @@ -1,4 +1,3 @@ - import frappe @@ -6,8 +5,10 @@ def execute(): frappe.reload_doctype("Pricing Rule") currency = frappe.db.get_default("currency") - for doc in frappe.get_all('Pricing Rule', fields = ["company", "name"]): + for doc in frappe.get_all("Pricing Rule", fields=["company", "name"]): if doc.company: - currency = frappe.get_cached_value('Company', doc.company, "default_currency") + currency = frappe.get_cached_value("Company", doc.company, "default_currency") - frappe.db.sql("""update `tabPricing Rule` set currency = %s where name = %s""",(currency, doc.name)) + frappe.db.sql( + """update `tabPricing Rule` set currency = %s where name = %s""", (currency, doc.name) + ) diff --git a/erpnext/patches/v10_0/update_translatable_fields.py b/erpnext/patches/v10_0/update_translatable_fields.py index f111ac7b9de..481ff93984a 100644 --- a/erpnext/patches/v10_0/update_translatable_fields.py +++ b/erpnext/patches/v10_0/update_translatable_fields.py @@ -1,41 +1,42 @@ -#-*- coding: utf-8 -*- +# -*- coding: utf-8 -*- import frappe def execute(): - ''' + """ Enable translatable in these fields - Customer Name - Supplier Name - Contact Name - Item Name/ Description - Address - ''' + """ - frappe.reload_doc('core', 'doctype', 'docfield') - frappe.reload_doc('custom', 'doctype', 'custom_field') + frappe.reload_doc("core", "doctype", "docfield") + frappe.reload_doc("custom", "doctype", "custom_field") enable_for_fields = [ - ['Customer', 'customer_name'], - ['Supplier', 'supplier_name'], - ['Contact', 'first_name'], - ['Contact', 'last_name'], - ['Item', 'item_name'], - ['Item', 'description'], - ['Address', 'address_line1'], - ['Address', 'address_line2'], + ["Customer", "customer_name"], + ["Supplier", "supplier_name"], + ["Contact", "first_name"], + ["Contact", "last_name"], + ["Item", "item_name"], + ["Item", "description"], + ["Address", "address_line1"], + ["Address", "address_line2"], ] - for f in enable_for_fields: - frappe.get_doc({ - 'doctype': 'Property Setter', - 'doc_type': f[0], - 'doctype_or_field': 'DocField', - 'field_name': f[1], - 'property': 'translatable', - 'propery_type': 'Check', - 'value': 1 - }).db_insert() + frappe.get_doc( + { + "doctype": "Property Setter", + "doc_type": f[0], + "doctype_or_field": "DocField", + "field_name": f[1], + "property": "translatable", + "propery_type": "Check", + "value": 1, + } + ).db_insert() diff --git a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py index dcb4a57ba2a..87151c102b1 100644 --- a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py +++ b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py @@ -1,44 +1,59 @@ - import frappe from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('automation', 'doctype', 'auto_repeat') + frappe.reload_doc("automation", "doctype", "auto_repeat") doctypes_to_rename = { - 'accounts': ['Journal Entry', 'Payment Entry', 'Purchase Invoice', 'Sales Invoice'], - 'buying': ['Purchase Order', 'Supplier Quotation'], - 'selling': ['Quotation', 'Sales Order'], - 'stock': ['Delivery Note', 'Purchase Receipt'] + "accounts": ["Journal Entry", "Payment Entry", "Purchase Invoice", "Sales Invoice"], + "buying": ["Purchase Order", "Supplier Quotation"], + "selling": ["Quotation", "Sales Order"], + "stock": ["Delivery Note", "Purchase Receipt"], } for module, doctypes in doctypes_to_rename.items(): for doctype in doctypes: - frappe.reload_doc(module, 'doctype', frappe.scrub(doctype)) + frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) - if frappe.db.has_column(doctype, 'subscription'): - rename_field(doctype, 'subscription', 'auto_repeat') + if frappe.db.has_column(doctype, "subscription"): + rename_field(doctype, "subscription", "auto_repeat") - subscriptions = frappe.db.sql('select * from `tabSubscription`', as_dict=1) + subscriptions = frappe.db.sql("select * from `tabSubscription`", as_dict=1) for doc in subscriptions: - doc['doctype'] = 'Auto Repeat' + doc["doctype"] = "Auto Repeat" auto_repeat = frappe.get_doc(doc) auto_repeat.db_insert() - frappe.db.sql('delete from `tabSubscription`') + frappe.db.sql("delete from `tabSubscription`") frappe.db.commit() drop_columns_from_subscription() + def drop_columns_from_subscription(): - fields_to_drop = {'Subscription': []} - for field in ['naming_series', 'reference_doctype', 'reference_document', 'start_date', - 'end_date', 'submit_on_creation', 'disabled', 'frequency', 'repeat_on_day', - 'next_schedule_date', 'notify_by_email', 'subject', 'recipients', 'print_format', - 'message', 'status', 'amended_from']: + fields_to_drop = {"Subscription": []} + for field in [ + "naming_series", + "reference_doctype", + "reference_document", + "start_date", + "end_date", + "submit_on_creation", + "disabled", + "frequency", + "repeat_on_day", + "next_schedule_date", + "notify_by_email", + "subject", + "recipients", + "print_format", + "message", + "status", + "amended_from", + ]: if field in frappe.db.get_table_columns("Subscription"): - fields_to_drop['Subscription'].append(field) + fields_to_drop["Subscription"].append(field) frappe.model.delete_fields(fields_to_drop, delete=1) diff --git a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py index 01836ca6a45..48ca9b9f5b2 100644 --- a/erpnext/patches/v11_0/add_default_dispatch_notification_template.py +++ b/erpnext/patches/v11_0/add_default_dispatch_notification_template.py @@ -1,4 +1,3 @@ - import os import frappe @@ -11,15 +10,19 @@ def execute(): if not frappe.db.exists("Email Template", _("Dispatch Notification")): base_path = frappe.get_app_path("erpnext", "stock", "doctype") - response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "delivery_trip/dispatch_notification_template.html") + ) - frappe.get_doc({ - "doctype": "Email Template", - "name": _("Dispatch Notification"), - "response": response, - "subject": _("Your order is out for delivery!"), - "owner": frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Dispatch Notification"), + "response": response, + "subject": _("Your order is out for delivery!"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) delivery_settings = frappe.get_doc("Delivery Settings") delivery_settings.dispatch_template = _("Dispatch Notification") diff --git a/erpnext/patches/v11_0/add_default_email_template_for_leave.py b/erpnext/patches/v11_0/add_default_email_template_for_leave.py index e52d12429b5..1fddc7f11ef 100644 --- a/erpnext/patches/v11_0/add_default_email_template_for_leave.py +++ b/erpnext/patches/v11_0/add_default_email_template_for_leave.py @@ -1,4 +1,3 @@ - import os import frappe @@ -8,25 +7,32 @@ from frappe import _ def execute(): frappe.reload_doc("email", "doctype", "email_template") - if not frappe.db.exists("Email Template", _('Leave Approval Notification')): + if not frappe.db.exists("Email Template", _("Leave Approval Notification")): base_path = frappe.get_app_path("erpnext", "hr", "doctype") - response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _("Leave Approval Notification"), - 'response': response, - 'subject': _("Leave Approval Notification"), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + response = frappe.read_file( + os.path.join(base_path, "leave_application/leave_application_email_template.html") + ) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Leave Approval Notification"), + "response": response, + "subject": _("Leave Approval Notification"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - - if not frappe.db.exists("Email Template", _('Leave Status Notification')): + if not frappe.db.exists("Email Template", _("Leave Status Notification")): base_path = frappe.get_app_path("erpnext", "hr", "doctype") - response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _("Leave Status Notification"), - 'response': response, - 'subject': _("Leave Status Notification"), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + response = frappe.read_file( + os.path.join(base_path, "leave_application/leave_application_email_template.html") + ) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Leave Status Notification"), + "response": response, + "subject": _("Leave Status Notification"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v11_0/add_expense_claim_default_account.py b/erpnext/patches/v11_0/add_expense_claim_default_account.py index 8629798ba82..ff393502d7d 100644 --- a/erpnext/patches/v11_0/add_expense_claim_default_account.py +++ b/erpnext/patches/v11_0/add_expense_claim_default_account.py @@ -1,4 +1,3 @@ - import frappe @@ -9,4 +8,9 @@ def execute(): for company in companies: if company.default_payable_account is not None: - frappe.db.set_value("Company", company.name, "default_expense_claim_payable_account", company.default_payable_account) + frappe.db.set_value( + "Company", + company.name, + "default_expense_claim_payable_account", + company.default_payable_account, + ) diff --git a/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py b/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py index 6091216a009..c5405d7f1e8 100644 --- a/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py +++ b/erpnext/patches/v11_0/add_healthcare_service_unit_tree_root.py @@ -1,10 +1,9 @@ - import frappe from frappe import _ def execute(): - """ assign lft and rgt appropriately """ + """assign lft and rgt appropriately""" if "Healthcare" not in frappe.get_active_domains(): return @@ -13,9 +12,11 @@ def execute(): company = frappe.get_value("Company", {"domain": "Healthcare"}, "name") if company: - frappe.get_doc({ - 'doctype': 'Healthcare Service Unit', - 'healthcare_service_unit_name': _('All Healthcare Service Units'), - 'is_group': 1, - 'company': company - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Healthcare Service Unit", + "healthcare_service_unit_name": _("All Healthcare Service Units"), + "is_group": 1, + "company": company, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py b/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py index 7c99f580f7c..f354616fe7f 100644 --- a/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py +++ b/erpnext/patches/v11_0/add_index_on_nestedset_doctypes.py @@ -7,6 +7,16 @@ import frappe def execute(): frappe.reload_doc("assets", "doctype", "Location") - for dt in ("Account", "Cost Center", "File", "Employee", "Location", "Task", "Customer Group", "Sales Person", "Territory"): + for dt in ( + "Account", + "Cost Center", + "File", + "Employee", + "Location", + "Task", + "Customer Group", + "Sales Person", + "Territory", + ): frappe.reload_doctype(dt) frappe.get_doc("DocType", dt).run_module_method("on_doctype_update") diff --git a/erpnext/patches/v11_0/add_item_group_defaults.py b/erpnext/patches/v11_0/add_item_group_defaults.py index 026047a9612..4e6505a3565 100644 --- a/erpnext/patches/v11_0/add_item_group_defaults.py +++ b/erpnext/patches/v11_0/add_item_group_defaults.py @@ -6,31 +6,36 @@ import frappe def execute(): - ''' + """ Fields to move from item group to item defaults child table [ default_cost_center, default_expense_account, default_income_account ] - ''' + """ - frappe.reload_doc('stock', 'doctype', 'item_default') - frappe.reload_doc('setup', 'doctype', 'item_group') + frappe.reload_doc("stock", "doctype", "item_default") + frappe.reload_doc("setup", "doctype", "item_group") companies = frappe.get_all("Company") - item_groups = frappe.db.sql("""select name, default_income_account, default_expense_account,\ - default_cost_center from `tabItem Group`""", as_dict=True) + item_groups = frappe.db.sql( + """select name, default_income_account, default_expense_account,\ + default_cost_center from `tabItem Group`""", + as_dict=True, + ) if len(companies) == 1: for item_group in item_groups: doc = frappe.get_doc("Item Group", item_group.get("name")) item_group_defaults = [] - item_group_defaults.append({ - "company": companies[0].name, - "income_account": item_group.get("default_income_account"), - "expense_account": item_group.get("default_expense_account"), - "buying_cost_center": item_group.get("default_cost_center"), - "selling_cost_center": item_group.get("default_cost_center") - }) + item_group_defaults.append( + { + "company": companies[0].name, + "income_account": item_group.get("default_income_account"), + "expense_account": item_group.get("default_expense_account"), + "buying_cost_center": item_group.get("default_cost_center"), + "selling_cost_center": item_group.get("default_cost_center"), + } + ) doc.extend("item_group_defaults", item_group_defaults) for child_doc in doc.item_group_defaults: child_doc.db_insert() @@ -38,10 +43,11 @@ def execute(): item_group_dict = { "default_expense_account": ["expense_account"], "default_income_account": ["income_account"], - "default_cost_center": ["buying_cost_center", "selling_cost_center"] + "default_cost_center": ["buying_cost_center", "selling_cost_center"], } for item_group in item_groups: item_group_defaults = [] + def insert_into_item_defaults(doc_field_name, doc_field_value, company): for d in item_group_defaults: if d.get("company") == company: @@ -50,18 +56,16 @@ def execute(): d[doc_field_name[1]] = doc_field_value return - item_group_defaults.append({ - "company": company, - doc_field_name[0]: doc_field_value - }) + item_group_defaults.append({"company": company, doc_field_name[0]: doc_field_value}) - if(len(doc_field_name) > 1): - item_group_defaults[len(item_group_defaults)-1][doc_field_name[1]] = doc_field_value + if len(doc_field_name) > 1: + item_group_defaults[len(item_group_defaults) - 1][doc_field_name[1]] = doc_field_value for d in [ - ["default_expense_account", "Account"], ["default_income_account", "Account"], - ["default_cost_center", "Cost Center"] - ]: + ["default_expense_account", "Account"], + ["default_income_account", "Account"], + ["default_cost_center", "Cost Center"], + ]: if item_group.get(d[0]): company = frappe.get_value(d[1], item_group.get(d[0]), "company", cache=True) doc_field_name = item_group_dict.get(d[0]) diff --git a/erpnext/patches/v11_0/add_market_segments.py b/erpnext/patches/v11_0/add_market_segments.py index 820199569ab..d1111c21e07 100644 --- a/erpnext/patches/v11_0/add_market_segments.py +++ b/erpnext/patches/v11_0/add_market_segments.py @@ -1,12 +1,11 @@ - import frappe from erpnext.setup.setup_wizard.operations.install_fixtures import add_market_segments def execute(): - frappe.reload_doc('crm', 'doctype', 'market_segment') + frappe.reload_doc("crm", "doctype", "market_segment") - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" add_market_segments() diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py index 9df1b586e30..f3429ef1c91 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -4,7 +4,7 @@ from erpnext.regional.india.setup import add_permissions def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return diff --git a/erpnext/patches/v11_0/add_sales_stages.py b/erpnext/patches/v11_0/add_sales_stages.py index 1699572551b..0dac1e10ed2 100644 --- a/erpnext/patches/v11_0/add_sales_stages.py +++ b/erpnext/patches/v11_0/add_sales_stages.py @@ -1,12 +1,11 @@ - import frappe from erpnext.setup.setup_wizard.operations.install_fixtures import add_sale_stages def execute(): - frappe.reload_doc('crm', 'doctype', 'sales_stage') + frappe.reload_doc("crm", "doctype", "sales_stage") - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" add_sale_stages() diff --git a/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py b/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py index 039238f9e1b..d9d7981965b 100644 --- a/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py +++ b/erpnext/patches/v11_0/check_buying_selling_in_currency_exchange.py @@ -1,7 +1,6 @@ - import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'currency_exchange') + frappe.reload_doc("setup", "doctype", "currency_exchange") frappe.db.sql("""update `tabCurrency Exchange` set for_buying = 1, for_selling = 1""") diff --git a/erpnext/patches/v11_0/create_default_success_action.py b/erpnext/patches/v11_0/create_default_success_action.py index b45065cb0d2..e7b412cc5f2 100644 --- a/erpnext/patches/v11_0/create_default_success_action.py +++ b/erpnext/patches/v11_0/create_default_success_action.py @@ -1,4 +1,3 @@ - import frappe from erpnext.setup.install import create_default_success_action diff --git a/erpnext/patches/v11_0/create_department_records_for_each_company.py b/erpnext/patches/v11_0/create_department_records_for_each_company.py index a4cba0cfcca..84be2bee9dc 100644 --- a/erpnext/patches/v11_0/create_department_records_for_each_company.py +++ b/erpnext/patches/v11_0/create_department_records_for_each_company.py @@ -1,15 +1,14 @@ - import frappe from frappe import _ from frappe.utils.nestedset import rebuild_tree def execute(): - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" - for doctype in ['department', 'leave_period', 'staffing_plan', 'job_opening']: + for doctype in ["department", "leave_period", "staffing_plan", "job_opening"]: frappe.reload_doc("hr", "doctype", doctype) - frappe.reload_doc("Payroll", "doctype", 'payroll_entry') + frappe.reload_doc("Payroll", "doctype", "payroll_entry") companies = frappe.db.get_all("Company", fields=["name", "abbr"]) departments = frappe.db.get_all("Department") @@ -36,7 +35,7 @@ def execute(): # append list of new department for each company comp_dict[company.name][department.name] = copy_doc.name - rebuild_tree('Department', 'parent_department') + rebuild_tree("Department", "parent_department") doctypes = ["Asset", "Employee", "Payroll Entry", "Staffing Plan", "Job Opening"] for d in doctypes: @@ -44,7 +43,8 @@ def execute(): update_instructors(comp_dict) - frappe.local.lang = 'en' + frappe.local.lang = "en" + def update_records(doctype, comp_dict): when_then = [] @@ -52,20 +52,27 @@ def update_records(doctype, comp_dict): records = comp_dict[company] for department in records: - when_then.append(''' + when_then.append( + """ WHEN company = "%s" and department = "%s" THEN "%s" - '''%(company, department, records[department])) + """ + % (company, department, records[department]) + ) if not when_then: return - frappe.db.sql(""" + frappe.db.sql( + """ update `tab%s` set department = CASE %s END - """%(doctype, " ".join(when_then))) + """ + % (doctype, " ".join(when_then)) + ) + def update_instructors(comp_dict): when_then = [] @@ -75,17 +82,23 @@ def update_instructors(comp_dict): records = comp_dict[employee.company] if employee.company else [] for department in records: - when_then.append(''' + when_then.append( + """ WHEN employee = "%s" and department = "%s" THEN "%s" - '''%(employee.name, department, records[department])) + """ + % (employee.name, department, records[department]) + ) if not when_then: return - frappe.db.sql(""" + frappe.db.sql( + """ update `tabInstructor` set department = CASE %s END - """%(" ".join(when_then))) + """ + % (" ".join(when_then)) + ) diff --git a/erpnext/patches/v11_0/create_salary_structure_assignments.py b/erpnext/patches/v11_0/create_salary_structure_assignments.py index 823eca19b07..b81e867b9dd 100644 --- a/erpnext/patches/v11_0/create_salary_structure_assignments.py +++ b/erpnext/patches/v11_0/create_salary_structure_assignments.py @@ -13,48 +13,62 @@ from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assign def execute(): - frappe.reload_doc('Payroll', 'doctype', 'Salary Structure') + frappe.reload_doc("Payroll", "doctype", "Salary Structure") frappe.reload_doc("Payroll", "doctype", "Salary Structure Assignment") - frappe.db.sql(""" + frappe.db.sql( + """ delete from `tabSalary Structure Assignment` where salary_structure in (select name from `tabSalary Structure` where is_active='No' or docstatus!=1) - """) - if frappe.db.table_exists('Salary Structure Employee'): - ss_details = frappe.db.sql(""" + """ + ) + if frappe.db.table_exists("Salary Structure Employee"): + ss_details = frappe.db.sql( + """ select sse.employee, sse.employee_name, sse.from_date, sse.to_date, sse.base, sse.variable, sse.parent as salary_structure, ss.company from `tabSalary Structure Employee` sse, `tabSalary Structure` ss where ss.name = sse.parent AND ss.is_active='Yes' - AND sse.employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left')""", as_dict=1) + AND sse.employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left')""", + as_dict=1, + ) else: cols = "" if "base" in frappe.db.get_table_columns("Salary Structure"): cols = ", base, variable" - ss_details = frappe.db.sql(""" + ss_details = frappe.db.sql( + """ select name as salary_structure, employee, employee_name, from_date, to_date, company {0} from `tabSalary Structure` where is_active='Yes' AND employee in (select name from `tabEmployee` where ifNull(status, '') != 'Left') - """.format(cols), as_dict=1) + """.format( + cols + ), + as_dict=1, + ) all_companies = frappe.db.get_all("Company", fields=["name", "default_currency"]) for d in all_companies: company = d.name company_currency = d.default_currency - frappe.db.sql("""update `tabSalary Structure` set currency = %s where company=%s""", (company_currency, company)) + frappe.db.sql( + """update `tabSalary Structure` set currency = %s where company=%s""", + (company_currency, company), + ) for d in ss_details: try: - joining_date, relieving_date = frappe.db.get_value("Employee", d.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.db.get_value( + "Employee", d.employee, ["date_of_joining", "relieving_date"] + ) from_date = d.from_date if joining_date and getdate(from_date) < joining_date: from_date = joining_date elif relieving_date and getdate(from_date) > relieving_date: continue - company_currency = frappe.db.get_value('Company', d.company, 'default_currency') + company_currency = frappe.db.get_value("Company", d.company, "default_currency") s = frappe.new_doc("Salary Structure Assignment") s.employee = d.employee diff --git a/erpnext/patches/v11_0/drop_column_max_days_allowed.py b/erpnext/patches/v11_0/drop_column_max_days_allowed.py index 5c549258584..4b4770d809c 100644 --- a/erpnext/patches/v11_0/drop_column_max_days_allowed.py +++ b/erpnext/patches/v11_0/drop_column_max_days_allowed.py @@ -1,8 +1,7 @@ - import frappe def execute(): if frappe.db.exists("DocType", "Leave Type"): - if 'max_days_allowed' in frappe.db.get_table_columns("Leave Type"): + if "max_days_allowed" in frappe.db.get_table_columns("Leave Type"): frappe.db.sql("alter table `tabLeave Type` drop column max_days_allowed") diff --git a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py b/erpnext/patches/v11_0/ewaybill_fields_gst_india.py index a7e662c78c5..7a06d522426 100644 --- a/erpnext/patches/v11_0/ewaybill_fields_gst_india.py +++ b/erpnext/patches/v11_0/ewaybill_fields_gst_india.py @@ -1,12 +1,11 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company: - return + company = frappe.get_all("Company", filters={"country": "India"}) + if not company: + return - make_custom_fields() + make_custom_fields() diff --git a/erpnext/patches/v11_0/hr_ux_cleanups.py b/erpnext/patches/v11_0/hr_ux_cleanups.py index 00678c781e7..0749bfc0b9d 100644 --- a/erpnext/patches/v11_0/hr_ux_cleanups.py +++ b/erpnext/patches/v11_0/hr_ux_cleanups.py @@ -1,13 +1,12 @@ - import frappe def execute(): - frappe.reload_doctype('Employee') - frappe.db.sql('update tabEmployee set first_name = employee_name') + frappe.reload_doctype("Employee") + frappe.db.sql("update tabEmployee set first_name = employee_name") # update holiday list - frappe.reload_doctype('Holiday List') - for holiday_list in frappe.get_all('Holiday List'): - holiday_list = frappe.get_doc('Holiday List', holiday_list.name) - holiday_list.db_set('total_holidays', len(holiday_list.holidays), update_modified = False) + frappe.reload_doctype("Holiday List") + for holiday_list in frappe.get_all("Holiday List"): + holiday_list = frappe.get_doc("Holiday List", holiday_list.name) + holiday_list.db_set("total_holidays", len(holiday_list.holidays), update_modified=False) diff --git a/erpnext/patches/v11_0/inter_state_field_for_gst.py b/erpnext/patches/v11_0/inter_state_field_for_gst.py index d897941eab2..b8510297c28 100644 --- a/erpnext/patches/v11_0/inter_state_field_for_gst.py +++ b/erpnext/patches/v11_0/inter_state_field_for_gst.py @@ -1,11 +1,10 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return frappe.reload_doc("Payroll", "doctype", "Employee Tax Exemption Declaration") @@ -29,38 +28,64 @@ def execute(): frappe.reload_doc("accounts", "doctype", "purchase_taxes_and_charges_template") # set is_inter_state in Taxes And Charges Templates - if frappe.db.has_column("Sales Taxes and Charges Template", "is_inter_state") and\ - frappe.db.has_column("Purchase Taxes and Charges Template", "is_inter_state"): + if frappe.db.has_column( + "Sales Taxes and Charges Template", "is_inter_state" + ) and frappe.db.has_column("Purchase Taxes and Charges Template", "is_inter_state"): - igst_accounts = set(frappe.db.sql_list('''SELECT igst_account from `tabGST Account` WHERE parent = "GST Settings"''')) - cgst_accounts = set(frappe.db.sql_list('''SELECT cgst_account FROM `tabGST Account` WHERE parenttype = "GST Settings"''')) + igst_accounts = set( + frappe.db.sql_list( + '''SELECT igst_account from `tabGST Account` WHERE parent = "GST Settings"''' + ) + ) + cgst_accounts = set( + frappe.db.sql_list( + '''SELECT cgst_account FROM `tabGST Account` WHERE parenttype = "GST Settings"''' + ) + ) when_then_sales = get_formatted_data("Sales Taxes and Charges", igst_accounts, cgst_accounts) - when_then_purchase = get_formatted_data("Purchase Taxes and Charges", igst_accounts, cgst_accounts) + when_then_purchase = get_formatted_data( + "Purchase Taxes and Charges", igst_accounts, cgst_accounts + ) if when_then_sales: - frappe.db.sql('''update `tabSales Taxes and Charges Template` + frappe.db.sql( + """update `tabSales Taxes and Charges Template` set is_inter_state = Case {when_then} Else 0 End - '''.format(when_then=" ".join(when_then_sales))) + """.format( + when_then=" ".join(when_then_sales) + ) + ) if when_then_purchase: - frappe.db.sql('''update `tabPurchase Taxes and Charges Template` + frappe.db.sql( + """update `tabPurchase Taxes and Charges Template` set is_inter_state = Case {when_then} Else 0 End - '''.format(when_then=" ".join(when_then_purchase))) + """.format( + when_then=" ".join(when_then_purchase) + ) + ) + def get_formatted_data(doctype, igst_accounts, cgst_accounts): # fetch all the rows data from child table - all_details = frappe.db.sql(''' + all_details = frappe.db.sql( + ''' select parent, account_head from `tab{doctype}` - where parenttype="{doctype} Template"'''.format(doctype=doctype), as_dict=True) + where parenttype="{doctype} Template"'''.format( + doctype=doctype + ), + as_dict=True, + ) # group the data in the form "parent: [list of accounts]"" group_detail = {} for i in all_details: - if not i['parent'] in group_detail: group_detail[i['parent']] = [] + if not i["parent"] in group_detail: + group_detail[i["parent"]] = [] for j in all_details: - if i['parent']==j['parent']: - group_detail[i['parent']].append(j['account_head']) + if i["parent"] == j["parent"]: + group_detail[i["parent"]].append(j["account_head"]) # form when_then condition based on - if list of accounts for a document # matches any account in igst_accounts list and not matches any in cgst_accounts list @@ -68,6 +93,6 @@ def get_formatted_data(doctype, igst_accounts, cgst_accounts): for i in group_detail: temp = set(group_detail[i]) if not temp.isdisjoint(igst_accounts) and temp.isdisjoint(cgst_accounts): - when_then.append('''When name='{name}' Then 1'''.format(name=i)) + when_then.append("""When name='{name}' Then 1""".format(name=i)) return when_then diff --git a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py index cd3869b3600..213145653d3 100644 --- a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py +++ b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py @@ -6,40 +6,50 @@ import frappe def execute(): - frappe.reload_doc('assets', 'doctype', 'asset_finance_book') - frappe.reload_doc('assets', 'doctype', 'depreciation_schedule') - frappe.reload_doc('assets', 'doctype', 'asset_category') - frappe.reload_doc('assets', 'doctype', 'asset') - frappe.reload_doc('assets', 'doctype', 'asset_movement') - frappe.reload_doc('assets', 'doctype', 'asset_category_account') + frappe.reload_doc("assets", "doctype", "asset_finance_book") + frappe.reload_doc("assets", "doctype", "depreciation_schedule") + frappe.reload_doc("assets", "doctype", "asset_category") + frappe.reload_doc("assets", "doctype", "asset") + frappe.reload_doc("assets", "doctype", "asset_movement") + frappe.reload_doc("assets", "doctype", "asset_category_account") if frappe.db.has_column("Asset", "warehouse"): - frappe.db.sql(""" update `tabAsset` ast, `tabWarehouse` wh - set ast.location = wh.warehouse_name where ast.warehouse = wh.name""") + frappe.db.sql( + """ update `tabAsset` ast, `tabWarehouse` wh + set ast.location = wh.warehouse_name where ast.warehouse = wh.name""" + ) - for d in frappe.get_all('Asset'): - doc = frappe.get_doc('Asset', d.name) + for d in frappe.get_all("Asset"): + doc = frappe.get_doc("Asset", d.name) if doc.calculate_depreciation: - fb = doc.append('finance_books', { - 'depreciation_method': doc.depreciation_method, - 'total_number_of_depreciations': doc.total_number_of_depreciations, - 'frequency_of_depreciation': doc.frequency_of_depreciation, - 'depreciation_start_date': doc.next_depreciation_date, - 'expected_value_after_useful_life': doc.expected_value_after_useful_life, - 'value_after_depreciation': doc.value_after_depreciation - }) + fb = doc.append( + "finance_books", + { + "depreciation_method": doc.depreciation_method, + "total_number_of_depreciations": doc.total_number_of_depreciations, + "frequency_of_depreciation": doc.frequency_of_depreciation, + "depreciation_start_date": doc.next_depreciation_date, + "expected_value_after_useful_life": doc.expected_value_after_useful_life, + "value_after_depreciation": doc.value_after_depreciation, + }, + ) fb.db_update() - frappe.db.sql(""" update `tabDepreciation Schedule` ds, `tabAsset` ast - set ds.depreciation_method = ast.depreciation_method, ds.finance_book_id = 1 where ds.parent = ast.name """) + frappe.db.sql( + """ update `tabDepreciation Schedule` ds, `tabAsset` ast + set ds.depreciation_method = ast.depreciation_method, ds.finance_book_id = 1 where ds.parent = ast.name """ + ) - for category in frappe.get_all('Asset Category'): + for category in frappe.get_all("Asset Category"): asset_category_doc = frappe.get_doc("Asset Category", category) - row = asset_category_doc.append('finance_books', { - 'depreciation_method': asset_category_doc.depreciation_method, - 'total_number_of_depreciations': asset_category_doc.total_number_of_depreciations, - 'frequency_of_depreciation': asset_category_doc.frequency_of_depreciation - }) + row = asset_category_doc.append( + "finance_books", + { + "depreciation_method": asset_category_doc.depreciation_method, + "total_number_of_depreciations": asset_category_doc.total_number_of_depreciations, + "frequency_of_depreciation": asset_category_doc.frequency_of_depreciation, + }, + ) row.db_update() diff --git a/erpnext/patches/v11_0/make_italian_localization_fields.py b/erpnext/patches/v11_0/make_italian_localization_fields.py index 8ff23a50d4b..1b9793df80b 100644 --- a/erpnext/patches/v11_0/make_italian_localization_fields.py +++ b/erpnext/patches/v11_0/make_italian_localization_fields.py @@ -9,11 +9,11 @@ from erpnext.regional.italy.setup import make_custom_fields, setup_report def execute(): - company = frappe.get_all('Company', filters = {'country': 'Italy'}) + company = frappe.get_all("Company", filters={"country": "Italy"}) if not company: return - frappe.reload_doc('regional', 'report', 'electronic_invoice_register') + frappe.reload_doc("regional", "report", "electronic_invoice_register") make_custom_fields() setup_report() @@ -25,15 +25,21 @@ def execute(): if condition: condition = "state_code = (case state {0} end),".format(condition) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE tabAddress set {condition} country_code = UPPER(ifnull((select code from `tabCountry` where name = `tabAddress`.country), '')) where country_code is null and state_code is null - """.format(condition=condition)) + """.format( + condition=condition + ) + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSales Invoice Item` si, `tabSales Order` so set si.customer_po_no = so.po_no, si.customer_po_date = so.po_date WHERE si.sales_order = so.name and so.po_no is not null - """) + """ + ) diff --git a/erpnext/patches/v11_0/make_job_card.py b/erpnext/patches/v11_0/make_job_card.py index 120e018805a..d4b208956b8 100644 --- a/erpnext/patches/v11_0/make_job_card.py +++ b/erpnext/patches/v11_0/make_job_card.py @@ -8,21 +8,26 @@ from erpnext.manufacturing.doctype.work_order.work_order import create_job_card def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'work_order') - frappe.reload_doc('manufacturing', 'doctype', 'work_order_item') - frappe.reload_doc('manufacturing', 'doctype', 'job_card') - frappe.reload_doc('manufacturing', 'doctype', 'job_card_item') + frappe.reload_doc("manufacturing", "doctype", "work_order") + frappe.reload_doc("manufacturing", "doctype", "work_order_item") + frappe.reload_doc("manufacturing", "doctype", "job_card") + frappe.reload_doc("manufacturing", "doctype", "job_card_item") - fieldname = frappe.db.get_value('DocField', {'fieldname': 'work_order', 'parent': 'Timesheet'}, 'fieldname') + fieldname = frappe.db.get_value( + "DocField", {"fieldname": "work_order", "parent": "Timesheet"}, "fieldname" + ) if not fieldname: - fieldname = frappe.db.get_value('DocField', {'fieldname': 'production_order', 'parent': 'Timesheet'}, 'fieldname') - if not fieldname: return + fieldname = frappe.db.get_value( + "DocField", {"fieldname": "production_order", "parent": "Timesheet"}, "fieldname" + ) + if not fieldname: + return - for d in frappe.get_all('Timesheet', - filters={fieldname: ['!=', ""], 'docstatus': 0}, - fields=[fieldname, 'name']): + for d in frappe.get_all( + "Timesheet", filters={fieldname: ["!=", ""], "docstatus": 0}, fields=[fieldname, "name"] + ): if d[fieldname]: - doc = frappe.get_doc('Work Order', d[fieldname]) + doc = frappe.get_doc("Work Order", d[fieldname]) for row in doc.operations: create_job_card(doc, row, auto_create=True) - frappe.delete_doc('Timesheet', d.name) + frappe.delete_doc("Timesheet", d.name) diff --git a/erpnext/patches/v11_0/make_location_from_warehouse.py b/erpnext/patches/v11_0/make_location_from_warehouse.py index ef6262be15e..c863bb7ecf7 100644 --- a/erpnext/patches/v11_0/make_location_from_warehouse.py +++ b/erpnext/patches/v11_0/make_location_from_warehouse.py @@ -7,14 +7,16 @@ from frappe.utils.nestedset import rebuild_tree def execute(): - if not frappe.db.get_value('Asset', {'docstatus': ('<', 2) }, 'name'): return - frappe.reload_doc('assets', 'doctype', 'location') - frappe.reload_doc('stock', 'doctype', 'warehouse') + if not frappe.db.get_value("Asset", {"docstatus": ("<", 2)}, "name"): + return + frappe.reload_doc("assets", "doctype", "location") + frappe.reload_doc("stock", "doctype", "warehouse") - for d in frappe.get_all('Warehouse', - fields = ['warehouse_name', 'is_group', 'parent_warehouse'], order_by="lft asc"): + for d in frappe.get_all( + "Warehouse", fields=["warehouse_name", "is_group", "parent_warehouse"], order_by="lft asc" + ): try: - loc = frappe.new_doc('Location') + loc = frappe.new_doc("Location") loc.location_name = d.warehouse_name loc.is_group = d.is_group loc.flags.ignore_mandatory = True @@ -27,5 +29,6 @@ def execute(): rebuild_tree("Location", "parent_location") + def get_parent_warehouse_name(warehouse): - return frappe.db.get_value('Warehouse', warehouse, 'warehouse_name') + return frappe.db.get_value("Warehouse", warehouse, "warehouse_name") diff --git a/erpnext/patches/v11_0/make_quality_inspection_template.py b/erpnext/patches/v11_0/make_quality_inspection_template.py index 58c9fb9239f..deebfa88e6e 100644 --- a/erpnext/patches/v11_0/make_quality_inspection_template.py +++ b/erpnext/patches/v11_0/make_quality_inspection_template.py @@ -6,21 +6,29 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'quality_inspection_template') - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "quality_inspection_template") + frappe.reload_doc("stock", "doctype", "item") - for data in frappe.get_all('Item Quality Inspection Parameter', - fields = ["distinct parent"], filters = {'parenttype': 'Item'}): + for data in frappe.get_all( + "Item Quality Inspection Parameter", fields=["distinct parent"], filters={"parenttype": "Item"} + ): qc_doc = frappe.new_doc("Quality Inspection Template") - qc_doc.quality_inspection_template_name = 'QIT/%s' % data.parent + qc_doc.quality_inspection_template_name = "QIT/%s" % data.parent qc_doc.flags.ignore_mandatory = True qc_doc.save(ignore_permissions=True) - frappe.db.set_value('Item', data.parent, "quality_inspection_template", qc_doc.name, update_modified=False) - frappe.db.sql(""" update `tabItem Quality Inspection Parameter` + frappe.db.set_value( + "Item", data.parent, "quality_inspection_template", qc_doc.name, update_modified=False + ) + frappe.db.sql( + """ update `tabItem Quality Inspection Parameter` set parentfield = 'item_quality_inspection_parameter', parenttype = 'Quality Inspection Template', - parent = %s where parenttype = 'Item' and parent = %s""", (qc_doc.name, data.parent)) + parent = %s where parenttype = 'Item' and parent = %s""", + (qc_doc.name, data.parent), + ) # update field in item variant settings - frappe.db.sql(""" update `tabVariant Field` set field_name = 'quality_inspection_template' - where field_name = 'quality_parameters'""") + frappe.db.sql( + """ update `tabVariant Field` set field_name = 'quality_inspection_template' + where field_name = 'quality_parameters'""" + ) diff --git a/erpnext/patches/v11_0/merge_land_unit_with_location.py b/erpnext/patches/v11_0/merge_land_unit_with_location.py index e1d0b127b9d..c1afef67785 100644 --- a/erpnext/patches/v11_0/merge_land_unit_with_location.py +++ b/erpnext/patches/v11_0/merge_land_unit_with_location.py @@ -8,51 +8,55 @@ from frappe.model.utils.rename_field import rename_field def execute(): # Rename and reload the Land Unit and Linked Land Unit doctypes - if frappe.db.table_exists('Land Unit') and not frappe.db.table_exists('Location'): - frappe.rename_doc('DocType', 'Land Unit', 'Location', force=True) + if frappe.db.table_exists("Land Unit") and not frappe.db.table_exists("Location"): + frappe.rename_doc("DocType", "Land Unit", "Location", force=True) - frappe.reload_doc('assets', 'doctype', 'location') + frappe.reload_doc("assets", "doctype", "location") - if frappe.db.table_exists('Linked Land Unit') and not frappe.db.table_exists('Linked Location'): - frappe.rename_doc('DocType', 'Linked Land Unit', 'Linked Location', force=True) + if frappe.db.table_exists("Linked Land Unit") and not frappe.db.table_exists("Linked Location"): + frappe.rename_doc("DocType", "Linked Land Unit", "Linked Location", force=True) - frappe.reload_doc('assets', 'doctype', 'linked_location') + frappe.reload_doc("assets", "doctype", "linked_location") - if not frappe.db.table_exists('Crop Cycle'): - frappe.reload_doc('agriculture', 'doctype', 'crop_cycle') + if not frappe.db.table_exists("Crop Cycle"): + frappe.reload_doc("agriculture", "doctype", "crop_cycle") # Rename the fields in related doctypes - if 'linked_land_unit' in frappe.db.get_table_columns('Crop Cycle'): - rename_field('Crop Cycle', 'linked_land_unit', 'linked_location') + if "linked_land_unit" in frappe.db.get_table_columns("Crop Cycle"): + rename_field("Crop Cycle", "linked_land_unit", "linked_location") - if 'land_unit' in frappe.db.get_table_columns('Linked Location'): - rename_field('Linked Location', 'land_unit', 'location') + if "land_unit" in frappe.db.get_table_columns("Linked Location"): + rename_field("Linked Location", "land_unit", "location") if not frappe.db.exists("Location", "All Land Units"): - frappe.get_doc({"doctype": "Location", "is_group": True, "location_name": "All Land Units"}).insert(ignore_permissions=True) + frappe.get_doc( + {"doctype": "Location", "is_group": True, "location_name": "All Land Units"} + ).insert(ignore_permissions=True) - if frappe.db.table_exists('Land Unit'): - land_units = frappe.get_all('Land Unit', fields=['*'], order_by='lft') + if frappe.db.table_exists("Land Unit"): + land_units = frappe.get_all("Land Unit", fields=["*"], order_by="lft") for land_unit in land_units: - if not frappe.db.exists('Location', land_unit.get('land_unit_name')): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': land_unit.get('land_unit_name'), - 'parent_location': land_unit.get('parent_land_unit') or "All Land Units", - 'is_container': land_unit.get('is_container'), - 'is_group': land_unit.get('is_group'), - 'latitude': land_unit.get('latitude'), - 'longitude': land_unit.get('longitude'), - 'area': land_unit.get('area'), - 'location': land_unit.get('location'), - 'lft': land_unit.get('lft'), - 'rgt': land_unit.get('rgt') - }).insert(ignore_permissions=True) + if not frappe.db.exists("Location", land_unit.get("land_unit_name")): + frappe.get_doc( + { + "doctype": "Location", + "location_name": land_unit.get("land_unit_name"), + "parent_location": land_unit.get("parent_land_unit") or "All Land Units", + "is_container": land_unit.get("is_container"), + "is_group": land_unit.get("is_group"), + "latitude": land_unit.get("latitude"), + "longitude": land_unit.get("longitude"), + "area": land_unit.get("area"), + "location": land_unit.get("location"), + "lft": land_unit.get("lft"), + "rgt": land_unit.get("rgt"), + } + ).insert(ignore_permissions=True) # Delete the Land Unit and Linked Land Unit doctypes - if frappe.db.table_exists('Land Unit'): - frappe.delete_doc('DocType', 'Land Unit', force=1) + if frappe.db.table_exists("Land Unit"): + frappe.delete_doc("DocType", "Land Unit", force=1) - if frappe.db.table_exists('Linked Land Unit'): - frappe.delete_doc('DocType', 'Linked Land Unit', force=1) + if frappe.db.table_exists("Linked Land Unit"): + frappe.delete_doc("DocType", "Linked Land Unit", force=1) diff --git a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py index bfc3fbc6084..37c07799ddc 100644 --- a/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py +++ b/erpnext/patches/v11_0/move_item_defaults_to_child_table_for_multicompany.py @@ -6,22 +6,23 @@ import frappe def execute(): - ''' + """ Fields to move from the item to item defaults child table [ default_warehouse, buying_cost_center, expense_account, selling_cost_center, income_account ] - ''' - if not frappe.db.has_column('Item', 'default_warehouse'): + """ + if not frappe.db.has_column("Item", "default_warehouse"): return - frappe.reload_doc('stock', 'doctype', 'item_default') - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item_default") + frappe.reload_doc("stock", "doctype", "item") companies = frappe.get_all("Company") if len(companies) == 1 and not frappe.get_all("Item Default", limit=1): try: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabItem Default` (name, parent, parenttype, parentfield, idx, company, default_warehouse, buying_cost_center, selling_cost_center, expense_account, income_account, default_supplier) @@ -30,22 +31,30 @@ def execute(): 'item_defaults' as parentfield, 1 as idx, %s as company, default_warehouse, buying_cost_center, selling_cost_center, expense_account, income_account, default_supplier FROM `tabItem`; - ''', companies[0].name) + """, + companies[0].name, + ) except Exception: pass else: - item_details = frappe.db.sql(""" SELECT name, default_warehouse, + item_details = frappe.db.sql( + """ SELECT name, default_warehouse, buying_cost_center, expense_account, selling_cost_center, income_account FROM tabItem WHERE - name not in (select distinct parent from `tabItem Default`) and ifnull(disabled, 0) = 0""" - , as_dict=1) + name not in (select distinct parent from `tabItem Default`) and ifnull(disabled, 0) = 0""", + as_dict=1, + ) items_default_data = {} for item_data in item_details: - for d in [["default_warehouse", "Warehouse"], ["expense_account", "Account"], - ["income_account", "Account"], ["buying_cost_center", "Cost Center"], - ["selling_cost_center", "Cost Center"]]: + for d in [ + ["default_warehouse", "Warehouse"], + ["expense_account", "Account"], + ["income_account", "Account"], + ["buying_cost_center", "Cost Center"], + ["selling_cost_center", "Cost Center"], + ]: if item_data.get(d[0]): company = frappe.get_value(d[1], item_data.get(d[0]), "company", cache=True) @@ -73,25 +82,32 @@ def execute(): for item_code, companywise_item_data in items_default_data.items(): for company, item_default_data in companywise_item_data.items(): - to_insert_data.append(( - frappe.generate_hash("", 10), - item_code, - 'Item', - 'item_defaults', - company, - item_default_data.get('default_warehouse'), - item_default_data.get('expense_account'), - item_default_data.get('income_account'), - item_default_data.get('buying_cost_center'), - item_default_data.get('selling_cost_center'), - )) + to_insert_data.append( + ( + frappe.generate_hash("", 10), + item_code, + "Item", + "item_defaults", + company, + item_default_data.get("default_warehouse"), + item_default_data.get("expense_account"), + item_default_data.get("income_account"), + item_default_data.get("buying_cost_center"), + item_default_data.get("selling_cost_center"), + ) + ) if to_insert_data: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabItem Default` ( `name`, `parent`, `parenttype`, `parentfield`, `company`, `default_warehouse`, `expense_account`, `income_account`, `buying_cost_center`, `selling_cost_center` ) VALUES {} - '''.format(', '.join(['%s'] * len(to_insert_data))), tuple(to_insert_data)) + """.format( + ", ".join(["%s"] * len(to_insert_data)) + ), + tuple(to_insert_data), + ) diff --git a/erpnext/patches/v11_0/move_leave_approvers_from_employee.py b/erpnext/patches/v11_0/move_leave_approvers_from_employee.py index fc3dbfbab92..f91a7db2a38 100644 --- a/erpnext/patches/v11_0/move_leave_approvers_from_employee.py +++ b/erpnext/patches/v11_0/move_leave_approvers_from_employee.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -8,20 +7,23 @@ def execute(): frappe.reload_doc("hr", "doctype", "employee") frappe.reload_doc("hr", "doctype", "department") - if frappe.db.has_column('Department', 'leave_approver'): - rename_field('Department', "leave_approver", "leave_approvers") + if frappe.db.has_column("Department", "leave_approver"): + rename_field("Department", "leave_approver", "leave_approvers") - if frappe.db.has_column('Department', 'expense_approver'): - rename_field('Department', "expense_approver", "expense_approvers") + if frappe.db.has_column("Department", "expense_approver"): + rename_field("Department", "expense_approver", "expense_approvers") if not frappe.db.table_exists("Employee Leave Approver"): return - approvers = frappe.db.sql("""select distinct app.leave_approver, emp.department from + approvers = frappe.db.sql( + """select distinct app.leave_approver, emp.department from `tabEmployee Leave Approver` app, `tabEmployee` emp where app.parenttype = 'Employee' and emp.name = app.parent - """, as_dict=True) + """, + as_dict=True, + ) for record in approvers: if record.department: @@ -29,6 +31,4 @@ def execute(): if not department: return if not len(department.leave_approvers): - department.append("leave_approvers",{ - "approver": record.leave_approver - }).db_insert() + department.append("leave_approvers", {"approver": record.leave_approver}).db_insert() diff --git a/erpnext/patches/v11_0/rebuild_tree_for_company.py b/erpnext/patches/v11_0/rebuild_tree_for_company.py index 7866cfab4f7..fc06c5d30d9 100644 --- a/erpnext/patches/v11_0/rebuild_tree_for_company.py +++ b/erpnext/patches/v11_0/rebuild_tree_for_company.py @@ -1,8 +1,7 @@ - import frappe from frappe.utils.nestedset import rebuild_tree def execute(): frappe.reload_doc("setup", "doctype", "company") - rebuild_tree('Company', 'parent_company') + rebuild_tree("Company", "parent_company") diff --git a/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py b/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py index 9b723cb4840..674df7933eb 100644 --- a/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py +++ b/erpnext/patches/v11_0/redesign_healthcare_billing_work_flow.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.modules import get_doctype_module, scrub @@ -10,59 +9,77 @@ sales_invoice_referenced_doc = { "Patient Encounter": "invoice", "Lab Test": "invoice", "Lab Prescription": "invoice", - "Sample Collection": "invoice" + "Sample Collection": "invoice", } + def execute(): - frappe.reload_doc('accounts', 'doctype', 'loyalty_program') - frappe.reload_doc('accounts', 'doctype', 'sales_invoice_item') + frappe.reload_doc("accounts", "doctype", "loyalty_program") + frappe.reload_doc("accounts", "doctype", "sales_invoice_item") if "Healthcare" not in frappe.get_active_domains(): return healthcare_custom_field_in_sales_invoice() for si_ref_doc in sales_invoice_referenced_doc: - if frappe.db.exists('DocType', si_ref_doc): - frappe.reload_doc(get_doctype_module(si_ref_doc), 'doctype', scrub(si_ref_doc)) + if frappe.db.exists("DocType", si_ref_doc): + frappe.reload_doc(get_doctype_module(si_ref_doc), "doctype", scrub(si_ref_doc)) - if frappe.db.has_column(si_ref_doc, sales_invoice_referenced_doc[si_ref_doc]) \ - and frappe.db.has_column(si_ref_doc, 'invoiced'): + if frappe.db.has_column( + si_ref_doc, sales_invoice_referenced_doc[si_ref_doc] + ) and frappe.db.has_column(si_ref_doc, "invoiced"): # Set Reference DocType and Reference Docname - doc_list = frappe.db.sql(""" + doc_list = frappe.db.sql( + """ select name from `tab{0}` where {1} is not null - """.format(si_ref_doc, sales_invoice_referenced_doc[si_ref_doc])) + """.format( + si_ref_doc, sales_invoice_referenced_doc[si_ref_doc] + ) + ) if doc_list: - frappe.reload_doc(get_doctype_module("Sales Invoice"), 'doctype', 'sales_invoice') + frappe.reload_doc(get_doctype_module("Sales Invoice"), "doctype", "sales_invoice") for doc_id in doc_list: - invoice_id = frappe.db.get_value(si_ref_doc, doc_id[0], sales_invoice_referenced_doc[si_ref_doc]) + invoice_id = frappe.db.get_value( + si_ref_doc, doc_id[0], sales_invoice_referenced_doc[si_ref_doc] + ) if frappe.db.exists("Sales Invoice", invoice_id): if si_ref_doc == "Lab Test": template = frappe.db.get_value("Lab Test", doc_id[0], "template") if template: item = frappe.db.get_value("Lab Test Template", template, "item") if item: - frappe.db.sql("""update `tabSales Invoice Item` set reference_dt = '{0}', - reference_dn = '{1}' where parent = '{2}' and item_code='{3}'""".format\ - (si_ref_doc, doc_id[0], invoice_id, item)) + frappe.db.sql( + """update `tabSales Invoice Item` set reference_dt = '{0}', + reference_dn = '{1}' where parent = '{2}' and item_code='{3}'""".format( + si_ref_doc, doc_id[0], invoice_id, item + ) + ) else: invoice = frappe.get_doc("Sales Invoice", invoice_id) for item_line in invoice.items: if not item_line.reference_dn: - item_line.db_set({"reference_dt":si_ref_doc, "reference_dn": doc_id[0]}) + item_line.db_set({"reference_dt": si_ref_doc, "reference_dn": doc_id[0]}) break # Documents mark invoiced for submitted sales invoice - frappe.db.sql("""update `tab{0}` doc, `tabSales Invoice` si + frappe.db.sql( + """update `tab{0}` doc, `tabSales Invoice` si set doc.invoiced = 1 where si.docstatus = 1 and doc.{1} = si.name - """.format(si_ref_doc, sales_invoice_referenced_doc[si_ref_doc])) + """.format( + si_ref_doc, sales_invoice_referenced_doc[si_ref_doc] + ) + ) + def healthcare_custom_field_in_sales_invoice(): - frappe.reload_doc('healthcare', 'doctype', 'patient') - frappe.reload_doc('healthcare', 'doctype', 'healthcare_practitioner') - if data['custom_fields']: - create_custom_fields(data['custom_fields']) + frappe.reload_doc("healthcare", "doctype", "patient") + frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner") + if data["custom_fields"]: + create_custom_fields(data["custom_fields"]) - frappe.db.sql(""" + frappe.db.sql( + """ delete from `tabCustom Field` where fieldname = 'appointment' and options = 'Patient Appointment' - """) + """ + ) diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py index 1c4d8f1f79f..de453ccf21d 100644 --- a/erpnext/patches/v11_0/refactor_autoname_naming.py +++ b/erpnext/patches/v11_0/refactor_autoname_naming.py @@ -6,99 +6,102 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter doctype_series_map = { - 'Activity Cost': 'PROJ-ACC-.#####', - 'Agriculture Task': 'AG-TASK-.#####', - 'Assessment Plan': 'EDU-ASP-.YYYY.-.#####', - 'Assessment Result': 'EDU-RES-.YYYY.-.#####', - 'Asset Movement': 'ACC-ASM-.YYYY.-.#####', - 'Attendance Request': 'HR-ARQ-.YY.-.MM.-.#####', - 'Authorization Rule': 'HR-ARU-.#####', - 'Bank Guarantee': 'ACC-BG-.YYYY.-.#####', - 'Bin': 'MAT-BIN-.YYYY.-.#####', - 'Certification Application': 'NPO-CAPP-.YYYY.-.#####', - 'Certified Consultant': 'NPO-CONS-.YYYY.-.#####', - 'Chat Room': 'CHAT-ROOM-.#####', - 'Compensatory Leave Request': 'HR-CMP-.YY.-.MM.-.#####', - 'Client Script': 'SYS-SCR-.#####', - 'Employee Benefit Application': 'HR-BEN-APP-.YY.-.MM.-.#####', - 'Employee Benefit Application Detail': '', - 'Employee Benefit Claim': 'HR-BEN-CLM-.YY.-.MM.-.#####', - 'Employee Incentive': 'HR-EINV-.YY.-.MM.-.#####', - 'Employee Onboarding': 'HR-EMP-ONB-.YYYY.-.#####', - 'Employee Onboarding Template': 'HR-EMP-ONT-.#####', - 'Employee Promotion': 'HR-EMP-PRO-.YYYY.-.#####', - 'Employee Separation': 'HR-EMP-SEP-.YYYY.-.#####', - 'Employee Separation Template': 'HR-EMP-STP-.#####', - 'Employee Tax Exemption Declaration': 'HR-TAX-DEC-.YYYY.-.#####', - 'Employee Tax Exemption Proof Submission': 'HR-TAX-PRF-.YYYY.-.#####', - 'Employee Transfer': 'HR-EMP-TRN-.YYYY.-.#####', - 'Event': 'EVENT-.YYYY.-.#####', - 'Exchange Rate Revaluation': 'ACC-ERR-.YYYY.-.#####', - 'GL Entry': 'ACC-GLE-.YYYY.-.#####', - 'Guardian': 'EDU-GRD-.YYYY.-.#####', - 'Hotel Room Reservation': 'HTL-RES-.YYYY.-.#####', - 'Item Price': '', - 'Job Applicant': 'HR-APP-.YYYY.-.#####', - 'Job Offer': 'HR-OFF-.YYYY.-.#####', - 'Leave Encashment': 'HR-ENC-.YYYY.-.#####', - 'Leave Period': 'HR-LPR-.YYYY.-.#####', - 'Leave Policy': 'HR-LPOL-.YYYY.-.#####', - 'Loan': 'ACC-LOAN-.YYYY.-.#####', - 'Loan Application': 'ACC-LOAP-.YYYY.-.#####', - 'Loyalty Point Entry': '', - 'Membership': 'NPO-MSH-.YYYY.-.#####', - 'Packing Slip': 'MAT-PAC-.YYYY.-.#####', - 'Patient Appointment': 'HLC-APP-.YYYY.-.#####', - 'Payment Terms Template Detail': '', - 'Payroll Entry': 'HR-PRUN-.YYYY.-.#####', - 'Period Closing Voucher': 'ACC-PCV-.YYYY.-.#####', - 'Plant Analysis': 'AG-PLA-.YYYY.-.#####', - 'POS Closing Entry': 'POS-CLO-.YYYY.-.#####', - 'Prepared Report': 'SYS-PREP-.YYYY.-.#####', - 'Program Enrollment': 'EDU-ENR-.YYYY.-.#####', - 'Quotation Item': '', - 'Restaurant Reservation': 'RES-RES-.YYYY.-.#####', - 'Retention Bonus': 'HR-RTB-.YYYY.-.#####', - 'Room': 'HTL-ROOM-.YYYY.-.#####', - 'Salary Structure Assignment': 'HR-SSA-.YY.-.MM.-.#####', - 'Sales Taxes and Charges': '', - 'Share Transfer': 'ACC-SHT-.YYYY.-.#####', - 'Shift Assignment': 'HR-SHA-.YY.-.MM.-.#####', - 'Shift Request': 'HR-SHR-.YY.-.MM.-.#####', - 'SMS Log': 'SYS-SMS-.#####', - 'Soil Analysis': 'AG-ANA-.YY.-.MM.-.#####', - 'Soil Texture': 'AG-TEX-.YYYY.-.#####', - 'Stock Ledger Entry': 'MAT-SLE-.YYYY.-.#####', - 'Student Leave Application': 'EDU-SLA-.YYYY.-.#####', - 'Student Log': 'EDU-SLOG-.YYYY.-.#####', - 'Subscription': 'ACC-SUB-.YYYY.-.#####', - 'Task': 'TASK-.YYYY.-.#####', - 'Tax Rule': 'ACC-TAX-RULE-.YYYY.-.#####', - 'Training Feedback': 'HR-TRF-.YYYY.-.#####', - 'Training Result': 'HR-TRR-.YYYY.-.#####', - 'Travel Request': 'HR-TRQ-.YYYY.-.#####', - 'UOM Conversion Factor': 'MAT-UOM-CNV-.#####', - 'Water Analysis': 'HR-WAT-.YYYY.-.#####', - 'Workflow Action': 'SYS-WACT-.#####', + "Activity Cost": "PROJ-ACC-.#####", + "Agriculture Task": "AG-TASK-.#####", + "Assessment Plan": "EDU-ASP-.YYYY.-.#####", + "Assessment Result": "EDU-RES-.YYYY.-.#####", + "Asset Movement": "ACC-ASM-.YYYY.-.#####", + "Attendance Request": "HR-ARQ-.YY.-.MM.-.#####", + "Authorization Rule": "HR-ARU-.#####", + "Bank Guarantee": "ACC-BG-.YYYY.-.#####", + "Bin": "MAT-BIN-.YYYY.-.#####", + "Certification Application": "NPO-CAPP-.YYYY.-.#####", + "Certified Consultant": "NPO-CONS-.YYYY.-.#####", + "Chat Room": "CHAT-ROOM-.#####", + "Compensatory Leave Request": "HR-CMP-.YY.-.MM.-.#####", + "Client Script": "SYS-SCR-.#####", + "Employee Benefit Application": "HR-BEN-APP-.YY.-.MM.-.#####", + "Employee Benefit Application Detail": "", + "Employee Benefit Claim": "HR-BEN-CLM-.YY.-.MM.-.#####", + "Employee Incentive": "HR-EINV-.YY.-.MM.-.#####", + "Employee Onboarding": "HR-EMP-ONB-.YYYY.-.#####", + "Employee Onboarding Template": "HR-EMP-ONT-.#####", + "Employee Promotion": "HR-EMP-PRO-.YYYY.-.#####", + "Employee Separation": "HR-EMP-SEP-.YYYY.-.#####", + "Employee Separation Template": "HR-EMP-STP-.#####", + "Employee Tax Exemption Declaration": "HR-TAX-DEC-.YYYY.-.#####", + "Employee Tax Exemption Proof Submission": "HR-TAX-PRF-.YYYY.-.#####", + "Employee Transfer": "HR-EMP-TRN-.YYYY.-.#####", + "Event": "EVENT-.YYYY.-.#####", + "Exchange Rate Revaluation": "ACC-ERR-.YYYY.-.#####", + "GL Entry": "ACC-GLE-.YYYY.-.#####", + "Guardian": "EDU-GRD-.YYYY.-.#####", + "Hotel Room Reservation": "HTL-RES-.YYYY.-.#####", + "Item Price": "", + "Job Applicant": "HR-APP-.YYYY.-.#####", + "Job Offer": "HR-OFF-.YYYY.-.#####", + "Leave Encashment": "HR-ENC-.YYYY.-.#####", + "Leave Period": "HR-LPR-.YYYY.-.#####", + "Leave Policy": "HR-LPOL-.YYYY.-.#####", + "Loan": "ACC-LOAN-.YYYY.-.#####", + "Loan Application": "ACC-LOAP-.YYYY.-.#####", + "Loyalty Point Entry": "", + "Membership": "NPO-MSH-.YYYY.-.#####", + "Packing Slip": "MAT-PAC-.YYYY.-.#####", + "Patient Appointment": "HLC-APP-.YYYY.-.#####", + "Payment Terms Template Detail": "", + "Payroll Entry": "HR-PRUN-.YYYY.-.#####", + "Period Closing Voucher": "ACC-PCV-.YYYY.-.#####", + "Plant Analysis": "AG-PLA-.YYYY.-.#####", + "POS Closing Entry": "POS-CLO-.YYYY.-.#####", + "Prepared Report": "SYS-PREP-.YYYY.-.#####", + "Program Enrollment": "EDU-ENR-.YYYY.-.#####", + "Quotation Item": "", + "Restaurant Reservation": "RES-RES-.YYYY.-.#####", + "Retention Bonus": "HR-RTB-.YYYY.-.#####", + "Room": "HTL-ROOM-.YYYY.-.#####", + "Salary Structure Assignment": "HR-SSA-.YY.-.MM.-.#####", + "Sales Taxes and Charges": "", + "Share Transfer": "ACC-SHT-.YYYY.-.#####", + "Shift Assignment": "HR-SHA-.YY.-.MM.-.#####", + "Shift Request": "HR-SHR-.YY.-.MM.-.#####", + "SMS Log": "SYS-SMS-.#####", + "Soil Analysis": "AG-ANA-.YY.-.MM.-.#####", + "Soil Texture": "AG-TEX-.YYYY.-.#####", + "Stock Ledger Entry": "MAT-SLE-.YYYY.-.#####", + "Student Leave Application": "EDU-SLA-.YYYY.-.#####", + "Student Log": "EDU-SLOG-.YYYY.-.#####", + "Subscription": "ACC-SUB-.YYYY.-.#####", + "Task": "TASK-.YYYY.-.#####", + "Tax Rule": "ACC-TAX-RULE-.YYYY.-.#####", + "Training Feedback": "HR-TRF-.YYYY.-.#####", + "Training Result": "HR-TRR-.YYYY.-.#####", + "Travel Request": "HR-TRQ-.YYYY.-.#####", + "UOM Conversion Factor": "MAT-UOM-CNV-.#####", + "Water Analysis": "HR-WAT-.YYYY.-.#####", + "Workflow Action": "SYS-WACT-.#####", } + def execute(): series_to_set = get_series() for doctype, opts in series_to_set.items(): - set_series(doctype, opts['value']) + set_series(doctype, opts["value"]) + def set_series(doctype, value): - doc = frappe.db.exists('Property Setter', {'doc_type': doctype, 'property': 'autoname'}) + doc = frappe.db.exists("Property Setter", {"doc_type": doctype, "property": "autoname"}) if doc: - frappe.db.set_value('Property Setter', doc, 'value', value) + frappe.db.set_value("Property Setter", doc, "value", value) else: - make_property_setter(doctype, '', 'autoname', value, '', for_doctype = True) + make_property_setter(doctype, "", "autoname", value, "", for_doctype=True) + def get_series(): series_to_set = {} for doctype in doctype_series_map: - if not frappe.db.exists('DocType', doctype): + if not frappe.db.exists("DocType", doctype): continue if not frappe.db.a_row_exists(doctype): @@ -110,10 +113,11 @@ def get_series(): # set autoname property setter if series_to_preserve: - series_to_set[doctype] = {'value': series_to_preserve} + series_to_set[doctype] = {"value": series_to_preserve} return series_to_set + def get_series_to_preserve(doctype): - series_to_preserve = frappe.db.get_value('DocType', doctype, 'autoname') + series_to_preserve = frappe.db.get_value("DocType", doctype, "autoname") return series_to_preserve diff --git a/erpnext/patches/v11_0/refactor_erpnext_shopify.py b/erpnext/patches/v11_0/refactor_erpnext_shopify.py index 684b1b3fd6a..308676d057d 100644 --- a/erpnext/patches/v11_0/refactor_erpnext_shopify.py +++ b/erpnext/patches/v11_0/refactor_erpnext_shopify.py @@ -1,18 +1,17 @@ - import frappe from frappe.installer import remove_from_installed_apps def execute(): - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_tax_account') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_log') - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_webhook_detail') + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings") + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_tax_account") + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_log") + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_webhook_detail") - if 'erpnext_shopify' in frappe.get_installed_apps(): - remove_from_installed_apps('erpnext_shopify') + if "erpnext_shopify" in frappe.get_installed_apps(): + remove_from_installed_apps("erpnext_shopify") - frappe.delete_doc("Module Def", 'erpnext_shopify') + frappe.delete_doc("Module Def", "erpnext_shopify") frappe.db.commit() @@ -22,11 +21,14 @@ def execute(): else: disable_shopify() + def setup_app_type(): try: shopify_settings = frappe.get_doc("Shopify Settings") - shopify_settings.app_type = 'Private' - shopify_settings.update_price_in_erpnext_price_list = 0 if getattr(shopify_settings, 'push_prices_to_shopify', None) else 1 + shopify_settings.app_type = "Private" + shopify_settings.update_price_in_erpnext_price_list = ( + 0 if getattr(shopify_settings, "push_prices_to_shopify", None) else 1 + ) shopify_settings.flags.ignore_mandatory = True shopify_settings.ignore_permissions = True shopify_settings.save() @@ -34,11 +36,15 @@ def setup_app_type(): frappe.db.set_value("Shopify Settings", None, "enable_shopify", 0) frappe.log_error(frappe.get_traceback()) + def disable_shopify(): # due to frappe.db.set_value wrongly written and enable_shopify being default 1 # Shopify Settings isn't properly configured and leads to error - shopify = frappe.get_doc('Shopify Settings') + shopify = frappe.get_doc("Shopify Settings") - if shopify.app_type == "Public" or shopify.app_type == None or \ - (shopify.enable_shopify and not (shopify.shopify_url or shopify.api_key)): + if ( + shopify.app_type == "Public" + or shopify.app_type == None + or (shopify.enable_shopify and not (shopify.shopify_url or shopify.api_key)) + ): frappe.db.set_value("Shopify Settings", None, "enable_shopify", 0) diff --git a/erpnext/patches/v11_0/refactor_naming_series.py b/erpnext/patches/v11_0/refactor_naming_series.py index 6b275e2b1ef..efc1540f288 100644 --- a/erpnext/patches/v11_0/refactor_naming_series.py +++ b/erpnext/patches/v11_0/refactor_naming_series.py @@ -6,88 +6,94 @@ import frappe from frappe.custom.doctype.property_setter.property_setter import make_property_setter doctype_series_map = { - 'Additional Salary': 'HR-ADS-.YY.-.MM.-', - 'Appraisal': 'HR-APR-.YY.-.MM.', - 'Asset': 'ACC-ASS-.YYYY.-', - 'Attendance': 'HR-ATT-.YYYY.-', - 'Auto Repeat': 'SYS-ARP-.YYYY.-', - 'Blanket Order': 'MFG-BLR-.YYYY.-', - 'C-Form': 'ACC-CF-.YYYY.-', - 'Campaign': 'SAL-CAM-.YYYY.-', - 'Clinical Procedure': 'HLC-CPR-.YYYY.-', - 'Course Schedule': 'EDU-CSH-.YYYY.-', - 'Customer': 'CUST-.YYYY.-', - 'Delivery Note': 'MAT-DN-.YYYY.-', - 'Delivery Trip': 'MAT-DT-.YYYY.-', - 'Driver': 'HR-DRI-.YYYY.-', - 'Employee': 'HR-EMP-', - 'Employee Advance': 'HR-EAD-.YYYY.-', - 'Expense Claim': 'HR-EXP-.YYYY.-', - 'Fee Schedule': 'EDU-FSH-.YYYY.-', - 'Fee Structure': 'EDU-FST-.YYYY.-', - 'Fees': 'EDU-FEE-.YYYY.-', - 'Inpatient Record': 'HLC-INP-.YYYY.-', - 'Installation Note': 'MAT-INS-.YYYY.-', - 'Instructor': 'EDU-INS-.YYYY.-', - 'Issue': 'ISS-.YYYY.-', - 'Journal Entry': 'ACC-JV-.YYYY.-', - 'Lab Test': 'HLC-LT-.YYYY.-', - 'Landed Cost Voucher': 'MAT-LCV-.YYYY.-', - 'Lead': 'CRM-LEAD-.YYYY.-', - 'Leave Allocation': 'HR-LAL-.YYYY.-', - 'Leave Application': 'HR-LAP-.YYYY.-', - 'Maintenance Schedule': 'MAT-MSH-.YYYY.-', - 'Maintenance Visit': 'MAT-MVS-.YYYY.-', - 'Material Request': 'MAT-MR-.YYYY.-', - 'Member': 'NPO-MEM-.YYYY.-', - 'Opportunity': 'CRM-OPP-.YYYY.-', - 'Packing Slip': 'MAT-PAC-.YYYY.-', - 'Patient': 'HLC-PAT-.YYYY.-', - 'Patient Encounter': 'HLC-ENC-.YYYY.-', - 'Patient Medical Record': 'HLC-PMR-.YYYY.-', - 'Payment Entry': 'ACC-PAY-.YYYY.-', - 'Payment Request': 'ACC-PRQ-.YYYY.-', - 'Production Plan': 'MFG-PP-.YYYY.-', - 'Project Update': 'PROJ-UPD-.YYYY.-', - 'Purchase Invoice': 'ACC-PINV-.YYYY.-', - 'Purchase Order': 'PUR-ORD-.YYYY.-', - 'Purchase Receipt': 'MAT-PRE-.YYYY.-', - 'Quality Inspection': 'MAT-QA-.YYYY.-', - 'Quotation': 'SAL-QTN-.YYYY.-', - 'Request for Quotation': 'PUR-RFQ-.YYYY.-', - 'Sales Invoice': 'ACC-SINV-.YYYY.-', - 'Sales Order': 'SAL-ORD-.YYYY.-', - 'Sample Collection': 'HLC-SC-.YYYY.-', - 'Shareholder': 'ACC-SH-.YYYY.-', - 'Stock Entry': 'MAT-STE-.YYYY.-', - 'Stock Reconciliation': 'MAT-RECO-.YYYY.-', - 'Student': 'EDU-STU-.YYYY.-', - 'Student Applicant': 'EDU-APP-.YYYY.-', - 'Supplier': 'SUP-.YYYY.-', - 'Supplier Quotation': 'PUR-SQTN-.YYYY.-', - 'Supplier Scorecard Period': 'PU-SSP-.YYYY.-', - 'Timesheet': 'TS-.YYYY.-', - 'Vehicle Log': 'HR-VLOG-.YYYY.-', - 'Warranty Claim': 'SER-WRN-.YYYY.-', - 'Work Order': 'MFG-WO-.YYYY.-' + "Additional Salary": "HR-ADS-.YY.-.MM.-", + "Appraisal": "HR-APR-.YY.-.MM.", + "Asset": "ACC-ASS-.YYYY.-", + "Attendance": "HR-ATT-.YYYY.-", + "Auto Repeat": "SYS-ARP-.YYYY.-", + "Blanket Order": "MFG-BLR-.YYYY.-", + "C-Form": "ACC-CF-.YYYY.-", + "Campaign": "SAL-CAM-.YYYY.-", + "Clinical Procedure": "HLC-CPR-.YYYY.-", + "Course Schedule": "EDU-CSH-.YYYY.-", + "Customer": "CUST-.YYYY.-", + "Delivery Note": "MAT-DN-.YYYY.-", + "Delivery Trip": "MAT-DT-.YYYY.-", + "Driver": "HR-DRI-.YYYY.-", + "Employee": "HR-EMP-", + "Employee Advance": "HR-EAD-.YYYY.-", + "Expense Claim": "HR-EXP-.YYYY.-", + "Fee Schedule": "EDU-FSH-.YYYY.-", + "Fee Structure": "EDU-FST-.YYYY.-", + "Fees": "EDU-FEE-.YYYY.-", + "Inpatient Record": "HLC-INP-.YYYY.-", + "Installation Note": "MAT-INS-.YYYY.-", + "Instructor": "EDU-INS-.YYYY.-", + "Issue": "ISS-.YYYY.-", + "Journal Entry": "ACC-JV-.YYYY.-", + "Lab Test": "HLC-LT-.YYYY.-", + "Landed Cost Voucher": "MAT-LCV-.YYYY.-", + "Lead": "CRM-LEAD-.YYYY.-", + "Leave Allocation": "HR-LAL-.YYYY.-", + "Leave Application": "HR-LAP-.YYYY.-", + "Maintenance Schedule": "MAT-MSH-.YYYY.-", + "Maintenance Visit": "MAT-MVS-.YYYY.-", + "Material Request": "MAT-MR-.YYYY.-", + "Member": "NPO-MEM-.YYYY.-", + "Opportunity": "CRM-OPP-.YYYY.-", + "Packing Slip": "MAT-PAC-.YYYY.-", + "Patient": "HLC-PAT-.YYYY.-", + "Patient Encounter": "HLC-ENC-.YYYY.-", + "Patient Medical Record": "HLC-PMR-.YYYY.-", + "Payment Entry": "ACC-PAY-.YYYY.-", + "Payment Request": "ACC-PRQ-.YYYY.-", + "Production Plan": "MFG-PP-.YYYY.-", + "Project Update": "PROJ-UPD-.YYYY.-", + "Purchase Invoice": "ACC-PINV-.YYYY.-", + "Purchase Order": "PUR-ORD-.YYYY.-", + "Purchase Receipt": "MAT-PRE-.YYYY.-", + "Quality Inspection": "MAT-QA-.YYYY.-", + "Quotation": "SAL-QTN-.YYYY.-", + "Request for Quotation": "PUR-RFQ-.YYYY.-", + "Sales Invoice": "ACC-SINV-.YYYY.-", + "Sales Order": "SAL-ORD-.YYYY.-", + "Sample Collection": "HLC-SC-.YYYY.-", + "Shareholder": "ACC-SH-.YYYY.-", + "Stock Entry": "MAT-STE-.YYYY.-", + "Stock Reconciliation": "MAT-RECO-.YYYY.-", + "Student": "EDU-STU-.YYYY.-", + "Student Applicant": "EDU-APP-.YYYY.-", + "Supplier": "SUP-.YYYY.-", + "Supplier Quotation": "PUR-SQTN-.YYYY.-", + "Supplier Scorecard Period": "PU-SSP-.YYYY.-", + "Timesheet": "TS-.YYYY.-", + "Vehicle Log": "HR-VLOG-.YYYY.-", + "Warranty Claim": "SER-WRN-.YYYY.-", + "Work Order": "MFG-WO-.YYYY.-", } + def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update `tabProperty Setter` set name=concat(doc_type, '-', field_name, '-', property) where property='fetch_from' - """) + """ + ) series_to_set = get_series() for doctype, opts in series_to_set.items(): set_series(doctype, opts["options"], opts["default"]) + def set_series(doctype, options, default): def _make_property_setter(property_name, value): - property_setter = frappe.db.exists('Property Setter', - {'doc_type': doctype, 'field_name': 'naming_series', 'property': property_name}) + property_setter = frappe.db.exists( + "Property Setter", + {"doc_type": doctype, "field_name": "naming_series", "property": property_name}, + ) if property_setter: - frappe.db.set_value('Property Setter', property_setter, 'value', value) + frappe.db.set_value("Property Setter", property_setter, "value", value) else: make_property_setter(doctype, "naming_series", "options", value, "Text") @@ -95,17 +101,18 @@ def set_series(doctype, options, default): if default: _make_property_setter("default", default) + def get_series(): series_to_set = {} for doctype in doctype_series_map: - if not frappe.db.exists('DocType', doctype): + if not frappe.db.exists("DocType", doctype): continue if not frappe.db.a_row_exists(doctype): continue - if not frappe.db.has_column(doctype, 'naming_series'): + if not frappe.db.has_column(doctype, "naming_series"): continue - if not frappe.get_meta(doctype).has_field('naming_series'): + if not frappe.get_meta(doctype).has_field("naming_series"): continue series_to_preserve = list(filter(None, get_series_to_preserve(doctype))) default_series = get_default_series(doctype) @@ -123,12 +130,18 @@ def get_series(): return series_to_set + def get_series_to_preserve(doctype): - series_to_preserve = frappe.db.sql_list("""select distinct naming_series from `tab{doctype}` where ifnull(naming_series, '') != ''""".format(doctype=doctype)) + series_to_preserve = frappe.db.sql_list( + """select distinct naming_series from `tab{doctype}` where ifnull(naming_series, '') != ''""".format( + doctype=doctype + ) + ) series_to_preserve.sort() return series_to_preserve + def get_default_series(doctype): field = frappe.get_meta(doctype).get_field("naming_series") - default_series = field.get('default', '') if field else '' + default_series = field.get("default", "") if field else "" return default_series diff --git a/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py b/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py index caf74f578de..2e0204c22b9 100644 --- a/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py +++ b/erpnext/patches/v11_0/remove_barcodes_field_from_copy_fields_to_variants.py @@ -2,7 +2,7 @@ import frappe def execute(): - '''Remove barcodes field from "Copy Fields to Variants" table because barcodes must be unique''' + """Remove barcodes field from "Copy Fields to Variants" table because barcodes must be unique""" - settings = frappe.get_doc('Item Variant Settings') + settings = frappe.get_doc("Item Variant Settings") settings.remove_invalid_fields_for_copy_fields_in_variants() diff --git a/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py b/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py index 85292e8d135..036ae8ebfc1 100644 --- a/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py +++ b/erpnext/patches/v11_0/rename_additional_salary_component_additional_salary.py @@ -1,11 +1,11 @@ - import frappe # this patch should have been included with this PR https://github.com/frappe/erpnext/pull/14302 + def execute(): if frappe.db.table_exists("Additional Salary Component"): if not frappe.db.table_exists("Additional Salary"): frappe.rename_doc("DocType", "Additional Salary Component", "Additional Salary") - frappe.delete_doc('DocType', "Additional Salary Component") + frappe.delete_doc("DocType", "Additional Salary Component") diff --git a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py index c7a3aa2abd4..c444c16a59b 100644 --- a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py +++ b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py @@ -6,6 +6,8 @@ import frappe def execute(): - if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists("Asset Value Adjustment"): - frappe.rename_doc('DocType', 'Asset Adjustment', 'Asset Value Adjustment', force=True) - frappe.reload_doc('assets', 'doctype', 'asset_value_adjustment') + if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists( + "Asset Value Adjustment" + ): + frappe.rename_doc("DocType", "Asset Adjustment", "Asset Value Adjustment", force=True) + frappe.reload_doc("assets", "doctype", "asset_value_adjustment") diff --git a/erpnext/patches/v11_0/rename_bom_wo_fields.py b/erpnext/patches/v11_0/rename_bom_wo_fields.py index cab7d0a673f..fb25eeb6fcc 100644 --- a/erpnext/patches/v11_0/rename_bom_wo_fields.py +++ b/erpnext/patches/v11_0/rename_bom_wo_fields.py @@ -7,28 +7,36 @@ from frappe.model.utils.rename_field import rename_field def execute(): - # updating column value to handle field change from Data to Currency - changed_field = "base_scrap_material_cost" - frappe.db.sql(f"update `tabBOM` set {changed_field} = '0' where trim(coalesce({changed_field}, ''))= ''") + # updating column value to handle field change from Data to Currency + changed_field = "base_scrap_material_cost" + frappe.db.sql( + f"update `tabBOM` set {changed_field} = '0' where trim(coalesce({changed_field}, ''))= ''" + ) - for doctype in ['BOM Explosion Item', 'BOM Item', 'Work Order Item', 'Item']: - if frappe.db.has_column(doctype, 'allow_transfer_for_manufacture'): - if doctype != 'Item': - frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) - else: - frappe.reload_doc('stock', 'doctype', frappe.scrub(doctype)) + for doctype in ["BOM Explosion Item", "BOM Item", "Work Order Item", "Item"]: + if frappe.db.has_column(doctype, "allow_transfer_for_manufacture"): + if doctype != "Item": + frappe.reload_doc("manufacturing", "doctype", frappe.scrub(doctype)) + else: + frappe.reload_doc("stock", "doctype", frappe.scrub(doctype)) - rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing") + rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing") - for doctype in ['BOM', 'Work Order']: - frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) + for doctype in ["BOM", "Work Order"]: + frappe.reload_doc("manufacturing", "doctype", frappe.scrub(doctype)) - if frappe.db.has_column(doctype, 'transfer_material_against_job_card'): - frappe.db.sql(""" UPDATE `tab%s` + if frappe.db.has_column(doctype, "transfer_material_against_job_card"): + frappe.db.sql( + """ UPDATE `tab%s` SET transfer_material_against = CASE WHEN transfer_material_against_job_card = 1 then 'Job Card' Else 'Work Order' END - WHERE docstatus < 2""" % (doctype)) - else: - frappe.db.sql(""" UPDATE `tab%s` + WHERE docstatus < 2""" + % (doctype) + ) + else: + frappe.db.sql( + """ UPDATE `tab%s` SET transfer_material_against = 'Work Order' - WHERE docstatus < 2""" % (doctype)) + WHERE docstatus < 2""" + % (doctype) + ) diff --git a/erpnext/patches/v11_0/rename_duplicate_item_code_values.py b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py index 61f3856e8eb..1f65e14814d 100644 --- a/erpnext/patches/v11_0/rename_duplicate_item_code_values.py +++ b/erpnext/patches/v11_0/rename_duplicate_item_code_values.py @@ -3,7 +3,9 @@ import frappe def execute(): items = [] - items = frappe.db.sql("""select item_code from `tabItem` group by item_code having count(*) > 1""", as_dict=True) + items = frappe.db.sql( + """select item_code from `tabItem` group by item_code having count(*) > 1""", as_dict=True + ) if items: for item in items: frappe.db.sql("""update `tabItem` set item_code=name where item_code = %s""", (item.item_code)) diff --git a/erpnext/patches/v11_0/rename_field_max_days_allowed.py b/erpnext/patches/v11_0/rename_field_max_days_allowed.py index fb08be8628b..0813770efcc 100644 --- a/erpnext/patches/v11_0/rename_field_max_days_allowed.py +++ b/erpnext/patches/v11_0/rename_field_max_days_allowed.py @@ -1,14 +1,15 @@ - import frappe from frappe.model.utils.rename_field import rename_field def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLeave Type` SET max_days_allowed = '0' WHERE trim(coalesce(max_days_allowed, '')) = '' - """) + """ + ) frappe.db.sql_ddl("""ALTER table `tabLeave Type` modify max_days_allowed int(8) NOT NULL""") frappe.reload_doc("hr", "doctype", "leave_type") rename_field("Leave Type", "max_days_allowed", "max_continuous_days_allowed") diff --git a/erpnext/patches/v11_0/rename_health_insurance.py b/erpnext/patches/v11_0/rename_health_insurance.py index 1b6db89101b..3509c6b104e 100644 --- a/erpnext/patches/v11_0/rename_health_insurance.py +++ b/erpnext/patches/v11_0/rename_health_insurance.py @@ -6,5 +6,5 @@ import frappe def execute(): - frappe.rename_doc('DocType', 'Health Insurance', 'Employee Health Insurance', force=True) - frappe.reload_doc('hr', 'doctype', 'employee_health_insurance') + frappe.rename_doc("DocType", "Health Insurance", "Employee Health Insurance", force=True) + frappe.reload_doc("hr", "doctype", "employee_health_insurance") diff --git a/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py b/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py index 55717f88ea1..f237937e9cd 100644 --- a/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py +++ b/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field from frappe.modules import get_doctype_module, scrub @@ -8,21 +7,15 @@ field_rename_map = { ["consultation_time", "encounter_time"], ["consultation_date", "encounter_date"], ["consultation_comment", "encounter_comment"], - ["physician", "practitioner"] - ], - "Fee Validity": [ - ["physician", "practitioner"] - ], - "Lab Test": [ - ["physician", "practitioner"] + ["physician", "practitioner"], ], + "Fee Validity": [["physician", "practitioner"]], + "Lab Test": [["physician", "practitioner"]], "Patient Appointment": [ ["physician", "practitioner"], - ["referring_physician", "referring_practitioner"] + ["referring_physician", "referring_practitioner"], ], - "Procedure Prescription": [ - ["physician", "practitioner"] - ] + "Procedure Prescription": [["physician", "practitioner"]], } doc_rename_map = { @@ -30,37 +23,40 @@ doc_rename_map = { "Physician Schedule": "Practitioner Schedule", "Physician Service Unit Schedule": "Practitioner Service Unit Schedule", "Consultation": "Patient Encounter", - "Physician": "Healthcare Practitioner" + "Physician": "Healthcare Practitioner", } + def execute(): for dt in doc_rename_map: - if frappe.db.exists('DocType', dt): - frappe.rename_doc('DocType', dt, doc_rename_map[dt], force=True) + if frappe.db.exists("DocType", dt): + frappe.rename_doc("DocType", dt, doc_rename_map[dt], force=True) for dn in field_rename_map: - if frappe.db.exists('DocType', dn): + if frappe.db.exists("DocType", dn): frappe.reload_doc(get_doctype_module(dn), "doctype", scrub(dn)) for dt, field_list in field_rename_map.items(): - if frappe.db.exists('DocType', dt): + if frappe.db.exists("DocType", dt): for field in field_list: if frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) - if frappe.db.exists('DocType', 'Practitioner Service Unit Schedule'): - if frappe.db.has_column('Practitioner Service Unit Schedule', 'parentfield'): - frappe.db.sql(""" + if frappe.db.exists("DocType", "Practitioner Service Unit Schedule"): + if frappe.db.has_column("Practitioner Service Unit Schedule", "parentfield"): + frappe.db.sql( + """ update `tabPractitioner Service Unit Schedule` set parentfield = 'practitioner_schedules' where parentfield = 'physician_schedules' and parenttype = 'Healthcare Practitioner' - """) + """ + ) if frappe.db.exists("DocType", "Healthcare Practitioner"): frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner") frappe.reload_doc("healthcare", "doctype", "practitioner_service_unit_schedule") - if frappe.db.has_column('Healthcare Practitioner', 'physician_schedule'): - for doc in frappe.get_all('Healthcare Practitioner'): - _doc = frappe.get_doc('Healthcare Practitioner', doc.name) + if frappe.db.has_column("Healthcare Practitioner", "physician_schedule"): + for doc in frappe.get_all("Healthcare Practitioner"): + _doc = frappe.get_doc("Healthcare Practitioner", doc.name) if _doc.physician_schedule: - _doc.append('practitioner_schedules', {'schedule': _doc.physician_schedule}) + _doc.append("practitioner_schedules", {"schedule": _doc.physician_schedule}) _doc.save() diff --git a/erpnext/patches/v11_0/rename_healthcare_fields.py b/erpnext/patches/v11_0/rename_healthcare_fields.py index 88aac61333a..f2ac4d223ef 100644 --- a/erpnext/patches/v11_0/rename_healthcare_fields.py +++ b/erpnext/patches/v11_0/rename_healthcare_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field from frappe.modules import get_doctype_module, scrub @@ -18,36 +17,48 @@ lab_test_event = ["test_event", "lab_test_event"] lab_test_particulars = ["test_particulars", "lab_test_particulars"] field_rename_map = { - "Lab Test Template": [lab_test_name, lab_test_code, lab_test_rate, lab_test_description, - lab_test_group, lab_test_template_type, lab_test_uom, lab_test_normal_range], + "Lab Test Template": [ + lab_test_name, + lab_test_code, + lab_test_rate, + lab_test_description, + lab_test_group, + lab_test_template_type, + lab_test_uom, + lab_test_normal_range, + ], "Normal Test Items": [lab_test_name, lab_test_comment, lab_test_uom, lab_test_event], "Lab Test": [lab_test_name, lab_test_comment, lab_test_group], "Lab Prescription": [lab_test_name, lab_test_code, lab_test_comment, lab_test_created], "Lab Test Groups": [lab_test_template, lab_test_rate, lab_test_description], "Lab Test UOM": [lab_test_uom], "Normal Test Template": [lab_test_uom, lab_test_event], - "Special Test Items": [lab_test_particulars] + "Special Test Items": [lab_test_particulars], } def execute(): for dt, field_list in field_rename_map.items(): - if frappe.db.exists('DocType', dt): + if frappe.db.exists("DocType", dt): frappe.reload_doc(get_doctype_module(dt), "doctype", scrub(dt)) for field in field_list: if frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) - if frappe.db.exists('DocType', 'Lab Prescription'): - if frappe.db.has_column('Lab Prescription', 'parentfield'): - frappe.db.sql(""" + if frappe.db.exists("DocType", "Lab Prescription"): + if frappe.db.has_column("Lab Prescription", "parentfield"): + frappe.db.sql( + """ update `tabLab Prescription` set parentfield = 'lab_test_prescription' where parentfield = 'test_prescription' - """) + """ + ) - if frappe.db.exists('DocType', 'Lab Test Groups'): - if frappe.db.has_column('Lab Test Groups', 'parentfield'): - frappe.db.sql(""" + if frappe.db.exists("DocType", "Lab Test Groups"): + if frappe.db.has_column("Lab Test Groups", "parentfield"): + frappe.db.sql( + """ update `tabLab Test Groups` set parentfield = 'lab_test_groups' where parentfield = 'test_groups' - """) + """ + ) diff --git a/erpnext/patches/v11_0/rename_members_with_naming_series.py b/erpnext/patches/v11_0/rename_members_with_naming_series.py index 49dbc8a6dc8..4dffbc8fe81 100644 --- a/erpnext/patches/v11_0/rename_members_with_naming_series.py +++ b/erpnext/patches/v11_0/rename_members_with_naming_series.py @@ -1,11 +1,10 @@ - import frappe def execute(): frappe.reload_doc("non_profit", "doctype", "member") - old_named_members = frappe.get_all("Member", filters = {"name": ("not like", "MEM-%")}) - correctly_named_members = frappe.get_all("Member", filters = {"name": ("like", "MEM-%")}) + old_named_members = frappe.get_all("Member", filters={"name": ("not like", "MEM-%")}) + correctly_named_members = frappe.get_all("Member", filters={"name": ("like", "MEM-%")}) current_index = len(correctly_named_members) for member in old_named_members: diff --git a/erpnext/patches/v11_0/rename_overproduction_percent_field.py b/erpnext/patches/v11_0/rename_overproduction_percent_field.py index c78ec5d0128..74699db41ef 100644 --- a/erpnext/patches/v11_0/rename_overproduction_percent_field.py +++ b/erpnext/patches/v11_0/rename_overproduction_percent_field.py @@ -7,5 +7,9 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') - rename_field('Manufacturing Settings', 'over_production_allowance_percentage', 'overproduction_percentage_for_sales_order') + frappe.reload_doc("manufacturing", "doctype", "manufacturing_settings") + rename_field( + "Manufacturing Settings", + "over_production_allowance_percentage", + "overproduction_percentage_for_sales_order", + ) diff --git a/erpnext/patches/v11_0/rename_production_order_to_work_order.py b/erpnext/patches/v11_0/rename_production_order_to_work_order.py index 453a5710a1d..b58ac4e72f1 100644 --- a/erpnext/patches/v11_0/rename_production_order_to_work_order.py +++ b/erpnext/patches/v11_0/rename_production_order_to_work_order.py @@ -7,22 +7,28 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.rename_doc('DocType', 'Production Order', 'Work Order', force=True) - frappe.reload_doc('manufacturing', 'doctype', 'work_order') + frappe.rename_doc("DocType", "Production Order", "Work Order", force=True) + frappe.reload_doc("manufacturing", "doctype", "work_order") - frappe.rename_doc('DocType', 'Production Order Item', 'Work Order Item', force=True) - frappe.reload_doc('manufacturing', 'doctype', 'work_order_item') + frappe.rename_doc("DocType", "Production Order Item", "Work Order Item", force=True) + frappe.reload_doc("manufacturing", "doctype", "work_order_item") - frappe.rename_doc('DocType', 'Production Order Operation', 'Work Order Operation', force=True) - frappe.reload_doc('manufacturing', 'doctype', 'work_order_operation') + frappe.rename_doc("DocType", "Production Order Operation", "Work Order Operation", force=True) + frappe.reload_doc("manufacturing", "doctype", "work_order_operation") - frappe.reload_doc('projects', 'doctype', 'timesheet') - frappe.reload_doc('stock', 'doctype', 'stock_entry') + frappe.reload_doc("projects", "doctype", "timesheet") + frappe.reload_doc("stock", "doctype", "stock_entry") rename_field("Timesheet", "production_order", "work_order") rename_field("Stock Entry", "production_order", "work_order") - frappe.rename_doc("Report", "Production Orders in Progress", "Work Orders in Progress", force=True) + frappe.rename_doc( + "Report", "Production Orders in Progress", "Work Orders in Progress", force=True + ) frappe.rename_doc("Report", "Completed Production Orders", "Completed Work Orders", force=True) frappe.rename_doc("Report", "Open Production Orders", "Open Work Orders", force=True) - frappe.rename_doc("Report", "Issued Items Against Production Order", "Issued Items Against Work Order", force=True) - frappe.rename_doc("Report", "Production Order Stock Report", "Work Order Stock Report", force=True) + frappe.rename_doc( + "Report", "Issued Items Against Production Order", "Issued Items Against Work Order", force=True + ) + frappe.rename_doc( + "Report", "Production Order Stock Report", "Work Order Stock Report", force=True + ) diff --git a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py index fd7e684c61a..96daba7d368 100644 --- a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py +++ b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.model.utils.rename_field import rename_field @@ -7,10 +6,10 @@ from frappe.utils.nestedset import rebuild_tree def execute(): if frappe.db.table_exists("Supplier Group"): - frappe.reload_doc('setup', 'doctype', 'supplier_group') + frappe.reload_doc("setup", "doctype", "supplier_group") elif frappe.db.table_exists("Supplier Type"): frappe.rename_doc("DocType", "Supplier Type", "Supplier Group", force=True) - frappe.reload_doc('setup', 'doctype', 'supplier_group') + frappe.reload_doc("setup", "doctype", "supplier_group") frappe.reload_doc("accounts", "doctype", "pricing_rule") frappe.reload_doc("accounts", "doctype", "tax_rule") frappe.reload_doc("buying", "doctype", "buying_settings") @@ -23,16 +22,23 @@ def execute(): build_tree() -def build_tree(): - frappe.db.sql("""update `tabSupplier Group` set parent_supplier_group = '{0}' - where is_group = 0""".format(_('All Supplier Groups'))) - if not frappe.db.exists("Supplier Group", _('All Supplier Groups')): - frappe.get_doc({ - 'doctype': 'Supplier Group', - 'supplier_group_name': _('All Supplier Groups'), - 'is_group': 1, - 'parent_supplier_group': '' - }).insert(ignore_permissions=True) +def build_tree(): + frappe.db.sql( + """update `tabSupplier Group` set parent_supplier_group = '{0}' + where is_group = 0""".format( + _("All Supplier Groups") + ) + ) + + if not frappe.db.exists("Supplier Group", _("All Supplier Groups")): + frappe.get_doc( + { + "doctype": "Supplier Group", + "supplier_group_name": _("All Supplier Groups"), + "is_group": 1, + "parent_supplier_group": "", + } + ).insert(ignore_permissions=True) rebuild_tree("Supplier Group", "parent_supplier_group") diff --git a/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py index f23a81494be..4dc2521d391 100644 --- a/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py +++ b/erpnext/patches/v11_0/renamed_from_to_fields_in_project.py @@ -7,8 +7,8 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('projects', 'doctype', 'project') + frappe.reload_doc("projects", "doctype", "project") - if frappe.db.has_column('Project', 'from'): - rename_field('Project', 'from', 'from_time') - rename_field('Project', 'to', 'to_time') + if frappe.db.has_column("Project", "from"): + rename_field("Project", "from", "from_time") + rename_field("Project", "to", "to_time") diff --git a/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py b/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py index 2f75c0826e5..1bdb53b7074 100644 --- a/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py +++ b/erpnext/patches/v11_0/reset_publish_in_hub_for_all_items.py @@ -1,7 +1,6 @@ - import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item") frappe.db.sql("""update `tabItem` set publish_in_hub = 0""") diff --git a/erpnext/patches/v11_0/set_default_email_template_in_hr.py b/erpnext/patches/v11_0/set_default_email_template_in_hr.py index a77dee93692..ee083ca4b80 100644 --- a/erpnext/patches/v11_0/set_default_email_template_in_hr.py +++ b/erpnext/patches/v11_0/set_default_email_template_in_hr.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ diff --git a/erpnext/patches/v11_0/set_department_for_doctypes.py b/erpnext/patches/v11_0/set_department_for_doctypes.py index a3ece7fc768..1e14b9ceb0f 100644 --- a/erpnext/patches/v11_0/set_department_for_doctypes.py +++ b/erpnext/patches/v11_0/set_department_for_doctypes.py @@ -1,24 +1,37 @@ - import frappe # Set department value based on employee value + def execute(): doctypes_to_update = { - 'hr': ['Appraisal', 'Leave Allocation', 'Expense Claim', 'Salary Slip', - 'Attendance', 'Training Feedback', 'Training Result Employee','Leave Application', - 'Employee Advance', 'Training Event Employee', 'Payroll Employee Detail'], - 'education': ['Instructor'], - 'projects': ['Activity Cost', 'Timesheet'], - 'setup': ['Sales Person'] + "hr": [ + "Appraisal", + "Leave Allocation", + "Expense Claim", + "Salary Slip", + "Attendance", + "Training Feedback", + "Training Result Employee", + "Leave Application", + "Employee Advance", + "Training Event Employee", + "Payroll Employee Detail", + ], + "education": ["Instructor"], + "projects": ["Activity Cost", "Timesheet"], + "setup": ["Sales Person"], } for module, doctypes in doctypes_to_update.items(): for doctype in doctypes: if frappe.db.table_exists(doctype): - frappe.reload_doc(module, 'doctype', frappe.scrub(doctype)) - frappe.db.sql(""" + frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) + frappe.db.sql( + """ update `tab%s` dt set department=(select department from `tabEmployee` where name=dt.employee) - """ % doctype) + """ + % doctype + ) diff --git a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py b/erpnext/patches/v11_0/set_missing_gst_hsn_code.py index 262ca2d61f9..d9356e758d7 100644 --- a/erpnext/patches/v11_0/set_missing_gst_hsn_code.py +++ b/erpnext/patches/v11_0/set_missing_gst_hsn_code.py @@ -1,4 +1,3 @@ - import frappe from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_html @@ -9,15 +8,24 @@ def execute(): if not company: return - doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", - "Supplier Quotation", "Purchase Order", "Purchase Receipt", "Purchase Invoice"] + doctypes = [ + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ] for dt in doctypes: date_field = "posting_date" if dt in ["Quotation", "Sales Order", "Supplier Quotation", "Purchase Order"]: date_field = "transaction_date" - transactions = frappe.db.sql(""" + transactions = frappe.db.sql( + """ select dt.name, dt_item.name as child_name from `tab{dt}` dt, `tab{dt} Item` dt_item where dt.name = dt_item.parent @@ -26,18 +34,28 @@ def execute(): and ifnull(dt_item.gst_hsn_code, '') = '' and ifnull(dt_item.item_code, '') != '' and dt.company in ({company}) - """.format(dt=dt, date_field=date_field, company=", ".join(['%s']*len(company))), tuple(company), as_dict=1) + """.format( + dt=dt, date_field=date_field, company=", ".join(["%s"] * len(company)) + ), + tuple(company), + as_dict=1, + ) if not transactions: continue transaction_rows_name = [d.child_name for d in transactions] - frappe.db.sql(""" + frappe.db.sql( + """ update `tab{dt} Item` dt_item set dt_item.gst_hsn_code = (select gst_hsn_code from tabItem where name=dt_item.item_code) where dt_item.name in ({rows_name}) - """.format(dt=dt, rows_name=", ".join(['%s']*len(transaction_rows_name))), tuple(transaction_rows_name)) + """.format( + dt=dt, rows_name=", ".join(["%s"] * len(transaction_rows_name)) + ), + tuple(transaction_rows_name), + ) parent = set([d.name for d in transactions]) for t in list(parent): diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py index 5ff9e4ab6fd..3ec9f8ab29a 100644 --- a/erpnext/patches/v11_0/set_salary_component_properties.py +++ b/erpnext/patches/v11_0/set_salary_component_properties.py @@ -1,17 +1,22 @@ - import frappe def execute(): - frappe.reload_doc('Payroll', 'doctype', 'salary_detail') - frappe.reload_doc('Payroll', 'doctype', 'salary_component') + frappe.reload_doc("Payroll", "doctype", "salary_detail") + frappe.reload_doc("Payroll", "doctype", "salary_component") frappe.db.sql("update `tabSalary Component` set is_tax_applicable=1 where type='Earning'") - frappe.db.sql("""update `tabSalary Component` set variable_based_on_taxable_salary=1 - where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""") + frappe.db.sql( + """update `tabSalary Component` set variable_based_on_taxable_salary=1 + where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""" + ) - frappe.db.sql("""update `tabSalary Detail` set is_tax_applicable=1 - where parentfield='earnings' and statistical_component=0""") - frappe.db.sql("""update `tabSalary Detail` set variable_based_on_taxable_salary=1 - where parentfield='deductions' and salary_component in ('TDS', 'Tax Deducted at Source')""") + frappe.db.sql( + """update `tabSalary Detail` set is_tax_applicable=1 + where parentfield='earnings' and statistical_component=0""" + ) + frappe.db.sql( + """update `tabSalary Detail` set variable_based_on_taxable_salary=1 + where parentfield='deductions' and salary_component in ('TDS', 'Tax Deducted at Source')""" + ) diff --git a/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py b/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py index f23018d0b4d..548a7cb158c 100644 --- a/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py +++ b/erpnext/patches/v11_0/set_update_field_and_value_in_workflow_state.py @@ -1,20 +1,21 @@ - import frappe from frappe.model.workflow import get_workflow_name def execute(): - for doctype in ['Expense Claim', 'Leave Application']: + for doctype in ["Expense Claim", "Leave Application"]: active_workflow = get_workflow_name(doctype) - if not active_workflow: continue + if not active_workflow: + continue - workflow_states = frappe.get_all('Workflow Document State', - filters=[['parent', '=', active_workflow]], - fields=['*']) + workflow_states = frappe.get_all( + "Workflow Document State", filters=[["parent", "=", active_workflow]], fields=["*"] + ) for state in workflow_states: - if state.update_field: continue - status_field = 'approval_status' if doctype=="Expense Claim" else 'status' - frappe.set_value('Workflow Document State', state.name, 'update_field', status_field) - frappe.set_value('Workflow Document State', state.name, 'update_value', state.state) + if state.update_field: + continue + status_field = "approval_status" if doctype == "Expense Claim" else "status" + frappe.set_value("Workflow Document State", state.name, "update_field", status_field) + frappe.set_value("Workflow Document State", state.name, "update_value", state.state) diff --git a/erpnext/patches/v11_0/set_user_permissions_for_department.py b/erpnext/patches/v11_0/set_user_permissions_for_department.py index cb38beb51c1..9b5cb243729 100644 --- a/erpnext/patches/v11_0/set_user_permissions_for_department.py +++ b/erpnext/patches/v11_0/set_user_permissions_for_department.py @@ -1,20 +1,25 @@ - import frappe def execute(): - user_permissions = frappe.db.sql("""select name, for_value from `tabUser Permission` - where allow='Department'""", as_dict=1) - for d in user_permissions: - user_permission = frappe.get_doc("User Permission", d.name) - for new_dept in frappe.db.sql("""select name from tabDepartment - where ifnull(company, '') != '' and department_name=%s""", d.for_value): - try: - new_user_permission = frappe.copy_doc(user_permission) - new_user_permission.for_value = new_dept[0] - new_user_permission.save() - except frappe.DuplicateEntryError: - pass + user_permissions = frappe.db.sql( + """select name, for_value from `tabUser Permission` + where allow='Department'""", + as_dict=1, + ) + for d in user_permissions: + user_permission = frappe.get_doc("User Permission", d.name) + for new_dept in frappe.db.sql( + """select name from tabDepartment + where ifnull(company, '') != '' and department_name=%s""", + d.for_value, + ): + try: + new_user_permission = frappe.copy_doc(user_permission) + new_user_permission.for_value = new_dept[0] + new_user_permission.save() + except frappe.DuplicateEntryError: + pass - frappe.reload_doc("hr", "doctype", "department") - frappe.db.sql("update tabDepartment set disabled=1 where ifnull(company, '') = ''") + frappe.reload_doc("hr", "doctype", "department") + frappe.db.sql("update tabDepartment set disabled=1 where ifnull(company, '') = ''") diff --git a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py index 8e2aa47785d..1327da981e1 100644 --- a/erpnext/patches/v11_0/skip_user_permission_check_for_department.py +++ b/erpnext/patches/v11_0/skip_user_permission_check_for_department.py @@ -1,24 +1,40 @@ - import frappe from frappe.desk.form.linked_with import get_linked_doctypes # Skips user permission check for doctypes where department link field was recently added # https://github.com/frappe/erpnext/pull/14121 + def execute(): doctypes_to_skip = [] - for doctype in ['Appraisal', 'Leave Allocation', 'Expense Claim', 'Instructor', 'Salary Slip', - 'Attendance', 'Training Feedback', 'Training Result Employee', - 'Leave Application', 'Employee Advance', 'Activity Cost', 'Training Event Employee', - 'Timesheet', 'Sales Person', 'Payroll Employee Detail']: - if frappe.db.exists('Custom Field', { 'dt': doctype, 'fieldname': 'department'}): continue + for doctype in [ + "Appraisal", + "Leave Allocation", + "Expense Claim", + "Instructor", + "Salary Slip", + "Attendance", + "Training Feedback", + "Training Result Employee", + "Leave Application", + "Employee Advance", + "Activity Cost", + "Training Event Employee", + "Timesheet", + "Sales Person", + "Payroll Employee Detail", + ]: + if frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": "department"}): + continue doctypes_to_skip.append(doctype) - frappe.reload_doctype('User Permission') + frappe.reload_doctype("User Permission") - user_permissions = frappe.get_all("User Permission", - filters=[['allow', '=', 'Department'], ['applicable_for', 'in', [None] + doctypes_to_skip]], - fields=['name', 'applicable_for']) + user_permissions = frappe.get_all( + "User Permission", + filters=[["allow", "=", "Department"], ["applicable_for", "in", [None] + doctypes_to_skip]], + fields=["name", "applicable_for"], + ) user_permissions_to_delete = [] new_user_permissions_list = [] @@ -38,24 +54,32 @@ def execute(): for doctype in applicable_for_doctypes: if doctype: # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes) - new_user_permissions_list.append(( - frappe.generate_hash("", 10), - user_permission.user, - user_permission.allow, - user_permission.for_value, - doctype, - 0 - )) + new_user_permissions_list.append( + ( + frappe.generate_hash("", 10), + user_permission.user, + user_permission.allow, + user_permission.for_value, + doctype, + 0, + ) + ) if new_user_permissions_list: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabUser Permission` (`name`, `user`, `allow`, `for_value`, `applicable_for`, `apply_to_all_doctypes`) - VALUES {}'''.format(', '.join(['%s'] * len(new_user_permissions_list))), # nosec - tuple(new_user_permissions_list) + VALUES {}""".format( + ", ".join(["%s"] * len(new_user_permissions_list)) + ), # nosec + tuple(new_user_permissions_list), ) if user_permissions_to_delete: - frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `name` IN ({})'.format( # nosec - ','.join(['%s'] * len(user_permissions_to_delete)) - ), tuple(user_permissions_to_delete)) + frappe.db.sql( + "DELETE FROM `tabUser Permission` WHERE `name` IN ({})".format( # nosec + ",".join(["%s"] * len(user_permissions_to_delete)) + ), + tuple(user_permissions_to_delete), + ) diff --git a/erpnext/patches/v11_0/uom_conversion_data.py b/erpnext/patches/v11_0/uom_conversion_data.py index 854f5223470..5dee0840eb8 100644 --- a/erpnext/patches/v11_0/uom_conversion_data.py +++ b/erpnext/patches/v11_0/uom_conversion_data.py @@ -1,4 +1,3 @@ - import frappe @@ -15,8 +14,8 @@ def execute(): # delete conversion data and insert again frappe.db.sql("delete from `tabUOM Conversion Factor`") try: - frappe.delete_doc('UOM', 'Hundredweight') - frappe.delete_doc('UOM', 'Pound Cubic Yard') + frappe.delete_doc("UOM", "Hundredweight") + frappe.delete_doc("UOM", "Pound Cubic Yard") except frappe.LinkExistsError: pass diff --git a/erpnext/patches/v11_0/update_account_type_in_party_type.py b/erpnext/patches/v11_0/update_account_type_in_party_type.py index c66cef042d9..e55f9f20cc8 100644 --- a/erpnext/patches/v11_0/update_account_type_in_party_type.py +++ b/erpnext/patches/v11_0/update_account_type_in_party_type.py @@ -6,9 +6,15 @@ import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'party_type') - party_types = {'Customer': 'Receivable', 'Supplier': 'Payable', - 'Employee': 'Payable', 'Member': 'Receivable', 'Shareholder': 'Payable', 'Student': 'Receivable'} + frappe.reload_doc("setup", "doctype", "party_type") + party_types = { + "Customer": "Receivable", + "Supplier": "Payable", + "Employee": "Payable", + "Member": "Receivable", + "Shareholder": "Payable", + "Student": "Receivable", + } for party_type, account_type in party_types.items(): - frappe.db.set_value('Party Type', party_type, 'account_type', account_type) + frappe.db.set_value("Party Type", party_type, "account_type", account_type) diff --git a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py index 3e36a4bb90d..a7351d27595 100644 --- a/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py +++ b/erpnext/patches/v11_0/update_allow_transfer_for_manufacture.py @@ -6,16 +6,22 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item') - frappe.db.sql(""" update `tabItem` set include_item_in_manufacturing = 1 - where ifnull(is_stock_item, 0) = 1""") + frappe.reload_doc("stock", "doctype", "item") + frappe.db.sql( + """ update `tabItem` set include_item_in_manufacturing = 1 + where ifnull(is_stock_item, 0) = 1""" + ) - for doctype in ['BOM Item', 'Work Order Item', 'BOM Explosion Item']: - frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) + for doctype in ["BOM Item", "Work Order Item", "BOM Explosion Item"]: + frappe.reload_doc("manufacturing", "doctype", frappe.scrub(doctype)) - frappe.db.sql(""" update `tab{0}` child, tabItem item + frappe.db.sql( + """ update `tab{0}` child, tabItem item set child.include_item_in_manufacturing = 1 where child.item_code = item.name and ifnull(item.is_stock_item, 0) = 1 - """.format(doctype)) + """.format( + doctype + ) + ) diff --git a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py index f3a2ac6a655..51ba706dcf0 100644 --- a/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py +++ b/erpnext/patches/v11_0/update_backflush_subcontract_rm_based_on_bom.py @@ -6,15 +6,19 @@ import frappe def execute(): - frappe.reload_doc('buying', 'doctype', 'buying_settings') - frappe.db.set_value('Buying Settings', None, 'backflush_raw_materials_of_subcontract_based_on', 'BOM') + frappe.reload_doc("buying", "doctype", "buying_settings") + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) - frappe.reload_doc('stock', 'doctype', 'stock_entry_detail') - frappe.db.sql(""" update `tabStock Entry Detail` as sed, + frappe.reload_doc("stock", "doctype", "stock_entry_detail") + frappe.db.sql( + """ update `tabStock Entry Detail` as sed, `tabStock Entry` as se, `tabPurchase Order Item Supplied` as pois set sed.subcontracted_item = pois.main_item_code where se.purpose = 'Send to Subcontractor' and sed.parent = se.name and pois.rm_item_code = sed.item_code and se.docstatus = 1 - and pois.parenttype = 'Purchase Order'""") + and pois.parenttype = 'Purchase Order'""" + ) diff --git a/erpnext/patches/v11_0/update_brand_in_item_price.py b/erpnext/patches/v11_0/update_brand_in_item_price.py index ce1df78083f..f4859ae1c77 100644 --- a/erpnext/patches/v11_0/update_brand_in_item_price.py +++ b/erpnext/patches/v11_0/update_brand_in_item_price.py @@ -6,11 +6,13 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item_price') + frappe.reload_doc("stock", "doctype", "item_price") - frappe.db.sql(""" update `tabItem Price`, `tabItem` + frappe.db.sql( + """ update `tabItem Price`, `tabItem` set `tabItem Price`.brand = `tabItem`.brand where `tabItem Price`.item_code = `tabItem`.name - and `tabItem`.brand is not null and `tabItem`.brand != ''""") + and `tabItem`.brand is not null and `tabItem`.brand != ''""" + ) diff --git a/erpnext/patches/v11_0/update_delivery_trip_status.py b/erpnext/patches/v11_0/update_delivery_trip_status.py index 35b95353b12..1badfab5025 100755 --- a/erpnext/patches/v11_0/update_delivery_trip_status.py +++ b/erpnext/patches/v11_0/update_delivery_trip_status.py @@ -6,18 +6,14 @@ import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'global_defaults', force=True) - frappe.reload_doc('stock', 'doctype', 'delivery_trip') - frappe.reload_doc('stock', 'doctype', 'delivery_stop', force=True) + frappe.reload_doc("setup", "doctype", "global_defaults", force=True) + frappe.reload_doc("stock", "doctype", "delivery_trip") + frappe.reload_doc("stock", "doctype", "delivery_stop", force=True) for trip in frappe.get_all("Delivery Trip"): trip_doc = frappe.get_doc("Delivery Trip", trip.name) - status = { - 0: "Draft", - 1: "Scheduled", - 2: "Cancelled" - }[trip_doc.docstatus] + status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[trip_doc.docstatus] if trip_doc.docstatus == 1: visited_stops = [stop.visited for stop in trip_doc.delivery_stops] diff --git a/erpnext/patches/v11_0/update_department_lft_rgt.py b/erpnext/patches/v11_0/update_department_lft_rgt.py index 4cb2dccbb27..bca5e9e8825 100644 --- a/erpnext/patches/v11_0/update_department_lft_rgt.py +++ b/erpnext/patches/v11_0/update_department_lft_rgt.py @@ -1,20 +1,21 @@ - import frappe from frappe import _ from frappe.utils.nestedset import rebuild_tree def execute(): - """ assign lft and rgt appropriately """ + """assign lft and rgt appropriately""" frappe.reload_doc("hr", "doctype", "department") - if not frappe.db.exists("Department", _('All Departments')): - frappe.get_doc({ - 'doctype': 'Department', - 'department_name': _('All Departments'), - 'is_group': 1 - }).insert(ignore_permissions=True, ignore_mandatory=True) + if not frappe.db.exists("Department", _("All Departments")): + frappe.get_doc( + {"doctype": "Department", "department_name": _("All Departments"), "is_group": 1} + ).insert(ignore_permissions=True, ignore_mandatory=True) - frappe.db.sql("""update `tabDepartment` set parent_department = '{0}' - where is_group = 0""".format(_('All Departments'))) + frappe.db.sql( + """update `tabDepartment` set parent_department = '{0}' + where is_group = 0""".format( + _("All Departments") + ) + ) rebuild_tree("Department", "parent_department") diff --git a/erpnext/patches/v11_0/update_hub_url.py b/erpnext/patches/v11_0/update_hub_url.py index 9150581c580..8eec3f3f318 100644 --- a/erpnext/patches/v11_0/update_hub_url.py +++ b/erpnext/patches/v11_0/update_hub_url.py @@ -1,7 +1,8 @@ - import frappe def execute(): - frappe.reload_doc('hub_node', 'doctype', 'Marketplace Settings') - frappe.db.set_value('Marketplace Settings', 'Marketplace Settings', 'marketplace_url', 'https://hubmarket.org') + frappe.reload_doc("hub_node", "doctype", "Marketplace Settings") + frappe.db.set_value( + "Marketplace Settings", "Marketplace Settings", "marketplace_url", "https://hubmarket.org" + ) diff --git a/erpnext/patches/v11_0/update_sales_partner_type.py b/erpnext/patches/v11_0/update_sales_partner_type.py index c7937e532be..2d37fd69b19 100644 --- a/erpnext/patches/v11_0/update_sales_partner_type.py +++ b/erpnext/patches/v11_0/update_sales_partner_type.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ @@ -6,25 +5,28 @@ from frappe import _ def execute(): from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type - frappe.reload_doc('selling', 'doctype', 'sales_partner_type') + frappe.reload_doc("selling", "doctype", "sales_partner_type") - frappe.local.lang = frappe.db.get_default("lang") or 'en' + frappe.local.lang = frappe.db.get_default("lang") or "en" for s in default_sales_partner_type: insert_sales_partner_type(_(s)) # get partner type in existing forms (customized) # and create a document if not created - for d in ['Sales Partner']: - partner_type = frappe.db.sql_list('select distinct partner_type from `tab{0}`'.format(d)) + for d in ["Sales Partner"]: + partner_type = frappe.db.sql_list("select distinct partner_type from `tab{0}`".format(d)) for s in partner_type: if s and s not in default_sales_partner_type: insert_sales_partner_type(s) # remove customization for partner type - for p in frappe.get_all('Property Setter', {'doc_type':d, 'field_name':'partner_type', 'property':'options'}): - frappe.delete_doc('Property Setter', p.name) + for p in frappe.get_all( + "Property Setter", {"doc_type": d, "field_name": "partner_type", "property": "options"} + ): + frappe.delete_doc("Property Setter", p.name) + def insert_sales_partner_type(s): - if not frappe.db.exists('Sales Partner Type', s): - frappe.get_doc(dict(doctype='Sales Partner Type', sales_partner_type=s)).insert() + if not frappe.db.exists("Sales Partner Type", s): + frappe.get_doc(dict(doctype="Sales Partner Type", sales_partner_type=s)).insert() diff --git a/erpnext/patches/v11_0/update_total_qty_field.py b/erpnext/patches/v11_0/update_total_qty_field.py index 09bb02f9593..09fcdb8723d 100644 --- a/erpnext/patches/v11_0/update_total_qty_field.py +++ b/erpnext/patches/v11_0/update_total_qty_field.py @@ -1,35 +1,47 @@ - import frappe def execute(): - frappe.reload_doc('buying', 'doctype', 'purchase_order') - frappe.reload_doc('buying', 'doctype', 'supplier_quotation') - frappe.reload_doc('selling', 'doctype', 'sales_order') - frappe.reload_doc('selling', 'doctype', 'quotation') - frappe.reload_doc('stock', 'doctype', 'delivery_note') - frappe.reload_doc('stock', 'doctype', 'purchase_receipt') - frappe.reload_doc('accounts', 'doctype', 'sales_invoice') - frappe.reload_doc('accounts', 'doctype', 'purchase_invoice') + frappe.reload_doc("buying", "doctype", "purchase_order") + frappe.reload_doc("buying", "doctype", "supplier_quotation") + frappe.reload_doc("selling", "doctype", "sales_order") + frappe.reload_doc("selling", "doctype", "quotation") + frappe.reload_doc("stock", "doctype", "delivery_note") + frappe.reload_doc("stock", "doctype", "purchase_receipt") + frappe.reload_doc("accounts", "doctype", "sales_invoice") + frappe.reload_doc("accounts", "doctype", "purchase_invoice") - doctypes = ["Sales Order", "Sales Invoice", "Delivery Note",\ - "Purchase Order", "Purchase Invoice", "Purchase Receipt", "Quotation", "Supplier Quotation"] + doctypes = [ + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + "Quotation", + "Supplier Quotation", + ] for doctype in doctypes: - total_qty = frappe.db.sql(''' + total_qty = frappe.db.sql( + """ SELECT parent, SUM(qty) as qty FROM `tab{0} Item` where parenttype = '{0}' GROUP BY parent - '''.format(doctype), as_dict = True) + """.format( + doctype + ), + as_dict=True, + ) # Query to update total_qty might become too big, Update in batches # batch_size is chosen arbitrarily, Don't try too hard to reason about it batch_size = 100000 for i in range(0, len(total_qty), batch_size): - batch_transactions = total_qty[i:i + batch_size] + batch_transactions = total_qty[i : i + batch_size] # UPDATE with CASE for some reason cannot use PRIMARY INDEX, # causing all rows to be examined, leading to a very slow update @@ -43,7 +55,11 @@ def execute(): for d in batch_transactions: values.append("({0}, {1})".format(frappe.db.escape(d.parent), d.qty)) conditions = ",".join(values) - frappe.db.sql(""" + frappe.db.sql( + """ INSERT INTO `tab{}` (name, total_qty) VALUES {} ON DUPLICATE KEY UPDATE name = VALUES(name), total_qty = VALUES(total_qty) - """.format(doctype, conditions)) + """.format( + doctype, conditions + ) + ) diff --git a/erpnext/patches/v11_1/delete_bom_browser.py b/erpnext/patches/v11_1/delete_bom_browser.py index 9b5c169717a..09ee1695b9f 100644 --- a/erpnext/patches/v11_1/delete_bom_browser.py +++ b/erpnext/patches/v11_1/delete_bom_browser.py @@ -6,4 +6,4 @@ import frappe def execute(): - frappe.delete_doc_if_exists('Page', 'bom-browser') + frappe.delete_doc_if_exists("Page", "bom-browser") diff --git a/erpnext/patches/v11_1/make_job_card_time_logs.py b/erpnext/patches/v11_1/make_job_card_time_logs.py index 100cd64f8fe..beb2c4e5341 100644 --- a/erpnext/patches/v11_1/make_job_card_time_logs.py +++ b/erpnext/patches/v11_1/make_job_card_time_logs.py @@ -6,25 +6,45 @@ import frappe def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'job_card_time_log') + frappe.reload_doc("manufacturing", "doctype", "job_card_time_log") - if (frappe.db.table_exists("Job Card") - and frappe.get_meta("Job Card").has_field("actual_start_date")): - time_logs = [] - for d in frappe.get_all('Job Card', - fields = ["actual_start_date", "actual_end_date", "time_in_mins", "name", "for_quantity"], - filters = {'docstatus': ("<", 2)}): - if d.actual_start_date: - time_logs.append([d.actual_start_date, d.actual_end_date, d.time_in_mins, - d.for_quantity, d.name, 'Job Card', 'time_logs', frappe.generate_hash("", 10)]) + if frappe.db.table_exists("Job Card") and frappe.get_meta("Job Card").has_field( + "actual_start_date" + ): + time_logs = [] + for d in frappe.get_all( + "Job Card", + fields=["actual_start_date", "actual_end_date", "time_in_mins", "name", "for_quantity"], + filters={"docstatus": ("<", 2)}, + ): + if d.actual_start_date: + time_logs.append( + [ + d.actual_start_date, + d.actual_end_date, + d.time_in_mins, + d.for_quantity, + d.name, + "Job Card", + "time_logs", + frappe.generate_hash("", 10), + ] + ) - if time_logs: - frappe.db.sql(""" INSERT INTO + if time_logs: + frappe.db.sql( + """ INSERT INTO `tabJob Card Time Log` (from_time, to_time, time_in_mins, completed_qty, parent, parenttype, parentfield, name) values {values} - """.format(values = ','.join(['%s'] * len(time_logs))), tuple(time_logs)) + """.format( + values=",".join(["%s"] * len(time_logs)) + ), + tuple(time_logs), + ) - frappe.reload_doc('manufacturing', 'doctype', 'job_card') - frappe.db.sql(""" update `tabJob Card` set total_completed_qty = for_quantity, - total_time_in_mins = time_in_mins where docstatus < 2 """) + frappe.reload_doc("manufacturing", "doctype", "job_card") + frappe.db.sql( + """ update `tabJob Card` set total_completed_qty = for_quantity, + total_time_in_mins = time_in_mins where docstatus < 2 """ + ) diff --git a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py index d292d7ae432..b681f25d84e 100644 --- a/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py +++ b/erpnext/patches/v11_1/move_customer_lead_to_dynamic_column.py @@ -8,8 +8,14 @@ import frappe def execute(): frappe.reload_doctype("Quotation") frappe.db.sql(""" UPDATE `tabQuotation` set party_name = lead WHERE quotation_to = 'Lead' """) - frappe.db.sql(""" UPDATE `tabQuotation` set party_name = customer WHERE quotation_to = 'Customer' """) + frappe.db.sql( + """ UPDATE `tabQuotation` set party_name = customer WHERE quotation_to = 'Customer' """ + ) frappe.reload_doctype("Opportunity") - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = lead WHERE opportunity_from = 'Lead' """) - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = customer WHERE opportunity_from = 'Customer' """) + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = lead WHERE opportunity_from = 'Lead' """ + ) + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = customer WHERE opportunity_from = 'Customer' """ + ) diff --git a/erpnext/patches/v11_1/renamed_delayed_item_report.py b/erpnext/patches/v11_1/renamed_delayed_item_report.py index c160b79d0e9..86247815e29 100644 --- a/erpnext/patches/v11_1/renamed_delayed_item_report.py +++ b/erpnext/patches/v11_1/renamed_delayed_item_report.py @@ -6,6 +6,6 @@ import frappe def execute(): - for report in ["Delayed Order Item Summary", "Delayed Order Summary"]: - if frappe.db.exists("Report", report): - frappe.delete_doc("Report", report) + for report in ["Delayed Order Item Summary", "Delayed Order Summary"]: + if frappe.db.exists("Report", report): + frappe.delete_doc("Report", report) diff --git a/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py b/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py index 672b7628bb4..39aa6dd8e8e 100644 --- a/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py +++ b/erpnext/patches/v11_1/set_default_action_for_quality_inspection.py @@ -6,11 +6,13 @@ import frappe def execute(): - stock_settings = frappe.get_doc('Stock Settings') - if stock_settings.default_warehouse and not frappe.db.exists("Warehouse", stock_settings.default_warehouse): - stock_settings.default_warehouse = None - if stock_settings.stock_uom and not frappe.db.exists("UOM", stock_settings.stock_uom): - stock_settings.stock_uom = None - stock_settings.flags.ignore_mandatory = True - stock_settings.action_if_quality_inspection_is_not_submitted = "Stop" - stock_settings.save() + stock_settings = frappe.get_doc("Stock Settings") + if stock_settings.default_warehouse and not frappe.db.exists( + "Warehouse", stock_settings.default_warehouse + ): + stock_settings.default_warehouse = None + if stock_settings.stock_uom and not frappe.db.exists("UOM", stock_settings.stock_uom): + stock_settings.stock_uom = None + stock_settings.flags.ignore_mandatory = True + stock_settings.action_if_quality_inspection_is_not_submitted = "Stop" + stock_settings.save() diff --git a/erpnext/patches/v11_1/set_missing_opportunity_from.py b/erpnext/patches/v11_1/set_missing_opportunity_from.py index 7a041919a15..ae5f6200145 100644 --- a/erpnext/patches/v11_1/set_missing_opportunity_from.py +++ b/erpnext/patches/v11_1/set_missing_opportunity_from.py @@ -1,4 +1,3 @@ - import frappe @@ -6,13 +5,23 @@ def execute(): frappe.reload_doctype("Opportunity") if frappe.db.has_column("Opportunity", "enquiry_from"): - frappe.db.sql(""" UPDATE `tabOpportunity` set opportunity_from = enquiry_from - where ifnull(opportunity_from, '') = '' and ifnull(enquiry_from, '') != ''""") + frappe.db.sql( + """ UPDATE `tabOpportunity` set opportunity_from = enquiry_from + where ifnull(opportunity_from, '') = '' and ifnull(enquiry_from, '') != ''""" + ) - if frappe.db.has_column("Opportunity", "lead") and frappe.db.has_column("Opportunity", "enquiry_from"): - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = lead - where enquiry_from = 'Lead' and ifnull(party_name, '') = '' and ifnull(lead, '') != ''""") + if frappe.db.has_column("Opportunity", "lead") and frappe.db.has_column( + "Opportunity", "enquiry_from" + ): + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = lead + where enquiry_from = 'Lead' and ifnull(party_name, '') = '' and ifnull(lead, '') != ''""" + ) - if frappe.db.has_column("Opportunity", "customer") and frappe.db.has_column("Opportunity", "enquiry_from"): - frappe.db.sql(""" UPDATE `tabOpportunity` set party_name = customer - where enquiry_from = 'Customer' and ifnull(party_name, '') = '' and ifnull(customer, '') != ''""") + if frappe.db.has_column("Opportunity", "customer") and frappe.db.has_column( + "Opportunity", "enquiry_from" + ): + frappe.db.sql( + """ UPDATE `tabOpportunity` set party_name = customer + where enquiry_from = 'Customer' and ifnull(party_name, '') = '' and ifnull(customer, '') != ''""" + ) diff --git a/erpnext/patches/v11_1/set_missing_title_for_quotation.py b/erpnext/patches/v11_1/set_missing_title_for_quotation.py index 93d9f0e7d89..6e7e2c9d8bf 100644 --- a/erpnext/patches/v11_1/set_missing_title_for_quotation.py +++ b/erpnext/patches/v11_1/set_missing_title_for_quotation.py @@ -4,7 +4,8 @@ import frappe def execute(): frappe.reload_doctype("Quotation") # update customer_name from Customer document if quotation_to is set to Customer - frappe.db.sql(''' + frappe.db.sql( + """ update tabQuotation, tabCustomer set tabQuotation.customer_name = tabCustomer.customer_name, @@ -13,11 +14,13 @@ def execute(): tabQuotation.customer_name is null and tabQuotation.party_name = tabCustomer.name and tabQuotation.quotation_to = 'Customer' - ''') + """ + ) # update customer_name from Lead document if quotation_to is set to Lead - frappe.db.sql(''' + frappe.db.sql( + """ update tabQuotation, tabLead set tabQuotation.customer_name = case when ifnull(tabLead.company_name, '') != '' then tabLead.company_name else tabLead.lead_name end, @@ -26,4 +29,5 @@ def execute(): tabQuotation.customer_name is null and tabQuotation.party_name = tabLead.name and tabQuotation.quotation_to = 'Lead' - ''') + """ + ) diff --git a/erpnext/patches/v11_1/set_salary_details_submittable.py b/erpnext/patches/v11_1/set_salary_details_submittable.py index 8ad8ff8c2ba..e5ecce6486a 100644 --- a/erpnext/patches/v11_1/set_salary_details_submittable.py +++ b/erpnext/patches/v11_1/set_salary_details_submittable.py @@ -1,10 +1,11 @@ - import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update `tabSalary Structure` ss, `tabSalary Detail` sd set sd.docstatus=1 where ss.name=sd.parent and ss.docstatus=1 and sd.parenttype='Salary Structure' - """) + """ + ) diff --git a/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py index 2da1ecbda26..6e2edfea878 100644 --- a/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py +++ b/erpnext/patches/v11_1/set_status_for_material_request_type_manufacture.py @@ -1,10 +1,11 @@ - import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update `tabMaterial Request` set status='Manufactured' where docstatus=1 and material_request_type='Manufacture' and per_ordered=100 and status != 'Stopped' - """) + """ + ) diff --git a/erpnext/patches/v11_1/set_variant_based_on.py b/erpnext/patches/v11_1/set_variant_based_on.py index 2e06e63a8aa..1d57527b1f8 100644 --- a/erpnext/patches/v11_1/set_variant_based_on.py +++ b/erpnext/patches/v11_1/set_variant_based_on.py @@ -6,7 +6,9 @@ import frappe def execute(): - frappe.db.sql("""update tabItem set variant_based_on = 'Item Attribute' + frappe.db.sql( + """update tabItem set variant_based_on = 'Item Attribute' where ifnull(variant_based_on, '') = '' and (has_variants=1 or ifnull(variant_of, '') != '') - """) + """ + ) diff --git a/erpnext/patches/v11_1/setup_guardian_role.py b/erpnext/patches/v11_1/setup_guardian_role.py index 385bc209fa2..2b25e132540 100644 --- a/erpnext/patches/v11_1/setup_guardian_role.py +++ b/erpnext/patches/v11_1/setup_guardian_role.py @@ -1,13 +1,9 @@ - import frappe def execute(): - if 'Education' in frappe.get_active_domains() and not frappe.db.exists("Role", "Guardian"): + if "Education" in frappe.get_active_domains() and not frappe.db.exists("Role", "Guardian"): doc = frappe.new_doc("Role") - doc.update({ - "role_name": "Guardian", - "desk_access": 0 - }) + doc.update({"role_name": "Guardian", "desk_access": 0}) doc.insert(ignore_permissions=True) diff --git a/erpnext/patches/v11_1/update_bank_transaction_status.py b/erpnext/patches/v11_1/update_bank_transaction_status.py index 9b8be3de1b0..0615b04a57f 100644 --- a/erpnext/patches/v11_1/update_bank_transaction_status.py +++ b/erpnext/patches/v11_1/update_bank_transaction_status.py @@ -6,22 +6,26 @@ import frappe def execute(): - frappe.reload_doc("accounts", "doctype", "bank_transaction") + frappe.reload_doc("accounts", "doctype", "bank_transaction") - bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns() + bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns() - if 'debit' in bank_transaction_fields: - frappe.db.sql(""" UPDATE `tabBank Transaction` + if "debit" in bank_transaction_fields: + frappe.db.sql( + """ UPDATE `tabBank Transaction` SET status = 'Reconciled' WHERE status = 'Settled' and (debit = allocated_amount or credit = allocated_amount) and ifnull(allocated_amount, 0) > 0 - """) + """ + ) - elif 'deposit' in bank_transaction_fields: - frappe.db.sql(""" UPDATE `tabBank Transaction` + elif "deposit" in bank_transaction_fields: + frappe.db.sql( + """ UPDATE `tabBank Transaction` SET status = 'Reconciled' WHERE status = 'Settled' and (deposit = allocated_amount or withdrawal = allocated_amount) and ifnull(allocated_amount, 0) > 0 - """) + """ + ) diff --git a/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py b/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py index 902df201a42..16e11ed7be3 100644 --- a/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py +++ b/erpnext/patches/v11_1/update_default_supplier_in_item_defaults.py @@ -6,21 +6,23 @@ import frappe def execute(): - ''' - default supplier was not set in the item defaults for multi company instance, - this patch will set the default supplier + """ + default supplier was not set in the item defaults for multi company instance, + this patch will set the default supplier - ''' - if not frappe.db.has_column('Item', 'default_supplier'): + """ + if not frappe.db.has_column("Item", "default_supplier"): return - frappe.reload_doc('stock', 'doctype', 'item_default') - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item_default") + frappe.reload_doc("stock", "doctype", "item") companies = frappe.get_all("Company") if len(companies) > 1: - frappe.db.sql(""" UPDATE `tabItem Default`, `tabItem` + frappe.db.sql( + """ UPDATE `tabItem Default`, `tabItem` SET `tabItem Default`.default_supplier = `tabItem`.default_supplier WHERE `tabItem Default`.parent = `tabItem`.name and `tabItem Default`.default_supplier is null - and `tabItem`.default_supplier is not null and `tabItem`.default_supplier != '' """) + and `tabItem`.default_supplier is not null and `tabItem`.default_supplier != '' """ + ) diff --git a/erpnext/patches/v11_1/woocommerce_set_creation_user.py b/erpnext/patches/v11_1/woocommerce_set_creation_user.py index 1de25bb739c..e2d9e3ef38a 100644 --- a/erpnext/patches/v11_1/woocommerce_set_creation_user.py +++ b/erpnext/patches/v11_1/woocommerce_set_creation_user.py @@ -1,10 +1,9 @@ - import frappe from frappe.utils import cint def execute(): - frappe.reload_doc("erpnext_integrations", "doctype","woocommerce_settings") + frappe.reload_doc("erpnext_integrations", "doctype", "woocommerce_settings") doc = frappe.get_doc("Woocommerce Settings") if cint(doc.enable_sync): diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py index 53d363f233f..1778a45049b 100644 --- a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py +++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py @@ -1,21 +1,23 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return frappe.reload_doc("regional", "doctype", "e_invoice_user") - if not frappe.db.count('E Invoice User'): + if not frappe.db.count("E Invoice User"): return - for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']): - company_name = frappe.db.sql(""" + for creds in frappe.db.get_all("E Invoice User", fields=["name", "gstin"]): + company_name = frappe.db.sql( + """ select dl.link_name from `tabAddress` a, `tabDynamic Link` dl where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' - """, (creds.get('gstin'))) + """, + (creds.get("gstin")), + ) if company_name and len(company_name) > 0: - frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0]) + frappe.db.set_value("E Invoice User", creds.get("name"), "company", company_name[0][0]) diff --git a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py index 80187d834a4..284b616bbda 100644 --- a/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py +++ b/erpnext/patches/v12_0/add_default_buying_selling_terms_in_company.py @@ -8,12 +8,16 @@ from frappe.model.utils.rename_field import rename_field def execute(): frappe.reload_doc("setup", "doctype", "company") - if frappe.db.has_column('Company', 'default_terms'): - rename_field('Company', "default_terms", "default_selling_terms") + if frappe.db.has_column("Company", "default_terms"): + rename_field("Company", "default_terms", "default_selling_terms") - for company in frappe.get_all("Company", ["name", "default_selling_terms", "default_buying_terms"]): + for company in frappe.get_all( + "Company", ["name", "default_selling_terms", "default_buying_terms"] + ): if company.default_selling_terms and not company.default_buying_terms: - frappe.db.set_value("Company", company.name, "default_buying_terms", company.default_selling_terms) + frappe.db.set_value( + "Company", company.name, "default_buying_terms", company.default_selling_terms + ) frappe.reload_doc("setup", "doctype", "terms_and_conditions") frappe.db.sql("update `tabTerms and Conditions` set selling=1, buying=1, hr=1") diff --git a/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py index 41264819ef3..a98976a968c 100644 --- a/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py +++ b/erpnext/patches/v12_0/add_document_type_field_for_italy_einvoicing.py @@ -1,18 +1,21 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'Italy'}) + company = frappe.get_all("Company", filters={"country": "Italy"}) if not company: return custom_fields = { - 'Sales Invoice': [ - dict(fieldname='type_of_document', label='Type of Document', - fieldtype='Select', insert_after='customer_fiscal_code', - options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'), + "Sales Invoice": [ + dict( + fieldname="type_of_document", + label="Type of Document", + fieldtype="Select", + insert_after="customer_fiscal_code", + options="\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27", + ), ] } diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py index ae908573236..0948e822974 100644 --- a/erpnext/patches/v12_0/add_einvoice_status_field.py +++ b/erpnext/patches/v12_0/add_einvoice_status_field.py @@ -1,4 +1,3 @@ - import json import frappe @@ -6,66 +5,154 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return # move hidden einvoice fields to a different section custom_fields = { - 'Sales Invoice': [ - dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', - print_hide=1, hidden=1), - - dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', - no_copy=1, print_hide=1), - - dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - - dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', - no_copy=1, print_hide=1), - - dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', - options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', - hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) + "Sales Invoice": [ + dict( + fieldname="einvoice_section", + label="E-Invoice Fields", + fieldtype="Section Break", + insert_after="gst_vehicle_type", + print_hide=1, + hidden=1, + ), + dict( + fieldname="ack_no", + label="Ack. No.", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="einvoice_section", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="ack_date", + label="Ack. Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_no", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="irn_cancel_date", + label="Cancel Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_date", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="signed_einvoice", + label="Signed E-Invoice", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="irn_cancel_date", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="signed_qr_code", + label="Signed QRCode", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="signed_einvoice", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="qrcode_image", + label="QRCode", + fieldtype="Attach Image", + hidden=1, + insert_after="signed_qr_code", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="einvoice_status", + label="E-Invoice Status", + fieldtype="Select", + insert_after="qrcode_image", + options="\nPending\nGenerated\nCancelled\nFailed", + default=None, + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="failure_description", + label="E-Invoice Failure Description", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="einvoice_status", + no_copy=1, + print_hide=1, + read_only=1, + ), ] } create_custom_fields(custom_fields, update=True) - if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'): - frappe.db.sql(''' + if frappe.db.exists("E Invoice Settings") and frappe.db.get_single_value( + "E Invoice Settings", "enable" + ): + frappe.db.sql( + """ UPDATE `tabSales Invoice` SET einvoice_status = 'Pending' WHERE posting_date >= '2021-04-01' AND ifnull(irn, '') = '' AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '') AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export') - ''') + """ + ) # set appropriate statuses - frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated' - WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''') + frappe.db.sql( + """UPDATE `tabSales Invoice` SET einvoice_status = 'Generated' + WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0""" + ) - frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled' - WHERE ifnull(irn_cancelled, 0) = 1''') + frappe.db.sql( + """UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled' + WHERE ifnull(irn_cancelled, 0) = 1""" + ) # set correct acknowledgement in e-invoices - einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice']) + einvoices = frappe.get_all("Sales Invoice", {"irn": ["is", "set"]}, ["name", "signed_einvoice"]) if einvoices: for inv in einvoices: - signed_einvoice = inv.get('signed_einvoice') + signed_einvoice = inv.get("signed_einvoice") if signed_einvoice: signed_einvoice = json.loads(signed_einvoice) - frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False) - frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False) + frappe.db.set_value( + "Sales Invoice", + inv.get("name"), + "ack_no", + signed_einvoice.get("AckNo"), + update_modified=False, + ) + frappe.db.set_value( + "Sales Invoice", + inv.get("name"), + "ack_date", + signed_einvoice.get("AckDt"), + update_modified=False, + ) diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py index b5d8493ae0b..d15f4d1327e 100644 --- a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py +++ b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py @@ -1,19 +1,18 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - if frappe.db.exists('Report', 'E-Invoice Summary') and \ - not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')): - frappe.get_doc(dict( - doctype='Custom Role', - report='E-Invoice Summary', - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if frappe.db.exists("Report", "E-Invoice Summary") and not frappe.db.get_value( + "Custom Role", dict(report="E-Invoice Summary") + ): + frappe.get_doc( + dict( + doctype="Custom Role", + report="E-Invoice Summary", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() diff --git a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py index 973da895623..db9fa247ed5 100644 --- a/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py +++ b/erpnext/patches/v12_0/add_eway_bill_in_delivery_note.py @@ -3,18 +3,21 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) - if not company: - return + if not company: + return - create_custom_field('Delivery Note', { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', - 'allow_on_submit': 1, - 'insert_after': 'customer_name_in_arabic', - 'translatable': 0, - 'owner': 'Administrator' - }) + create_custom_field( + "Delivery Note", + { + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:(doc.docstatus === 1)", + "allow_on_submit": 1, + "insert_after": "customer_name_in_arabic", + "translatable": 0, + "owner": "Administrator", + }, + ) diff --git a/erpnext/patches/v12_0/add_ewaybill_validity_field.py b/erpnext/patches/v12_0/add_ewaybill_validity_field.py index 1c8a68a7517..19b96159ef7 100644 --- a/erpnext/patches/v12_0/add_ewaybill_validity_field.py +++ b/erpnext/patches/v12_0/add_ewaybill_validity_field.py @@ -1,17 +1,25 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_fields = { - 'Sales Invoice': [ - dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, - depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill') + "Sales Invoice": [ + dict( + fieldname="eway_bill_validity", + label="E-Way Bill Validity", + fieldtype="Data", + no_copy=1, + print_hide=1, + depends_on="ewaybill", + read_only=1, + allow_on_submit=1, + insert_after="ewaybill", + ) ] } create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py index df9bbea344a..b14ffd2d349 100644 --- a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py +++ b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py @@ -1,4 +1,3 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields @@ -6,37 +5,38 @@ from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return make_custom_fields() - frappe.reload_doctype('Tax Category') - frappe.reload_doctype('Sales Taxes and Charges Template') - frappe.reload_doctype('Purchase Taxes and Charges Template') + frappe.reload_doctype("Tax Category") + frappe.reload_doctype("Sales Taxes and Charges Template") + frappe.reload_doctype("Purchase Taxes and Charges Template") # Create tax category with inter state field checked - tax_category = frappe.db.get_value('Tax Category', {'name': 'OUT OF STATE'}, 'name') + tax_category = frappe.db.get_value("Tax Category", {"name": "OUT OF STATE"}, "name") if not tax_category: - inter_state_category = frappe.get_doc({ - 'doctype': 'Tax Category', - 'title': 'OUT OF STATE', - 'is_inter_state': 1 - }).insert() + inter_state_category = frappe.get_doc( + {"doctype": "Tax Category", "title": "OUT OF STATE", "is_inter_state": 1} + ).insert() tax_category = inter_state_category.name - for doctype in ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template'): - if not frappe.get_meta(doctype).has_field('is_inter_state'): continue + for doctype in ("Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"): + if not frappe.get_meta(doctype).has_field("is_inter_state"): + continue - template = frappe.db.get_value(doctype, {'is_inter_state': 1, 'disabled': 0}, ['name']) + template = frappe.db.get_value(doctype, {"is_inter_state": 1, "disabled": 0}, ["name"]) if template: - frappe.db.set_value(doctype, template, 'tax_category', tax_category) + frappe.db.set_value(doctype, template, "tax_category", tax_category) - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabCustom Field` WHERE fieldname = 'is_inter_state' AND dt IN ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template') - """) + """ + ) diff --git a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py index 77b63487ca7..5944889b63d 100644 --- a/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py +++ b/erpnext/patches/v12_0/add_gst_category_in_delivery_note.py @@ -1,19 +1,24 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_fields = { - 'Delivery Note': [ - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1), + "Delivery Note": [ + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_vehicle_type", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + ), ] } diff --git a/erpnext/patches/v12_0/add_item_name_in_work_orders.py b/erpnext/patches/v12_0/add_item_name_in_work_orders.py index d765b93d218..0e5cd4eed52 100644 --- a/erpnext/patches/v12_0/add_item_name_in_work_orders.py +++ b/erpnext/patches/v12_0/add_item_name_in_work_orders.py @@ -4,11 +4,13 @@ import frappe def execute(): frappe.reload_doc("manufacturing", "doctype", "work_order") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabWork Order` wo JOIN `tabItem` item ON wo.production_item = item.item_code SET wo.item_name = item.item_name - """) + """ + ) frappe.db.commit() diff --git a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py index 1d77e5ad3d1..24748b2cc65 100644 --- a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py +++ b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py @@ -3,12 +3,12 @@ from frappe.permissions import add_permission, update_permission_property def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'Lower Deduction Certificate') + frappe.reload_doc("regional", "doctype", "Lower Deduction Certificate") - add_permission('Lower Deduction Certificate', 'Accounts Manager', 0) - update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'write', 1) - update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1) + add_permission("Lower Deduction Certificate", "Accounts Manager", 0) + update_permission_property("Lower Deduction Certificate", "Accounts Manager", 0, "write", 1) + update_permission_property("Lower Deduction Certificate", "Accounts Manager", 0, "create", 1) diff --git a/erpnext/patches/v12_0/add_state_code_for_ladakh.py b/erpnext/patches/v12_0/add_state_code_for_ladakh.py index 6722b7bcef0..4b06eb1921e 100644 --- a/erpnext/patches/v12_0/add_state_code_for_ladakh.py +++ b/erpnext/patches/v12_0/add_state_code_for_ladakh.py @@ -5,15 +5,15 @@ from erpnext.regional.india import states def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - custom_fields = ['Address-gst_state', 'Tax Category-gst_state'] + custom_fields = ["Address-gst_state", "Tax Category-gst_state"] # Update options in gst_state custom fields for field in custom_fields: - if frappe.db.exists('Custom Field', field): - gst_state_field = frappe.get_doc('Custom Field', field) - gst_state_field.options = '\n'.join(states) + if frappe.db.exists("Custom Field", field): + gst_state_field = frappe.get_doc("Custom Field", field) + gst_state_field.options = "\n".join(states) gst_state_field.save() diff --git a/erpnext/patches/v12_0/add_taxjar_integration_field.py b/erpnext/patches/v12_0/add_taxjar_integration_field.py index fbaf6f83a7b..9217384b813 100644 --- a/erpnext/patches/v12_0/add_taxjar_integration_field.py +++ b/erpnext/patches/v12_0/add_taxjar_integration_field.py @@ -1,11 +1,10 @@ - import frappe from erpnext.regional.united_states.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters={'country': 'United States'}) + company = frappe.get_all("Company", filters={"country": "United States"}) if not company: return diff --git a/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py b/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py index c3a422c49e5..7f044c8d479 100644 --- a/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py +++ b/erpnext/patches/v12_0/add_variant_of_in_item_attribute_table.py @@ -2,9 +2,11 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'item_variant_attribute') - frappe.db.sql(''' + frappe.reload_doc("stock", "doctype", "item_variant_attribute") + frappe.db.sql( + """ UPDATE `tabItem Variant Attribute` t1 INNER JOIN `tabItem` t2 ON t2.name = t1.parent SET t1.variant_of = t2.variant_of - ''') + """ + ) diff --git a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py index e0ed9d8c147..744ea1ccd8a 100644 --- a/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py +++ b/erpnext/patches/v12_0/create_accounting_dimensions_in_missing_doctypes.py @@ -1,14 +1,16 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): - frappe.reload_doc('accounts', 'doctype', 'accounting_dimension') + frappe.reload_doc("accounts", "doctype", "accounting_dimension") - accounting_dimensions = frappe.db.sql("""select fieldname, label, document_type, disabled from - `tabAccounting Dimension`""", as_dict=1) + accounting_dimensions = frappe.db.sql( + """select fieldname, label, document_type, disabled from + `tabAccounting Dimension`""", + as_dict=1, + ) if not accounting_dimensions: return @@ -16,13 +18,19 @@ def execute(): count = 1 for d in accounting_dimensions: - if count%2 == 0: - insert_after_field = 'dimension_col_break' + if count % 2 == 0: + insert_after_field = "dimension_col_break" else: - insert_after_field = 'accounting_dimensions_section' + insert_after_field = "accounting_dimensions_section" - for doctype in ["Subscription Plan", "Subscription", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", - "Expense Claim Detail", "Expense Taxes and Charges"]: + for doctype in [ + "Subscription Plan", + "Subscription", + "Opening Invoice Creation Tool", + "Opening Invoice Creation Tool Item", + "Expense Claim Detail", + "Expense Taxes and Charges", + ]: field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) @@ -34,7 +42,7 @@ def execute(): "label": d.label, "fieldtype": "Link", "options": d.document_type, - "insert_after": insert_after_field + "insert_after": insert_after_field, } create_custom_field(doctype, df) diff --git a/erpnext/patches/v12_0/create_default_energy_point_rules.py b/erpnext/patches/v12_0/create_default_energy_point_rules.py index 35eaca7f400..da200de9b77 100644 --- a/erpnext/patches/v12_0/create_default_energy_point_rules.py +++ b/erpnext/patches/v12_0/create_default_energy_point_rules.py @@ -4,5 +4,5 @@ from erpnext.setup.install import create_default_energy_point_rules def execute(): - frappe.reload_doc('social', 'doctype', 'energy_point_rule') + frappe.reload_doc("social", "doctype", "energy_point_rule") create_default_energy_point_rules() diff --git a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py index 0f5e07b5e53..80e9047cf06 100644 --- a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py +++ b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py @@ -1,4 +1,3 @@ - import frappe from erpnext.regional.united_states.setup import make_custom_fields @@ -6,12 +5,12 @@ from erpnext.regional.united_states.setup import make_custom_fields def execute(): - frappe.reload_doc('accounts', 'doctype', 'allowed_to_transact_with', force=True) - frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail', force=True) - frappe.reload_doc('crm', 'doctype', 'lost_reason_detail', force=True) - frappe.reload_doc('setup', 'doctype', 'quotation_lost_reason_detail', force=True) + frappe.reload_doc("accounts", "doctype", "allowed_to_transact_with", force=True) + frappe.reload_doc("accounts", "doctype", "pricing_rule_detail", force=True) + frappe.reload_doc("crm", "doctype", "lost_reason_detail", force=True) + frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail", force=True) - company = frappe.get_all('Company', filters = {'country': 'United States'}) + company = frappe.get_all("Company", filters={"country": "United States"}) if not company: return diff --git a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py index 3dc1115a20a..906baf22ccb 100644 --- a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py +++ b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -7,44 +6,76 @@ from erpnext.regional.india.utils import get_gst_accounts def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "India"}, fields=["name"]) if not company: return frappe.reload_doc("regional", "doctype", "gst_settings") frappe.reload_doc("accounts", "doctype", "gst_account") - journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] - make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') + journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + [ + "Reversal Of ITC" + ] + make_property_setter( + "Journal Entry", "voucher_type", "options", "\n".join(journal_entry_types), "" + ) custom_fields = { - 'Journal Entry': [ - dict(fieldname='reversal_type', label='Reversal Type', - fieldtype='Select', insert_after='voucher_type', print_hide=1, + "Journal Entry": [ + dict( + fieldname="reversal_type", + label="Reversal Type", + fieldtype="Select", + insert_after="voucher_type", + print_hide=1, options="As per rules 42 & 43 of CGST Rules\nOthers", depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_address', label='Company Address', - fieldtype='Link', options='Address', insert_after='reversal_type', - print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1, - fetch_from='company_address.gstin', + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + options="Address", + insert_after="reversal_type", + print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'") + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + read_only=1, + insert_after="company_address", + print_hide=1, + fetch_from="company_address.gstin", + depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), ], - 'Purchase Invoice': [ - dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', - fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, - options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC', - default="All Other ITC") + "Purchase Invoice": [ + dict( + fieldname="eligibility_for_itc", + label="Eligibility For ITC", + fieldtype="Select", + insert_after="reason_for_issuing_document", + print_hide=1, + options="Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC", + default="All Other ITC", + ) + ], + "Purchase Invoice Item": [ + dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) ], - 'Purchase Invoice Item': [ - dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) - ] } create_custom_fields(custom_fields, update=True) @@ -54,28 +85,40 @@ def execute(): gst_accounts = get_gst_accounts(only_non_reverse_charge=1) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustom Field` SET fieldtype='Currency', options='Company:company:default_currency' WHERE dt = 'Purchase Invoice' and fieldname in ('itc_integrated_tax', 'itc_state_tax', 'itc_central_tax', 'itc_cess_amount') - """) + """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0' - WHERE trim(coalesce(itc_integrated_tax, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0' + WHERE trim(coalesce(itc_integrated_tax, '')) = '' """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_state_tax = '0' - WHERE trim(coalesce(itc_state_tax, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_state_tax = '0' + WHERE trim(coalesce(itc_state_tax, '')) = '' """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_central_tax = '0' - WHERE trim(coalesce(itc_central_tax, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_central_tax = '0' + WHERE trim(coalesce(itc_central_tax, '')) = '' """ + ) - frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_cess_amount = '0' - WHERE trim(coalesce(itc_cess_amount, '')) = '' """) + frappe.db.sql( + """UPDATE `tabPurchase Invoice` set itc_cess_amount = '0' + WHERE trim(coalesce(itc_cess_amount, '')) = '' """ + ) # Get purchase invoices - invoices = frappe.get_all('Purchase Invoice', - {'posting_date': ('>=', '2021-04-01'), 'eligibility_for_itc': ('!=', 'Ineligible')}, - ['name']) + invoices = frappe.get_all( + "Purchase Invoice", + {"posting_date": (">=", "2021-04-01"), "eligibility_for_itc": ("!=", "Ineligible")}, + ["name"], + ) amount_map = {} @@ -83,37 +126,42 @@ def execute(): invoice_list = set([d.name for d in invoices]) # Get GST applied - amounts = frappe.db.sql(""" + amounts = frappe.db.sql( + """ SELECT parent, account_head, sum(base_tax_amount_after_discount_amount) as amount FROM `tabPurchase Taxes and Charges` where parent in %s GROUP BY parent, account_head - """, (invoice_list), as_dict=1) + """, + (invoice_list), + as_dict=1, + ) for d in amounts: - amount_map.setdefault(d.parent, - { - 'itc_integrated_tax': 0, - 'itc_state_tax': 0, - 'itc_central_tax': 0, - 'itc_cess_amount': 0 - }) + amount_map.setdefault( + d.parent, + {"itc_integrated_tax": 0, "itc_state_tax": 0, "itc_central_tax": 0, "itc_cess_amount": 0}, + ) if not gst_accounts: continue - if d.account_head in gst_accounts.get('igst_account'): - amount_map[d.parent]['itc_integrated_tax'] += d.amount - if d.account_head in gst_accounts.get('cgst_account'): - amount_map[d.parent]['itc_central_tax'] += d.amount - if d.account_head in gst_accounts.get('sgst_account'): - amount_map[d.parent]['itc_state_tax'] += d.amount - if d.account_head in gst_accounts.get('cess_account'): - amount_map[d.parent]['itc_cess_amount'] += d.amount + if d.account_head in gst_accounts.get("igst_account"): + amount_map[d.parent]["itc_integrated_tax"] += d.amount + if d.account_head in gst_accounts.get("cgst_account"): + amount_map[d.parent]["itc_central_tax"] += d.amount + if d.account_head in gst_accounts.get("sgst_account"): + amount_map[d.parent]["itc_state_tax"] += d.amount + if d.account_head in gst_accounts.get("cess_account"): + amount_map[d.parent]["itc_cess_amount"] += d.amount for invoice, values in amount_map.items(): - frappe.db.set_value('Purchase Invoice', invoice, { - 'itc_integrated_tax': values.get('itc_integrated_tax'), - 'itc_central_tax': values.get('itc_central_tax'), - 'itc_state_tax': values['itc_state_tax'], - 'itc_cess_amount': values['itc_cess_amount'], - }) + frappe.db.set_value( + "Purchase Invoice", + invoice, + { + "itc_integrated_tax": values.get("itc_integrated_tax"), + "itc_central_tax": values.get("itc_central_tax"), + "itc_state_tax": values["itc_state_tax"], + "itc_cess_amount": values["itc_cess_amount"], + }, + ) diff --git a/erpnext/patches/v12_0/create_taxable_value_field.py b/erpnext/patches/v12_0/create_taxable_value_field.py index df0269d3a8c..ad953032e7c 100644 --- a/erpnext/patches/v12_0/create_taxable_value_field.py +++ b/erpnext/patches/v12_0/create_taxable_value_field.py @@ -1,18 +1,23 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_fields = { - 'Sales Invoice Item': [ - dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) + "Sales Invoice Item": [ + dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) ] } diff --git a/erpnext/patches/v12_0/delete_priority_property_setter.py b/erpnext/patches/v12_0/delete_priority_property_setter.py index cacc463d4b4..fbb02430f49 100644 --- a/erpnext/patches/v12_0/delete_priority_property_setter.py +++ b/erpnext/patches/v12_0/delete_priority_property_setter.py @@ -2,9 +2,11 @@ import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabProperty Setter` WHERE `tabProperty Setter`.doc_type='Issue' AND `tabProperty Setter`.field_name='priority' AND `tabProperty Setter`.property='options' - """) + """ + ) diff --git a/erpnext/patches/v12_0/fix_percent_complete_for_projects.py b/erpnext/patches/v12_0/fix_percent_complete_for_projects.py index 36f51bca60d..1fbcfa4e59a 100644 --- a/erpnext/patches/v12_0/fix_percent_complete_for_projects.py +++ b/erpnext/patches/v12_0/fix_percent_complete_for_projects.py @@ -4,10 +4,13 @@ from frappe.utils import flt def execute(): for project in frappe.get_all("Project", fields=["name", "percent_complete_method"]): - total = frappe.db.count('Task', dict(project=project.name)) + total = frappe.db.count("Task", dict(project=project.name)) if project.percent_complete_method == "Task Completion" and total > 0: - completed = frappe.db.sql("""select count(name) from tabTask where - project=%s and status in ('Cancelled', 'Completed')""", project.name)[0][0] + completed = frappe.db.sql( + """select count(name) from tabTask where + project=%s and status in ('Cancelled', 'Completed')""", + project.name, + )[0][0] percent_complete = flt(flt(completed) / total * 100, 2) if project.percent_complete != percent_complete: frappe.db.set_value("Project", project.name, "percent_complete", percent_complete) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index e5c4b8c524f..285183bfa25 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -13,12 +13,13 @@ def execute(): so_item.docstatus = 1 and so.docstatus = 1 and so_item.parent = so.name and so_item.prevdoc_docname = qo.name - and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired + and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""" - .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo) + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""".format( + cond=cond, invalid_so_against_quo=invalid_so_against_quo ) + ) valid_so_against_quo = """ SELECT @@ -27,9 +28,10 @@ def execute(): so_item.docstatus = 1 and so.docstatus = 1 and so_item.parent = so.name and so_item.prevdoc_docname = qo.name - and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired + and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""" - .format(cond=cond, valid_so_against_quo=valid_so_against_quo) + """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""".format( + cond=cond, valid_so_against_quo=valid_so_against_quo ) + ) diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index 90e46d07e40..354c5096c0f 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -7,8 +7,8 @@ from frappe.utils import getdate, today def execute(): - """ Generates leave ledger entries for leave allocation/application/encashment - for last allocation """ + """Generates leave ledger entries for leave allocation/application/encashment + for last allocation""" frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") frappe.reload_doc("HR", "doctype", "Leave Encashment") frappe.reload_doc("HR", "doctype", "Leave Type") @@ -22,55 +22,79 @@ def execute(): generate_encashment_leave_ledger_entries() generate_expiry_allocation_ledger_entries() + def update_leave_allocation_fieldname(): - ''' maps data from old field to the new field ''' - frappe.db.sql(""" + """maps data from old field to the new field""" + frappe.db.sql( + """ UPDATE `tabLeave Allocation` SET `unused_leaves` = `carry_forwarded_leaves` - """) + """ + ) + def generate_allocation_ledger_entries(): - ''' fix ledger entries for missing leave allocation transaction ''' + """fix ledger entries for missing leave allocation transaction""" allocation_list = get_allocation_records() for allocation in allocation_list: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Allocation", "transaction_name": allocation.name}, + ): allocation_obj = frappe.get_doc("Leave Allocation", allocation) allocation_obj.create_leave_ledger_entry() + def generate_application_leave_ledger_entries(): - ''' fix ledger entries for missing leave application transaction ''' + """fix ledger entries for missing leave application transaction""" leave_applications = get_leaves_application_records() for application in leave_applications: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Application", "transaction_name": application.name}, + ): frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry() + def generate_encashment_leave_ledger_entries(): - ''' fix ledger entries for missing leave encashment transaction ''' + """fix ledger entries for missing leave encashment transaction""" leave_encashments = get_leave_encashment_records() for encashment in leave_encashments: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Encashment", "transaction_name": encashment.name}, + ): frappe.get_doc("Leave Encashment", encashment).create_leave_ledger_entry() + def generate_expiry_allocation_ledger_entries(): - ''' fix ledger entries for missing leave allocation transaction ''' + """fix ledger entries for missing leave allocation transaction""" from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation + allocation_list = get_allocation_records() for allocation in allocation_list: - if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): + if not frappe.db.exists( + "Leave Ledger Entry", + {"transaction_type": "Leave Allocation", "transaction_name": allocation.name, "is_expired": 1}, + ): allocation_obj = frappe.get_doc("Leave Allocation", allocation) if allocation_obj.to_date <= getdate(today()): expire_allocation(allocation_obj) + def get_allocation_records(): - return frappe.get_all("Leave Allocation", filters={"docstatus": 1}, - fields=['name'], order_by='to_date ASC') + return frappe.get_all( + "Leave Allocation", filters={"docstatus": 1}, fields=["name"], order_by="to_date ASC" + ) + def get_leaves_application_records(): - return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name']) + return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=["name"]) + def get_leave_encashment_records(): - return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name']) + return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=["name"]) diff --git a/erpnext/patches/v12_0/make_item_manufacturer.py b/erpnext/patches/v12_0/make_item_manufacturer.py index d66f429de3c..3f233659d01 100644 --- a/erpnext/patches/v12_0/make_item_manufacturer.py +++ b/erpnext/patches/v12_0/make_item_manufacturer.py @@ -9,20 +9,29 @@ def execute(): frappe.reload_doc("stock", "doctype", "item_manufacturer") item_manufacturer = [] - for d in frappe.db.sql(""" SELECT name, manufacturer, manufacturer_part_no, creation, owner - FROM `tabItem` WHERE manufacturer is not null and manufacturer != ''""", as_dict=1): - item_manufacturer.append(( - frappe.generate_hash("", 10), - d.name, - d.manufacturer, - d.manufacturer_part_no, - d.creation, - d.owner - )) + for d in frappe.db.sql( + """ SELECT name, manufacturer, manufacturer_part_no, creation, owner + FROM `tabItem` WHERE manufacturer is not null and manufacturer != ''""", + as_dict=1, + ): + item_manufacturer.append( + ( + frappe.generate_hash("", 10), + d.name, + d.manufacturer, + d.manufacturer_part_no, + d.creation, + d.owner, + ) + ) if item_manufacturer: - frappe.db.sql(''' + frappe.db.sql( + """ INSERT INTO `tabItem Manufacturer` (`name`, `item_code`, `manufacturer`, `manufacturer_part_no`, `creation`, `owner`) - VALUES {}'''.format(', '.join(['%s'] * len(item_manufacturer))), tuple(item_manufacturer) + VALUES {}""".format( + ", ".join(["%s"] * len(item_manufacturer)) + ), + tuple(item_manufacturer), ) diff --git a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py index bca24c8dcd4..c069c24cfa5 100644 --- a/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py +++ b/erpnext/patches/v12_0/move_bank_account_swift_number_to_bank.py @@ -1,18 +1,23 @@ - import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'bank', force=1) + frappe.reload_doc("accounts", "doctype", "bank", force=1) - if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'): + if ( + frappe.db.table_exists("Bank") + and frappe.db.table_exists("Bank Account") + and frappe.db.has_column("Bank Account", "swift_number") + ): try: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabBank` b, `tabBank Account` ba SET b.swift_number = ba.swift_number WHERE b.name = ba.bank - """) + """ + ) except Exception as e: frappe.log_error(e, title="Patch Migration Failed") - frappe.reload_doc('accounts', 'doctype', 'bank_account') - frappe.reload_doc('accounts', 'doctype', 'payment_request') + frappe.reload_doc("accounts", "doctype", "bank_account") + frappe.reload_doc("accounts", "doctype", "payment_request") diff --git a/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py b/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py index 82dfba52c9f..17c1966624e 100644 --- a/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py +++ b/erpnext/patches/v12_0/move_credit_limit_to_customer_credit_limit.py @@ -6,7 +6,7 @@ import frappe def execute(): - ''' Move credit limit and bypass credit limit to the child table of customer credit limit ''' + """Move credit limit and bypass credit limit to the child table of customer credit limit""" frappe.reload_doc("Selling", "doctype", "Customer Credit Limit") frappe.reload_doc("Selling", "doctype", "Customer") frappe.reload_doc("Setup", "doctype", "Customer Group") @@ -16,28 +16,32 @@ def execute(): move_credit_limit_to_child_table() -def move_credit_limit_to_child_table(): - ''' maps data from old field to the new field in the child table ''' - companies = frappe.get_all("Company", 'name') +def move_credit_limit_to_child_table(): + """maps data from old field to the new field in the child table""" + + companies = frappe.get_all("Company", "name") for doctype in ("Customer", "Customer Group"): fields = "" - if doctype == "Customer" \ - and frappe.db.has_column('Customer', 'bypass_credit_limit_check_at_sales_order'): + if doctype == "Customer" and frappe.db.has_column( + "Customer", "bypass_credit_limit_check_at_sales_order" + ): fields = ", bypass_credit_limit_check_at_sales_order" - credit_limit_records = frappe.db.sql(''' + credit_limit_records = frappe.db.sql( + """ SELECT name, credit_limit {0} FROM `tab{1}` where credit_limit > 0 - '''.format(fields, doctype), as_dict=1) #nosec + """.format( + fields, doctype + ), + as_dict=1, + ) # nosec for record in credit_limit_records: doc = frappe.get_doc(doctype, record.name) for company in companies: - row = frappe._dict({ - 'credit_limit': record.credit_limit, - 'company': company.name - }) + row = frappe._dict({"credit_limit": record.credit_limit, "company": company.name}) if doctype == "Customer": row.bypass_credit_limit_check = record.bypass_credit_limit_check_at_sales_order diff --git a/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py index 5de7e69620b..8b8d9637f22 100644 --- a/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py +++ b/erpnext/patches/v12_0/move_due_advance_amount_to_pending_amount.py @@ -6,7 +6,7 @@ import frappe def execute(): - ''' Move from due_advance_amount to pending_amount ''' + """Move from due_advance_amount to pending_amount""" - if frappe.db.has_column("Employee Advance", "due_advance_amount"): - frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''') + if frappe.db.has_column("Employee Advance", "due_advance_amount"): + frappe.db.sql(""" UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount """) diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index 677a564af0d..c658dde57c5 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -13,17 +13,22 @@ def execute(): frappe.reload_doc("accounts", "doctype", "item_tax_template_detail", force=1) frappe.reload_doc("accounts", "doctype", "item_tax_template", force=1) - existing_templates = frappe.db.sql("""select template.name, details.tax_type, details.tax_rate + existing_templates = frappe.db.sql( + """select template.name, details.tax_type, details.tax_rate from `tabItem Tax Template` template, `tabItem Tax Template Detail` details where details.parent=template.name - """, as_dict=1) + """, + as_dict=1, + ) if len(existing_templates): for d in existing_templates: item_tax_templates.setdefault(d.name, {}) item_tax_templates[d.name][d.tax_type] = d.tax_rate - for d in frappe.db.sql("""select parent as item_code, tax_type, tax_rate from `tabItem Tax`""", as_dict=1): + for d in frappe.db.sql( + """select parent as item_code, tax_type, tax_rate from `tabItem Tax`""", as_dict=1 + ): old_item_taxes.setdefault(d.item_code, []) old_item_taxes[d.item_code].append(d) @@ -50,7 +55,9 @@ def execute(): item_tax_map[d.tax_type] = d.tax_rate tax_types = [] - item_tax_template_name = get_item_tax_template(item_tax_templates, item_tax_map, item_code, tax_types=tax_types) + item_tax_template_name = get_item_tax_template( + item_tax_templates, item_tax_map, item_code, tax_types=tax_types + ) # update the item tax table frappe.db.sql("delete from `tabItem Tax` where parent=%s and parenttype='Item'", item_code) @@ -62,17 +69,29 @@ def execute(): d.db_insert() doctypes = [ - 'Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', - 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice' + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", ] for dt in doctypes: - for d in frappe.db.sql("""select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` + for d in frappe.db.sql( + """select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` where ifnull(item_tax_rate, '') not in ('', '{{}}') - and item_tax_template is NULL""".format(dt), as_dict=1): + and item_tax_template is NULL""".format( + dt + ), + as_dict=1, + ): item_tax_map = json.loads(d.item_tax_rate) - item_tax_template_name = get_item_tax_template(item_tax_templates, - item_tax_map, d.item_code, d.parenttype, d.parent, tax_types=tax_types) + item_tax_template_name = get_item_tax_template( + item_tax_templates, item_tax_map, d.item_code, d.parenttype, d.parent, tax_types=tax_types + ) frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) frappe.db.auto_commit_on_many_writes = False @@ -82,7 +101,10 @@ def execute(): settings.determine_address_tax_category_from = "Billing Address" settings.save() -def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None, tax_types=None): + +def get_item_tax_template( + item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None, tax_types=None +): # search for previously created item tax template by comparing tax maps for template, item_tax_template_map in iteritems(item_tax_templates): if item_tax_map == item_tax_template_map: @@ -93,11 +115,19 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp item_tax_template.title = make_autoname("Item Tax Template-.####") for tax_type, tax_rate in iteritems(item_tax_map): - account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type', 'company'], as_dict=1) + account_details = frappe.db.get_value( + "Account", tax_type, ["name", "account_type", "company"], as_dict=1 + ) if account_details: item_tax_template.company = account_details.company - if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): - frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') + if account_details.account_type not in ( + "Tax", + "Chargeable", + "Income Account", + "Expense Account", + "Expenses Included In Valuation", + ): + frappe.db.set_value("Account", account_details.name, "account_type", "Chargeable") else: parts = tax_type.strip().split(" - ") account_name = " - ".join(parts[:-1]) @@ -105,18 +135,25 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp tax_type = None else: company = get_company(parts[-1], parenttype, parent) - parent_account = frappe.get_value("Account", {"account_name": account_name, "company": company}, "parent_account") + parent_account = frappe.get_value( + "Account", {"account_name": account_name, "company": company}, "parent_account" + ) if not parent_account: - parent_account = frappe.db.get_value("Account", - filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account") + parent_account = frappe.db.get_value( + "Account", + filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, + fieldname="parent_account", + ) if not parent_account: - parent_account = frappe.db.get_value("Account", - filters={"account_type": "Tax", "root_type": "Liability", "is_group": 1, "company": company}) + parent_account = frappe.db.get_value( + "Account", + filters={"account_type": "Tax", "root_type": "Liability", "is_group": 1, "company": company}, + ) filters = { "account_name": account_name, "company": company, "account_type": "Tax", - "parent_account": parent_account + "parent_account": parent_account, } tax_type = frappe.db.get_value("Account", filters) if not tax_type: @@ -126,11 +163,19 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp account.insert() tax_type = account.name except frappe.DuplicateEntryError: - tax_type = frappe.db.get_value("Account", {"account_name": account_name, "company": company}, "name") + tax_type = frappe.db.get_value( + "Account", {"account_name": account_name, "company": company}, "name" + ) account_type = frappe.get_cached_value("Account", tax_type, "account_type") - if tax_type and account_type in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): + if tax_type and account_type in ( + "Tax", + "Chargeable", + "Income Account", + "Expense Account", + "Expenses Included In Valuation", + ): if tax_type not in tax_types: item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate}) tax_types.append(tax_type) @@ -140,14 +185,15 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttyp item_tax_template.save() return item_tax_template.name + def get_company(company_abbr, parenttype=None, parent=None): if parenttype and parent: - company = frappe.get_cached_value(parenttype, parent, 'company') + company = frappe.get_cached_value(parenttype, parent, "company") else: company = frappe.db.get_value("Company", filters={"abbr": company_abbr}) if not company: - companies = frappe.get_all('Company') + companies = frappe.get_all("Company") if len(companies) == 1: company = companies[0].name diff --git a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py index c396891b59e..6788cb2e264 100644 --- a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py +++ b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py @@ -12,10 +12,12 @@ def execute(): if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret): plaid_settings.enabled = 0 else: - plaid_settings.update({ - "plaid_client_id": frappe.conf.plaid_client_id, - "plaid_env": frappe.conf.plaid_env, - "plaid_secret": frappe.conf.plaid_secret - }) + plaid_settings.update( + { + "plaid_client_id": frappe.conf.plaid_client_id, + "plaid_env": frappe.conf.plaid_env, + "plaid_secret": frappe.conf.plaid_secret, + } + ) plaid_settings.flags.ignore_mandatory = True plaid_settings.save() diff --git a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py index 36fe18d8b71..71926107bd5 100644 --- a/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py +++ b/erpnext/patches/v12_0/move_target_distribution_from_parent_to_child.py @@ -6,19 +6,23 @@ import frappe def execute(): - frappe.reload_doc("setup", "doctype", "target_detail") - frappe.reload_doc("core", "doctype", "prepared_report") + frappe.reload_doc("setup", "doctype", "target_detail") + frappe.reload_doc("core", "doctype", "prepared_report") - for d in ['Sales Person', 'Sales Partner', 'Territory']: - frappe.db.sql(""" + for d in ["Sales Person", "Sales Partner", "Territory"]: + frappe.db.sql( + """ UPDATE `tab{child_doc}`, `tab{parent_doc}` SET `tab{child_doc}`.distribution_id = `tab{parent_doc}`.distribution_id WHERE `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.distribution_id is not null and `tab{parent_doc}`.distribution_id != '' - """.format(parent_doc = d, child_doc = "Target Detail")) + """.format( + parent_doc=d, child_doc="Target Detail" + ) + ) - frappe.delete_doc("Report", "Sales Partner-wise Transaction Summary") - frappe.delete_doc("Report", "Sales Person Target Variance Item Group-Wise") - frappe.delete_doc("Report", "Territory Target Variance Item Group-Wise") + frappe.delete_doc("Report", "Sales Partner-wise Transaction Summary") + frappe.delete_doc("Report", "Sales Person Target Variance Item Group-Wise") + frappe.delete_doc("Report", "Territory Target Variance Item Group-Wise") diff --git a/erpnext/patches/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py index 459221e7695..3b828d69cb2 100644 --- a/erpnext/patches/v12_0/purchase_receipt_status.py +++ b/erpnext/patches/v12_0/purchase_receipt_status.py @@ -7,6 +7,7 @@ import frappe logger = frappe.logger("patch", allow_site=True, file_count=50) + def execute(): affected_purchase_receipts = frappe.db.sql( """select name from `tabPurchase Receipt` @@ -16,13 +17,13 @@ def execute(): if not affected_purchase_receipts: return - logger.info("purchase_receipt_status: begin patch, PR count: {}" - .format(len(affected_purchase_receipts))) + logger.info( + "purchase_receipt_status: begin patch, PR count: {}".format(len(affected_purchase_receipts)) + ) frappe.reload_doc("stock", "doctype", "Purchase Receipt") frappe.reload_doc("stock", "doctype", "Purchase Receipt Item") - for pr in affected_purchase_receipts: pr_name = pr[0] logger.info("purchase_receipt_status: patching PR - {}".format(pr_name)) diff --git a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py index 50d97c4830d..661152bef34 100644 --- a/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py +++ b/erpnext/patches/v12_0/recalculate_requested_qty_in_bin.py @@ -1,17 +1,21 @@ - import frappe from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty def execute(): - bin_details = frappe.db.sql(""" + bin_details = frappe.db.sql( + """ SELECT item_code, warehouse - FROM `tabBin`""",as_dict=1) + FROM `tabBin`""", + as_dict=1, + ) for entry in bin_details: if not (entry.item_code and entry.warehouse): continue - update_bin_qty(entry.get("item_code"), entry.get("warehouse"), { - "indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse")) - }) + update_bin_qty( + entry.get("item_code"), + entry.get("warehouse"), + {"indented_qty": get_indented_qty(entry.get("item_code"), entry.get("warehouse"))}, + ) diff --git a/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py b/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py index 0f4a366c657..b18f4ebe2ed 100644 --- a/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py +++ b/erpnext/patches/v12_0/remove_bank_remittance_custom_fields.py @@ -1,14 +1,18 @@ - import frappe def execute(): frappe.reload_doc("accounts", "doctype", "tax_category") frappe.reload_doc("stock", "doctype", "item_manufacturer") - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return if frappe.db.exists("Custom Field", "Company-bank_remittance_section"): - deprecated_fields = ['bank_remittance_section', 'client_code', 'remittance_column_break', 'product_code'] + deprecated_fields = [ + "bank_remittance_section", + "client_code", + "remittance_column_break", + "product_code", + ] for i in range(len(deprecated_fields)): - frappe.delete_doc("Custom Field", 'Company-'+deprecated_fields[i]) + frappe.delete_doc("Custom Field", "Company-" + deprecated_fields[i]) diff --git a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py index d1d4bcc140f..4029a3f0e22 100644 --- a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py +++ b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py @@ -6,8 +6,8 @@ import frappe def execute(): - ''' Delete leave ledger entry created - via leave applications with status != Approved ''' + """Delete leave ledger entry created + via leave applications with status != Approved""" if not frappe.db.a_row_exists("Leave Ledger Entry"): return @@ -15,14 +15,21 @@ def execute(): if leave_application_list: delete_denied_leaves_from_leave_ledger_entry(leave_application_list) + def get_denied_leave_application_list(): - return frappe.db.sql_list(''' Select name from `tabLeave Application` where status <> 'Approved' ''') + return frappe.db.sql_list( + """ Select name from `tabLeave Application` where status <> 'Approved' """ + ) + def delete_denied_leaves_from_leave_ledger_entry(leave_application_list): if leave_application_list: - frappe.db.sql(''' Delete + frappe.db.sql( + """ Delete FROM `tabLeave Ledger Entry` WHERE transaction_type = 'Leave Application' - AND transaction_name in (%s) ''' % (', '.join(['%s'] * len(leave_application_list))), #nosec - tuple(leave_application_list)) + AND transaction_name in (%s) """ + % (", ".join(["%s"] * len(leave_application_list))), # nosec + tuple(leave_application_list), + ) diff --git a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py index 6ad68ccc6e4..8247734a4a1 100644 --- a/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/remove_duplicate_leave_ledger_entries.py @@ -7,16 +7,18 @@ import frappe def execute(): """Delete duplicate leave ledger entries of type allocation created.""" - frappe.reload_doc('hr', 'doctype', 'leave_ledger_entry') + frappe.reload_doc("hr", "doctype", "leave_ledger_entry") if not frappe.db.a_row_exists("Leave Ledger Entry"): return duplicate_records_list = get_duplicate_records() delete_duplicate_ledger_entries(duplicate_records_list) + def get_duplicate_records(): """Fetch all but one duplicate records from the list of expired leave allocation.""" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT name, employee, transaction_name, leave_type, is_carry_forward, from_date, to_date FROM `tabLeave Ledger Entry` WHERE @@ -29,13 +31,17 @@ def get_duplicate_records(): count(name) > 1 ORDER BY creation - """) + """ + ) + def delete_duplicate_ledger_entries(duplicate_records_list): """Delete duplicate leave ledger entries.""" - if not duplicate_records_list: return + if not duplicate_records_list: + return for d in duplicate_records_list: - frappe.db.sql(''' + frappe.db.sql( + """ DELETE FROM `tabLeave Ledger Entry` WHERE name != %s AND employee = %s @@ -44,4 +50,6 @@ def delete_duplicate_ledger_entries(duplicate_records_list): AND is_carry_forward = %s AND from_date = %s AND to_date = %s - ''', tuple(d)) + """, + tuple(d), + ) diff --git a/erpnext/patches/v12_0/rename_account_type_doctype.py b/erpnext/patches/v12_0/rename_account_type_doctype.py index c2c834bf98c..ab195549a42 100644 --- a/erpnext/patches/v12_0/rename_account_type_doctype.py +++ b/erpnext/patches/v12_0/rename_account_type_doctype.py @@ -1,8 +1,7 @@ - import frappe def execute(): - frappe.rename_doc('DocType', 'Account Type', 'Bank Account Type', force=True) - frappe.rename_doc('DocType', 'Account Subtype', 'Bank Account Subtype', force=True) - frappe.reload_doc('accounts', 'doctype', 'bank_account') + frappe.rename_doc("DocType", "Account Type", "Bank Account Type", force=True) + frappe.rename_doc("DocType", "Account Subtype", "Bank Account Subtype", force=True) + frappe.reload_doc("accounts", "doctype", "bank_account") diff --git a/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py b/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py index a5d986a0a16..92687530ebc 100644 --- a/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py +++ b/erpnext/patches/v12_0/rename_bank_account_field_in_journal_entry_account.py @@ -7,12 +7,13 @@ from frappe.model.utils.rename_field import rename_field def execute(): - ''' Change the fieldname from bank_account_no to bank_account ''' + """Change the fieldname from bank_account_no to bank_account""" if not frappe.get_meta("Journal Entry Account").has_field("bank_account"): frappe.reload_doc("Accounts", "doctype", "Journal Entry Account") update_journal_entry_account_fieldname() + def update_journal_entry_account_fieldname(): - ''' maps data from old field to the new field ''' - if frappe.db.has_column('Journal Entry Account', 'bank_account_no'): + """maps data from old field to the new field""" + if frappe.db.has_column("Journal Entry Account", "bank_account_no"): rename_field("Journal Entry Account", "bank_account_no", "bank_account") diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation.py b/erpnext/patches/v12_0/rename_bank_reconciliation.py index 51ff0c8c949..aacd6e8161d 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation.py @@ -7,8 +7,8 @@ import frappe def execute(): if frappe.db.table_exists("Bank Reconciliation"): - frappe.rename_doc('DocType', 'Bank Reconciliation', 'Bank Clearance', force=True) - frappe.reload_doc('Accounts', 'doctype', 'Bank Clearance') + frappe.rename_doc("DocType", "Bank Reconciliation", "Bank Clearance", force=True) + frappe.reload_doc("Accounts", "doctype", "Bank Clearance") - frappe.rename_doc('DocType', 'Bank Reconciliation Detail', 'Bank Clearance Detail', force=True) - frappe.reload_doc('Accounts', 'doctype', 'Bank Clearance Detail') + frappe.rename_doc("DocType", "Bank Reconciliation Detail", "Bank Clearance Detail", force=True) + frappe.reload_doc("Accounts", "doctype", "Bank Clearance Detail") diff --git a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py index 629cd5bda66..e2a3887b9ac 100644 --- a/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py +++ b/erpnext/patches/v12_0/rename_bank_reconciliation_fields.py @@ -5,11 +5,24 @@ import frappe def _rename_single_field(**kwargs): - count = frappe.db.sql("SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format(**kwargs))[0][0] #nosec + count = frappe.db.sql( + "SELECT COUNT(*) FROM tabSingles WHERE doctype='{doctype}' AND field='{new_name}';".format( + **kwargs + ) + )[0][ + 0 + ] # nosec if count == 0: - frappe.db.sql("UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format(**kwargs)) #nosec + frappe.db.sql( + "UPDATE tabSingles SET field='{new_name}' WHERE doctype='{doctype}' AND field='{old_name}';".format( + **kwargs + ) + ) # nosec + def execute(): - _rename_single_field(doctype = "Bank Clearance", old_name = "bank_account" , new_name = "account") - _rename_single_field(doctype = "Bank Clearance", old_name = "bank_account_no", new_name = "bank_account") + _rename_single_field(doctype="Bank Clearance", old_name="bank_account", new_name="account") + _rename_single_field( + doctype="Bank Clearance", old_name="bank_account_no", new_name="bank_account" + ) frappe.reload_doc("Accounts", "doctype", "Bank Clearance") diff --git a/erpnext/patches/v12_0/rename_lost_reason_detail.py b/erpnext/patches/v12_0/rename_lost_reason_detail.py index 55bf6f223fb..2f7f8428482 100644 --- a/erpnext/patches/v12_0/rename_lost_reason_detail.py +++ b/erpnext/patches/v12_0/rename_lost_reason_detail.py @@ -1,19 +1,24 @@ - import frappe def execute(): - if frappe.db.exists("DocType", "Lost Reason Detail"): - frappe.reload_doc("crm", "doctype", "opportunity_lost_reason") - frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") - frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") + if frappe.db.exists("DocType", "Lost Reason Detail"): + frappe.reload_doc("crm", "doctype", "opportunity_lost_reason") + frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") + frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") - frappe.db.sql("""INSERT INTO `tabOpportunity Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Opportunity'""") + frappe.db.sql( + """INSERT INTO `tabOpportunity Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Opportunity'""" + ) - frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""") + frappe.db.sql( + """INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""" + ) - frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) + frappe.db.sql( + """INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` - FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""") + FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""" + ) - frappe.delete_doc("DocType", "Lost Reason Detail") + frappe.delete_doc("DocType", "Lost Reason Detail") diff --git a/erpnext/patches/v12_0/rename_mws_settings_fields.py b/erpnext/patches/v12_0/rename_mws_settings_fields.py index d5bf38d204d..97c89e0dbf9 100644 --- a/erpnext/patches/v12_0/rename_mws_settings_fields.py +++ b/erpnext/patches/v12_0/rename_mws_settings_fields.py @@ -5,8 +5,12 @@ import frappe def execute(): - count = frappe.db.sql("SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';")[0][0] + count = frappe.db.sql( + "SELECT COUNT(*) FROM `tabSingles` WHERE doctype='Amazon MWS Settings' AND field='enable_sync';" + )[0][0] if count == 0: - frappe.db.sql("UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';") + frappe.db.sql( + "UPDATE `tabSingles` SET field='enable_sync' WHERE doctype='Amazon MWS Settings' AND field='enable_synch';" + ) frappe.reload_doc("ERPNext Integrations", "doctype", "Amazon MWS Settings") diff --git a/erpnext/patches/v12_0/rename_pos_closing_doctype.py b/erpnext/patches/v12_0/rename_pos_closing_doctype.py index f5f0112e036..fb80f8dc615 100644 --- a/erpnext/patches/v12_0/rename_pos_closing_doctype.py +++ b/erpnext/patches/v12_0/rename_pos_closing_doctype.py @@ -7,17 +7,19 @@ import frappe def execute(): if frappe.db.table_exists("POS Closing Voucher"): if not frappe.db.exists("DocType", "POS Closing Entry"): - frappe.rename_doc('DocType', 'POS Closing Voucher', 'POS Closing Entry', force=True) + frappe.rename_doc("DocType", "POS Closing Voucher", "POS Closing Entry", force=True) - if not frappe.db.exists('DocType', 'POS Closing Entry Taxes'): - frappe.rename_doc('DocType', 'POS Closing Voucher Taxes', 'POS Closing Entry Taxes', force=True) + if not frappe.db.exists("DocType", "POS Closing Entry Taxes"): + frappe.rename_doc("DocType", "POS Closing Voucher Taxes", "POS Closing Entry Taxes", force=True) - if not frappe.db.exists('DocType', 'POS Closing Voucher Details'): - frappe.rename_doc('DocType', 'POS Closing Voucher Details', 'POS Closing Entry Detail', force=True) + if not frappe.db.exists("DocType", "POS Closing Voucher Details"): + frappe.rename_doc( + "DocType", "POS Closing Voucher Details", "POS Closing Entry Detail", force=True + ) - frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry') - frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry Taxes') - frappe.reload_doc('Accounts', 'doctype', 'POS Closing Entry Detail') + frappe.reload_doc("Accounts", "doctype", "POS Closing Entry") + frappe.reload_doc("Accounts", "doctype", "POS Closing Entry Taxes") + frappe.reload_doc("Accounts", "doctype", "POS Closing Entry Detail") if frappe.db.exists("DocType", "POS Closing Voucher"): frappe.delete_doc("DocType", "POS Closing Voucher") diff --git a/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py b/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py index 87630fbcaf9..8d4c01359d4 100644 --- a/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py +++ b/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py @@ -5,16 +5,17 @@ import frappe doctypes = { - 'Price Discount Slab': 'Promotional Scheme Price Discount', - 'Product Discount Slab': 'Promotional Scheme Product Discount', - 'Apply Rule On Item Code': 'Pricing Rule Item Code', - 'Apply Rule On Item Group': 'Pricing Rule Item Group', - 'Apply Rule On Brand': 'Pricing Rule Brand' + "Price Discount Slab": "Promotional Scheme Price Discount", + "Product Discount Slab": "Promotional Scheme Product Discount", + "Apply Rule On Item Code": "Pricing Rule Item Code", + "Apply Rule On Item Group": "Pricing Rule Item Group", + "Apply Rule On Brand": "Pricing Rule Brand", } + def execute(): - for old_doc, new_doc in doctypes.items(): - if not frappe.db.table_exists(new_doc) and frappe.db.table_exists(old_doc): - frappe.rename_doc('DocType', old_doc, new_doc) - frappe.reload_doc("accounts", "doctype", frappe.scrub(new_doc)) - frappe.delete_doc("DocType", old_doc) + for old_doc, new_doc in doctypes.items(): + if not frappe.db.table_exists(new_doc) and frappe.db.table_exists(old_doc): + frappe.rename_doc("DocType", old_doc, new_doc) + frappe.reload_doc("accounts", "doctype", frappe.scrub(new_doc)) + frappe.delete_doc("DocType", old_doc) diff --git a/erpnext/patches/v12_0/rename_tolerance_fields.py b/erpnext/patches/v12_0/rename_tolerance_fields.py index ca2427bc3dd..ef1ba655a9f 100644 --- a/erpnext/patches/v12_0/rename_tolerance_fields.py +++ b/erpnext/patches/v12_0/rename_tolerance_fields.py @@ -7,8 +7,8 @@ def execute(): frappe.reload_doc("stock", "doctype", "stock_settings") frappe.reload_doc("accounts", "doctype", "accounts_settings") - rename_field('Stock Settings', "tolerance", "over_delivery_receipt_allowance") - rename_field('Item', "tolerance", "over_delivery_receipt_allowance") + rename_field("Stock Settings", "tolerance", "over_delivery_receipt_allowance") + rename_field("Item", "tolerance", "over_delivery_receipt_allowance") qty_allowance = frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") frappe.db.set_value("Accounts Settings", None, "over_delivery_receipt_allowance", qty_allowance) diff --git a/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py b/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py index ff332f771d3..21dd258eadc 100644 --- a/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py +++ b/erpnext/patches/v12_0/replace_accounting_with_accounts_in_home_settings.py @@ -2,5 +2,7 @@ import frappe def execute(): - frappe.db.sql("""UPDATE `tabUser` SET `home_settings` = REPLACE(`home_settings`, 'Accounting', 'Accounts')""") - frappe.cache().delete_key('home_settings') + frappe.db.sql( + """UPDATE `tabUser` SET `home_settings` = REPLACE(`home_settings`, 'Accounting', 'Accounts')""" + ) + frappe.cache().delete_key("home_settings") diff --git a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py index 198963df711..a4a85871ea2 100644 --- a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py +++ b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py @@ -6,51 +6,79 @@ import frappe def execute(): - warehouse_perm = frappe.get_all("User Permission", - fields=["count(*) as p_count", "is_default", "user"], filters={"allow": "Warehouse"}, group_by="user") + warehouse_perm = frappe.get_all( + "User Permission", + fields=["count(*) as p_count", "is_default", "user"], + filters={"allow": "Warehouse"}, + group_by="user", + ) if not warehouse_perm: return execute_patch = False for perm_data in warehouse_perm: - if perm_data.p_count == 1 or (perm_data.p_count > 1 and frappe.get_all("User Permission", - filters = {"user": perm_data.user, "allow": "warehouse", "is_default": 1}, limit=1)): + if perm_data.p_count == 1 or ( + perm_data.p_count > 1 + and frappe.get_all( + "User Permission", + filters={"user": perm_data.user, "allow": "warehouse", "is_default": 1}, + limit=1, + ) + ): execute_patch = True break - if not execute_patch: return + if not execute_patch: + return for doctype in ["Sales Invoice", "Delivery Note"]: - if not frappe.get_meta(doctype + ' Item').get_field("target_warehouse").hidden: continue + if not frappe.get_meta(doctype + " Item").get_field("target_warehouse").hidden: + continue cond = "" if doctype == "Sales Invoice": cond = " AND parent_doc.update_stock = 1" - data = frappe.db.sql(""" SELECT parent_doc.name as name, child_doc.name as child_name + data = frappe.db.sql( + """ SELECT parent_doc.name as name, child_doc.name as child_name FROM `tab{doctype}` parent_doc, `tab{doctype} Item` child_doc WHERE parent_doc.name = child_doc.parent AND parent_doc.docstatus < 2 AND child_doc.target_warehouse is not null AND child_doc.target_warehouse != '' AND child_doc.creation > '2020-04-16' {cond} - """.format(doctype=doctype, cond=cond), as_dict=1) + """.format( + doctype=doctype, cond=cond + ), + as_dict=1, + ) if data: names = [d.child_name for d in data] - frappe.db.sql(""" UPDATE `tab{0} Item` set target_warehouse = null - WHERE name in ({1}) """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + frappe.db.sql( + """ UPDATE `tab{0} Item` set target_warehouse = null + WHERE name in ({1}) """.format( + doctype, ",".join(["%s"] * len(names)) + ), + tuple(names), + ) - frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null + frappe.db.sql( + """ UPDATE `tabPacked Item` set target_warehouse = null WHERE parenttype = '{0}' and parent_detail_docname in ({1}) - """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + """.format( + doctype, ",".join(["%s"] * len(names)) + ), + tuple(names), + ) parent_names = list(set([d.name for d in data])) for d in parent_names: doc = frappe.get_doc(doctype, d) - if doc.docstatus != 1: continue + if doc.docstatus != 1: + continue doc.docstatus = 2 doc.update_stock_ledger() @@ -61,9 +89,13 @@ def execute(): doc.update_stock_ledger() doc.make_gl_entries() - if frappe.get_meta('Sales Order Item').get_field("target_warehouse").hidden: - frappe.db.sql(""" UPDATE `tabSales Order Item` set target_warehouse = null - WHERE creation > '2020-04-16' and docstatus < 2 """) + if frappe.get_meta("Sales Order Item").get_field("target_warehouse").hidden: + frappe.db.sql( + """ UPDATE `tabSales Order Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 """ + ) - frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null - WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """) + frappe.db.sql( + """ UPDATE `tabPacked Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """ + ) diff --git a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py index b76e34abe13..d88593b4984 100644 --- a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py +++ b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py @@ -3,12 +3,16 @@ import frappe def execute(): - frappe.reload_doc('selling', 'doctype', 'sales_order_item', force=True) - frappe.reload_doc('buying', 'doctype', 'purchase_order_item', force=True) + frappe.reload_doc("selling", "doctype", "sales_order_item", force=True) + frappe.reload_doc("buying", "doctype", "purchase_order_item", force=True) - for doctype in ('Sales Order Item', 'Purchase Order Item'): - frappe.db.sql(""" + for doctype in ("Sales Order Item", "Purchase Order Item"): + frappe.db.sql( + """ UPDATE `tab{0}` SET against_blanket_order = 1 WHERE ifnull(blanket_order, '') != '' - """.format(doctype)) + """.format( + doctype + ) + ) diff --git a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py index b4f8a0631a6..37af989549f 100644 --- a/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py +++ b/erpnext/patches/v12_0/set_automatically_process_deferred_accounting_in_accounts_settings.py @@ -1,8 +1,9 @@ - import frappe def execute(): frappe.reload_doc("accounts", "doctype", "accounts_settings") - frappe.db.set_value("Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1) + frappe.db.set_value( + "Accounts Settings", None, "automatically_process_deferred_accounting_entry", 1 + ) diff --git a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py index d3045a1a576..a5b4c66ce87 100644 --- a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py +++ b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py @@ -2,9 +2,11 @@ import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'expense_claim_detail') - frappe.db.sql(""" + frappe.reload_doc("hr", "doctype", "expense_claim_detail") + frappe.db.sql( + """ UPDATE `tabExpense Claim Detail` child, `tabExpense Claim` par SET child.cost_center = par.cost_center WHERE child.parent = par.name - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py index fe580ce0236..952f64be2e7 100644 --- a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py +++ b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py @@ -1,11 +1,10 @@ - import frappe from frappe.utils import cint def execute(): - '''Get 'Disable CWIP Accounting value' from Asset Settings, set it in 'Enable Capital Work in Progress Accounting' field - in Company, delete Asset Settings ''' + """Get 'Disable CWIP Accounting value' from Asset Settings, set it in 'Enable Capital Work in Progress Accounting' field + in Company, delete Asset Settings""" if frappe.db.exists("DocType", "Asset Settings"): frappe.reload_doctype("Asset Category") diff --git a/erpnext/patches/v12_0/set_default_batch_size.py b/erpnext/patches/v12_0/set_default_batch_size.py index 6fb69456dd1..ac3e2f47ee5 100644 --- a/erpnext/patches/v12_0/set_default_batch_size.py +++ b/erpnext/patches/v12_0/set_default_batch_size.py @@ -2,18 +2,22 @@ import frappe def execute(): - frappe.reload_doc("manufacturing", "doctype", "bom_operation") - frappe.reload_doc("manufacturing", "doctype", "work_order_operation") + frappe.reload_doc("manufacturing", "doctype", "bom_operation") + frappe.reload_doc("manufacturing", "doctype", "work_order_operation") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabBOM Operation` bo SET bo.batch_size = 1 - """) - frappe.db.sql(""" + """ + ) + frappe.db.sql( + """ UPDATE `tabWork Order Operation` wop SET wop.batch_size = 1 - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_default_homepage_type.py b/erpnext/patches/v12_0/set_default_homepage_type.py index 1e4333aa466..d70b28efd85 100644 --- a/erpnext/patches/v12_0/set_default_homepage_type.py +++ b/erpnext/patches/v12_0/set_default_homepage_type.py @@ -2,4 +2,4 @@ import frappe def execute(): - frappe.db.set_value('Homepage', 'Homepage', 'hero_section_based_on', 'Default') + frappe.db.set_value("Homepage", "Homepage", "hero_section_based_on", "Default") diff --git a/erpnext/patches/v12_0/set_default_payroll_based_on.py b/erpnext/patches/v12_0/set_default_payroll_based_on.py index b70bb18b60b..de641c65a1c 100644 --- a/erpnext/patches/v12_0/set_default_payroll_based_on.py +++ b/erpnext/patches/v12_0/set_default_payroll_based_on.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v12_0/set_default_shopify_app_type.py b/erpnext/patches/v12_0/set_default_shopify_app_type.py index c712287c0dc..41516ba850c 100644 --- a/erpnext/patches/v12_0/set_default_shopify_app_type.py +++ b/erpnext/patches/v12_0/set_default_shopify_app_type.py @@ -1,7 +1,6 @@ - import frappe def execute(): - frappe.reload_doc('erpnext_integrations', 'doctype', 'shopify_settings') - frappe.db.set_value('Shopify Settings', None, 'app_type', 'Private') + frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings") + frappe.db.set_value("Shopify Settings", None, "app_type", "Private") diff --git a/erpnext/patches/v12_0/set_employee_preferred_emails.py b/erpnext/patches/v12_0/set_employee_preferred_emails.py index f6eb12e2b9f..a6159c6b6b0 100644 --- a/erpnext/patches/v12_0/set_employee_preferred_emails.py +++ b/erpnext/patches/v12_0/set_employee_preferred_emails.py @@ -2,9 +2,11 @@ import frappe def execute(): - employees = frappe.get_all("Employee", + employees = frappe.get_all( + "Employee", filters={"prefered_email": ""}, - fields=["name", "prefered_contact_email", "company_email", "personal_email", "user_id"]) + fields=["name", "prefered_contact_email", "company_email", "personal_email", "user_id"], + ) for employee in employees: if not employee.prefered_contact_email: @@ -13,4 +15,6 @@ def execute(): preferred_email_field = frappe.scrub(employee.prefered_contact_email) preferred_email = employee.get(preferred_email_field) - frappe.db.set_value("Employee", employee.name, "prefered_email", preferred_email, update_modified=False) + frappe.db.set_value( + "Employee", employee.name, "prefered_email", preferred_email, update_modified=False + ) diff --git a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py index 47d4eb599b8..96f80a057b4 100644 --- a/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py +++ b/erpnext/patches/v12_0/set_expense_account_in_landed_cost_voucher_taxes.py @@ -1,17 +1,21 @@ - import frappe from six import iteritems def execute(): - frappe.reload_doctype('Landed Cost Taxes and Charges') + frappe.reload_doctype("Landed Cost Taxes and Charges") - company_account_map = frappe._dict(frappe.db.sql(""" + company_account_map = frappe._dict( + frappe.db.sql( + """ SELECT name, expenses_included_in_valuation from `tabCompany` - """)) + """ + ) + ) for company, account in iteritems(company_account_map): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLanded Cost Taxes and Charges` t, `tabLanded Cost Voucher` l SET @@ -20,9 +24,12 @@ def execute(): l.docstatus = 1 AND l.company = %s AND t.parent = l.name - """, (account, company)) + """, + (account, company), + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLanded Cost Taxes and Charges` t, `tabStock Entry` s SET @@ -31,4 +38,6 @@ def execute(): s.docstatus = 1 AND s.company = %s AND t.parent = s.name - """, (account, company)) + """, + (account, company), + ) diff --git a/erpnext/patches/v12_0/set_gst_category.py b/erpnext/patches/v12_0/set_gst_category.py index 094e2a3134b..126a73bd3d0 100644 --- a/erpnext/patches/v12_0/set_gst_category.py +++ b/erpnext/patches/v12_0/set_gst_category.py @@ -5,48 +5,62 @@ from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('accounts', 'doctype', 'Tax Category') + frappe.reload_doc("accounts", "doctype", "Tax Category") make_custom_fields() - for doctype in ['Sales Invoice', 'Purchase Invoice']: - has_column = frappe.db.has_column(doctype,'invoice_type') + for doctype in ["Sales Invoice", "Purchase Invoice"]: + has_column = frappe.db.has_column(doctype, "invoice_type") if has_column: update_map = { - 'Regular': 'Registered Regular', - 'Export': 'Overseas', - 'SEZ': 'SEZ', - 'Deemed Export': 'Deemed Export', + "Regular": "Registered Regular", + "Export": "Overseas", + "SEZ": "SEZ", + "Deemed Export": "Deemed Export", } for old, new in update_map.items(): - frappe.db.sql("UPDATE `tab{doctype}` SET gst_category = %s where invoice_type = %s".format(doctype=doctype), (new, old)) #nosec + frappe.db.sql( + "UPDATE `tab{doctype}` SET gst_category = %s where invoice_type = %s".format(doctype=doctype), + (new, old), + ) # nosec - frappe.delete_doc('Custom Field', 'Sales Invoice-invoice_type') - frappe.delete_doc('Custom Field', 'Purchase Invoice-invoice_type') + frappe.delete_doc("Custom Field", "Sales Invoice-invoice_type") + frappe.delete_doc("Custom Field", "Purchase Invoice-invoice_type") itc_update_map = { "ineligible": "Ineligible", "input service": "Input Service Distributor", "capital goods": "Import Of Capital Goods", - "input": "All Other ITC" + "input": "All Other ITC", } - has_gst_fields = frappe.db.has_column('Purchase Invoice','eligibility_for_itc') + has_gst_fields = frappe.db.has_column("Purchase Invoice", "eligibility_for_itc") if has_gst_fields: for old, new in itc_update_map.items(): - frappe.db.sql("UPDATE `tabPurchase Invoice` SET eligibility_for_itc = %s where eligibility_for_itc = %s ", (new, old)) + frappe.db.sql( + "UPDATE `tabPurchase Invoice` SET eligibility_for_itc = %s where eligibility_for_itc = %s ", + (new, old), + ) for doctype in ["Customer", "Supplier"]: - frappe.db.sql(""" UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Registered Regular" - where t3.link_name = t1.name and t3.parent = t2.name and t2.gstin IS NOT NULL and t2.gstin != '' """.format(doctype=doctype)) #nosec + frappe.db.sql( + """ UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Registered Regular" + where t3.link_name = t1.name and t3.parent = t2.name and t2.gstin IS NOT NULL and t2.gstin != '' """.format( + doctype=doctype + ) + ) # nosec - frappe.db.sql(""" UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Overseas" - where t3.link_name = t1.name and t3.parent = t2.name and t2.country != 'India' """.format(doctype=doctype)) #nosec + frappe.db.sql( + """ UPDATE `tab{doctype}` t1, `tabAddress` t2, `tabDynamic Link` t3 SET t1.gst_category = "Overseas" + where t3.link_name = t1.name and t3.parent = t2.name and t2.country != 'India' """.format( + doctype=doctype + ) + ) # nosec diff --git a/erpnext/patches/v12_0/set_job_offer_applicant_email.py b/erpnext/patches/v12_0/set_job_offer_applicant_email.py index 7dd8492081e..0e3b5c4d2aa 100644 --- a/erpnext/patches/v12_0/set_job_offer_applicant_email.py +++ b/erpnext/patches/v12_0/set_job_offer_applicant_email.py @@ -4,9 +4,11 @@ import frappe def execute(): frappe.reload_doc("hr", "doctype", "job_offer") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabJob Offer` AS offer SET applicant_email = (SELECT email_id FROM `tabJob Applicant` WHERE name = offer.job_applicant) - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_lead_title_field.py b/erpnext/patches/v12_0/set_lead_title_field.py index 86e00038f6c..eda3007e29b 100644 --- a/erpnext/patches/v12_0/set_lead_title_field.py +++ b/erpnext/patches/v12_0/set_lead_title_field.py @@ -3,9 +3,11 @@ import frappe def execute(): frappe.reload_doc("crm", "doctype", "lead") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabLead` SET title = IF(organization_lead = 1, company_name, lead_name) - """) + """ + ) diff --git a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py index a8e0ec1f816..4d19007313d 100644 --- a/erpnext/patches/v12_0/set_multi_uom_in_rfq.py +++ b/erpnext/patches/v12_0/set_multi_uom_in_rfq.py @@ -6,10 +6,12 @@ import frappe def execute(): - frappe.reload_doc('buying', 'doctype', 'request_for_quotation_item') + frappe.reload_doc("buying", "doctype", "request_for_quotation_item") - frappe.db.sql("""UPDATE `tabRequest for Quotation Item` + frappe.db.sql( + """UPDATE `tabRequest for Quotation Item` SET stock_uom = uom, conversion_factor = 1, - stock_qty = qty""") + stock_qty = qty""" + ) diff --git a/erpnext/patches/v12_0/set_payment_entry_status.py b/erpnext/patches/v12_0/set_payment_entry_status.py index f8792952d8b..2a3a3ad45e4 100644 --- a/erpnext/patches/v12_0/set_payment_entry_status.py +++ b/erpnext/patches/v12_0/set_payment_entry_status.py @@ -3,8 +3,10 @@ import frappe def execute(): frappe.reload_doctype("Payment Entry") - frappe.db.sql("""update `tabPayment Entry` set status = CASE + frappe.db.sql( + """update `tabPayment Entry` set status = CASE WHEN docstatus = 1 THEN 'Submitted' WHEN docstatus = 2 THEN 'Cancelled' ELSE 'Draft' - END;""") + END;""" + ) diff --git a/erpnext/patches/v12_0/set_permission_einvoicing.py b/erpnext/patches/v12_0/set_permission_einvoicing.py index 01cab14db9d..65d70977d20 100644 --- a/erpnext/patches/v12_0/set_permission_einvoicing.py +++ b/erpnext/patches/v12_0/set_permission_einvoicing.py @@ -5,7 +5,7 @@ from erpnext.regional.italy.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'Italy'}) + company = frappe.get_all("Company", filters={"country": "Italy"}) if not company: return @@ -14,6 +14,6 @@ def execute(): frappe.reload_doc("regional", "doctype", "import_supplier_invoice") - add_permission('Import Supplier Invoice', 'Accounts Manager', 0) - update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'write', 1) - update_permission_property('Import Supplier Invoice', 'Accounts Manager', 0, 'create', 1) + add_permission("Import Supplier Invoice", "Accounts Manager", 0) + update_permission_property("Import Supplier Invoice", "Accounts Manager", 0, "write", 1) + update_permission_property("Import Supplier Invoice", "Accounts Manager", 0, "create", 1) diff --git a/erpnext/patches/v12_0/set_priority_for_support.py b/erpnext/patches/v12_0/set_priority_for_support.py index 6d7d0993460..a8a07e76eab 100644 --- a/erpnext/patches/v12_0/set_priority_for_support.py +++ b/erpnext/patches/v12_0/set_priority_for_support.py @@ -4,21 +4,20 @@ import frappe def execute(): frappe.reload_doc("support", "doctype", "issue_priority") frappe.reload_doc("support", "doctype", "service_level_priority") - frappe.reload_doc('support', 'doctype', 'issue') + frappe.reload_doc("support", "doctype", "issue") set_issue_priority() set_priority_for_issue() set_priorities_service_level() set_priorities_service_level_agreement() + def set_issue_priority(): # Adds priority from issue to Issue Priority DocType as Priority is a new DocType. for priority in frappe.get_meta("Issue").get_field("priority").options.split("\n"): if priority and not frappe.db.exists("Issue Priority", priority): - frappe.get_doc({ - "doctype": "Issue Priority", - "name": priority - }).insert(ignore_permissions=True) + frappe.get_doc({"doctype": "Issue Priority", "name": priority}).insert(ignore_permissions=True) + def set_priority_for_issue(): # Sets priority for Issues as Select field is changed to Link field. @@ -28,38 +27,63 @@ def set_priority_for_issue(): for issue in issue_priority: frappe.db.set_value("Issue", issue.name, "priority", issue.priority) + def set_priorities_service_level(): # Migrates "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period" to Child Table # as a Service Level can have multiple priorities try: - service_level_priorities = frappe.get_list("Service Level", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"]) + service_level_priorities = frappe.get_list( + "Service Level", + fields=[ + "name", + "priority", + "response_time", + "response_time_period", + "resolution_time", + "resolution_time_period", + ], + ) frappe.reload_doc("support", "doctype", "service_level") frappe.reload_doc("support", "doctype", "support_settings") - frappe.db.set_value('Support Settings', None, 'track_service_level_agreement', 1) + frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) for service_level in service_level_priorities: if service_level: doc = frappe.get_doc("Service Level", service_level.name) if not doc.priorities: - doc.append("priorities", { - "priority": service_level.priority, - "default_priority": 1, - "response_time": service_level.response_time, - "response_time_period": service_level.response_time_period, - "resolution_time": service_level.resolution_time, - "resolution_time_period": service_level.resolution_time_period - }) + doc.append( + "priorities", + { + "priority": service_level.priority, + "default_priority": 1, + "response_time": service_level.response_time, + "response_time_period": service_level.response_time_period, + "resolution_time": service_level.resolution_time, + "resolution_time_period": service_level.resolution_time_period, + }, + ) doc.flags.ignore_validate = True doc.save(ignore_permissions=True) except frappe.db.TableMissingError: frappe.reload_doc("support", "doctype", "service_level") + def set_priorities_service_level_agreement(): # Migrates "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period" to Child Table # as a Service Level Agreement can have multiple priorities try: - service_level_agreement_priorities = frappe.get_list("Service Level Agreement", fields=["name", "priority", "response_time", "response_time_period", "resolution_time", "resolution_time_period"]) + service_level_agreement_priorities = frappe.get_list( + "Service Level Agreement", + fields=[ + "name", + "priority", + "response_time", + "response_time_period", + "resolution_time", + "resolution_time_period", + ], + ) frappe.reload_doc("support", "doctype", "service_level_agreement") @@ -71,14 +95,17 @@ def set_priorities_service_level_agreement(): doc.entity_type = "Customer" doc.entity = doc.customer - doc.append("priorities", { - "priority": service_level_agreement.priority, - "default_priority": 1, - "response_time": service_level_agreement.response_time, - "response_time_period": service_level_agreement.response_time_period, - "resolution_time": service_level_agreement.resolution_time, - "resolution_time_period": service_level_agreement.resolution_time_period - }) + doc.append( + "priorities", + { + "priority": service_level_agreement.priority, + "default_priority": 1, + "response_time": service_level_agreement.response_time, + "response_time_period": service_level_agreement.response_time_period, + "resolution_time": service_level_agreement.resolution_time, + "resolution_time_period": service_level_agreement.resolution_time_period, + }, + ) doc.flags.ignore_validate = True doc.save(ignore_permissions=True) except frappe.db.TableMissingError: diff --git a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py index 9c851ddcee1..562ebed757b 100644 --- a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py +++ b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py @@ -4,12 +4,14 @@ from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_ def execute(): - frappe.reload_doctype('Sales Order Item') - frappe.reload_doctype('Sales Order') + frappe.reload_doctype("Sales Order Item") + frappe.reload_doctype("Sales Order") - for d in frappe.get_all('Work Order', - fields = ['sales_order', 'sales_order_item'], - filters={'sales_order': ('!=', ''), 'sales_order_item': ('!=', '')}): + for d in frappe.get_all( + "Work Order", + fields=["sales_order", "sales_order_item"], + filters={"sales_order": ("!=", ""), "sales_order_item": ("!=", "")}, + ): # update produced qty in sales order update_produced_qty_in_so_item(d.sales_order, d.sales_order_item) diff --git a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py index 14956a23b43..0246c35447b 100644 --- a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py +++ b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py @@ -1,9 +1,10 @@ - import frappe def execute(): - frappe.reload_doc("manufacturing", "doctype", "workstation") + frappe.reload_doc("manufacturing", "doctype", "workstation") - frappe.db.sql(""" UPDATE `tabWorkstation` - SET production_capacity = 1 """) + frappe.db.sql( + """ UPDATE `tabWorkstation` + SET production_capacity = 1 """ + ) diff --git a/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py b/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py index a0fe8aa2fe8..d90464ea8e5 100644 --- a/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py +++ b/erpnext/patches/v12_0/set_published_in_hub_tracked_item.py @@ -1,4 +1,3 @@ - import frappe @@ -7,7 +6,9 @@ def execute(): if not frappe.db.a_row_exists("Hub Tracked Item"): return - frappe.db.sql(''' + frappe.db.sql( + """ Update `tabHub Tracked Item` SET published = 1 - ''') + """ + ) diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py index 05b86f3204a..2edf0f54fcf 100644 --- a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py +++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py @@ -1,4 +1,3 @@ - from collections import defaultdict import frappe @@ -6,29 +5,36 @@ import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'delivery_note_item', force=True) - frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item', force=True) + frappe.reload_doc("stock", "doctype", "delivery_note_item", force=True) + frappe.reload_doc("stock", "doctype", "purchase_receipt_item", force=True) def map_rows(doc_row, return_doc_row, detail_field, doctype): """Map rows after identifying similar ones.""" - frappe.db.sql(""" UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}' - where name = '{return_doc_row_name}'""" \ - .format(doctype=doctype, - detail_field=detail_field, - doc_row_name=doc_row.get('name'), - return_doc_row_name=return_doc_row.get('name'))) #nosec + frappe.db.sql( + """ UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}' + where name = '{return_doc_row_name}'""".format( + doctype=doctype, + detail_field=detail_field, + doc_row_name=doc_row.get("name"), + return_doc_row_name=return_doc_row.get("name"), + ) + ) # nosec def row_is_mappable(doc_row, return_doc_row, detail_field): """Checks if two rows are similar enough to be mapped.""" if doc_row.item_code == return_doc_row.item_code and not return_doc_row.get(detail_field): - if doc_row.get('batch_no') and return_doc_row.get('batch_no') and doc_row.batch_no == return_doc_row.batch_no: + if ( + doc_row.get("batch_no") + and return_doc_row.get("batch_no") + and doc_row.batch_no == return_doc_row.batch_no + ): return True - elif doc_row.get('serial_no') and return_doc_row.get('serial_no'): - doc_sn = doc_row.serial_no.split('\n') - return_doc_sn = return_doc_row.serial_no.split('\n') + elif doc_row.get("serial_no") and return_doc_row.get("serial_no"): + doc_sn = doc_row.serial_no.split("\n") + return_doc_sn = return_doc_row.serial_no.split("\n") if set(doc_sn) & set(return_doc_sn): # if two rows have serial nos in common, map them @@ -43,12 +49,17 @@ def execute(): """Returns a map of documents and it's return documents. Format => { 'document' : ['return_document_1','return_document_2'] }""" - return_against_documents = frappe.db.sql(""" + return_against_documents = frappe.db.sql( + """ SELECT return_against as document, name as return_document FROM `tab{doctype}` WHERE - is_return = 1 and docstatus = 1""".format(doctype=doctype),as_dict=1) #nosec + is_return = 1 and docstatus = 1""".format( + doctype=doctype + ), + as_dict=1, + ) # nosec for entry in return_against_documents: return_document_map[entry.document].append(entry.return_document) @@ -59,7 +70,7 @@ def execute(): """Map each row of the original document in the return document.""" mapped = [] return_document_map = defaultdict(list) - detail_field = "purchase_receipt_item" if doctype=="Purchase Receipt" else "dn_detail" + detail_field = "purchase_receipt_item" if doctype == "Purchase Receipt" else "dn_detail" child_doc = frappe.scrub("{0} Item".format(doctype)) frappe.reload_doc("stock", "doctype", child_doc) @@ -68,25 +79,27 @@ def execute(): count = 0 - #iterate through original documents and its return documents + # iterate through original documents and its return documents for docname in return_document_map: doc_items = frappe.get_cached_doc(doctype, docname).get("items") for return_doc in return_document_map[docname]: return_doc_items = frappe.get_cached_doc(doctype, return_doc).get("items") - #iterate through return document items and original document items for mapping + # iterate through return document items and original document items for mapping for return_item in return_doc_items: for doc_item in doc_items: - if row_is_mappable(doc_item, return_item, detail_field) and doc_item.get('name') not in mapped: + if ( + row_is_mappable(doc_item, return_item, detail_field) and doc_item.get("name") not in mapped + ): map_rows(doc_item, return_item, detail_field, doctype) - mapped.append(doc_item.get('name')) + mapped.append(doc_item.get("name")) break else: continue # commit after every 100 sql updates count += 1 - if count%100 == 0: + if count % 100 == 0: frappe.db.commit() set_document_detail_in_return_document("Purchase Receipt") diff --git a/erpnext/patches/v12_0/set_quotation_status.py b/erpnext/patches/v12_0/set_quotation_status.py index adfc5e96798..bebedd3a498 100644 --- a/erpnext/patches/v12_0/set_quotation_status.py +++ b/erpnext/patches/v12_0/set_quotation_status.py @@ -1,8 +1,9 @@ - import frappe def execute(): - frappe.db.sql(""" UPDATE `tabQuotation` set status = 'Open' - where docstatus = 1 and status = 'Submitted' """) + frappe.db.sql( + """ UPDATE `tabQuotation` set status = 'Open' + where docstatus = 1 and status = 'Submitted' """ + ) diff --git a/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py b/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py index 83db7961d9d..dcdd19fbb65 100644 --- a/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py +++ b/erpnext/patches/v12_0/set_received_qty_in_material_request_as_per_stock_uom.py @@ -1,15 +1,17 @@ - import frappe def execute(): - purchase_receipts = frappe.db.sql(""" + purchase_receipts = frappe.db.sql( + """ SELECT parent from `tabPurchase Receipt Item` WHERE material_request is not null AND docstatus=1 - """,as_dict=1) + """, + as_dict=1, + ) purchase_receipts = set([d.parent for d in purchase_receipts]) @@ -17,15 +19,15 @@ def execute(): doc = frappe.get_doc("Purchase Receipt", pr) doc.status_updater = [ { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Material Request Item', - 'join_field': 'material_request_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Material Request', - 'target_parent_field': 'per_received', - 'target_ref_field': 'stock_qty', - 'source_field': 'stock_qty', - 'percent_join_field': 'material_request' + "source_dt": "Purchase Receipt Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "received_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_received", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + "percent_join_field": "material_request", } ] doc.update_qty() diff --git a/erpnext/patches/v12_0/set_serial_no_status.py b/erpnext/patches/v12_0/set_serial_no_status.py index 57206ced3e2..8ab342e9f2b 100644 --- a/erpnext/patches/v12_0/set_serial_no_status.py +++ b/erpnext/patches/v12_0/set_serial_no_status.py @@ -1,20 +1,24 @@ - import frappe from frappe.utils import getdate, nowdate def execute(): - frappe.reload_doc('stock', 'doctype', 'serial_no') + frappe.reload_doc("stock", "doctype", "serial_no") - serial_no_list = frappe.db.sql("""select name, delivery_document_type, warranty_expiry_date, warehouse from `tabSerial No` - where (status is NULL OR status='')""", as_dict = 1) + serial_no_list = frappe.db.sql( + """select name, delivery_document_type, warranty_expiry_date, warehouse from `tabSerial No` + where (status is NULL OR status='')""", + as_dict=1, + ) if len(serial_no_list) > 20000: frappe.db.auto_commit_on_many_writes = True for serial_no in serial_no_list: if serial_no.get("delivery_document_type"): status = "Delivered" - elif serial_no.get("warranty_expiry_date") and getdate(serial_no.get("warranty_expiry_date")) <= getdate(nowdate()): + elif serial_no.get("warranty_expiry_date") and getdate( + serial_no.get("warranty_expiry_date") + ) <= getdate(nowdate()): status = "Expired" elif not serial_no.get("warehouse"): status = "Inactive" diff --git a/erpnext/patches/v12_0/set_task_status.py b/erpnext/patches/v12_0/set_task_status.py index 1b4955a75be..1c6654e57ac 100644 --- a/erpnext/patches/v12_0/set_task_status.py +++ b/erpnext/patches/v12_0/set_task_status.py @@ -2,14 +2,16 @@ import frappe def execute(): - frappe.reload_doctype('Task') + frappe.reload_doctype("Task") # add "Completed" if customized - property_setter_name = frappe.db.exists('Property Setter', dict(doc_type='Task', field_name = 'status', property = 'options')) + property_setter_name = frappe.db.exists( + "Property Setter", dict(doc_type="Task", field_name="status", property="options") + ) if property_setter_name: - property_setter = frappe.get_doc('Property Setter', property_setter_name) + property_setter = frappe.get_doc("Property Setter", property_setter_name) if not "Completed" in property_setter.value: - property_setter.value = property_setter.value + '\nCompleted' + property_setter.value = property_setter.value + "\nCompleted" property_setter.save() # renamed default status to Completed as status "Closed" is ambiguous diff --git a/erpnext/patches/v12_0/set_total_batch_quantity.py b/erpnext/patches/v12_0/set_total_batch_quantity.py index 7296eaa33d8..068e0a6a4c1 100644 --- a/erpnext/patches/v12_0/set_total_batch_quantity.py +++ b/erpnext/patches/v12_0/set_total_batch_quantity.py @@ -5,7 +5,12 @@ def execute(): frappe.reload_doc("stock", "doctype", "batch") for batch in frappe.get_all("Batch", fields=["name", "batch_id"]): - batch_qty = frappe.db.get_value("Stock Ledger Entry", - {"docstatus": 1, "batch_no": batch.batch_id, "is_cancelled": 0}, - "sum(actual_qty)") or 0.0 + batch_qty = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"docstatus": 1, "batch_no": batch.batch_id, "is_cancelled": 0}, + "sum(actual_qty)", + ) + or 0.0 + ) frappe.db.set_value("Batch", batch.name, "batch_qty", batch_qty, update_modified=False) diff --git a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py index 300d0f2ba47..1e390819cd5 100644 --- a/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py +++ b/erpnext/patches/v12_0/set_updated_purpose_in_pick_list.py @@ -6,6 +6,8 @@ import frappe def execute(): - frappe.reload_doc("stock", "doctype", "pick_list") - frappe.db.sql("""UPDATE `tabPick List` set purpose = 'Delivery' - WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """) + frappe.reload_doc("stock", "doctype", "pick_list") + frappe.db.sql( + """UPDATE `tabPick List` set purpose = 'Delivery' + WHERE docstatus = 1 and purpose = 'Delivery against Sales Order' """ + ) diff --git a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py index d79628f2a90..94322cd1971 100644 --- a/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py +++ b/erpnext/patches/v12_0/set_valid_till_date_in_supplier_quotation.py @@ -1,9 +1,10 @@ - import frappe def execute(): frappe.reload_doc("buying", "doctype", "supplier_quotation") - frappe.db.sql("""UPDATE `tabSupplier Quotation` + frappe.db.sql( + """UPDATE `tabSupplier Quotation` SET valid_till = DATE_ADD(transaction_date , INTERVAL 1 MONTH) - WHERE docstatus < 2""") + WHERE docstatus < 2""" + ) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index af2e60fd79a..f704f977aa3 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -6,53 +5,128 @@ from erpnext.regional.india.setup import add_permissions, add_print_formats def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return frappe.reload_doc("custom", "doctype", "custom_field") frappe.reload_doc("regional", "doctype", "e_invoice_settings") custom_fields = { - 'Sales Invoice': [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, - depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - - dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), - - dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - - dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - - dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - - dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + "Sales Invoice": [ + dict( + fieldname="irn", + label="IRN", + fieldtype="Data", + read_only=1, + insert_after="customer", + no_copy=1, + print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0', + ), + dict( + fieldname="ack_no", + label="Ack. No.", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="irn", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="ack_date", + label="Ack. Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_no", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="irn_cancelled", + label="IRN Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval:(doc.irn_cancelled === 1)", + read_only=1, + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="eway_bill_cancelled", + label="E-Way Bill Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval:(doc.eway_bill_cancelled === 1)", + read_only=1, + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="signed_einvoice", + fieldtype="Code", + options="JSON", + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="signed_qr_code", + fieldtype="Code", + options="JSON", + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="qrcode_image", + label="QRCode", + fieldtype="Attach Image", + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), ] } create_custom_fields(custom_fields, update=True) add_permissions() add_print_formats() - einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + einvoice_cond = ( + 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + ) t = { - 'mode_of_transport': [{'default': None}], - 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}], - 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], - 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], - 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}], - 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}], - 'ewaybill': [ - {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'}, - {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'} - ] + "mode_of_transport": [{"default": None}], + "distance": [{"mandatory_depends_on": f"eval:{einvoice_cond} && doc.transporter"}], + "gst_vehicle_type": [ + {"mandatory_depends_on": f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'} + ], + "lr_date": [ + { + "mandatory_depends_on": f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)' + } + ], + "lr_no": [ + { + "mandatory_depends_on": f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)' + } + ], + "vehicle_no": [ + {"mandatory_depends_on": f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'} + ], + "ewaybill": [ + {"read_only_depends_on": "eval:doc.irn && doc.ewaybill"}, + {"depends_on": "eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)"}, + ], } for field, conditions in t.items(): for c in conditions: [(prop, value)] = c.items() - frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value) + frappe.db.set_value("Custom Field", {"fieldname": field}, prop, value) diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py index 630a9046a4e..5508d260660 100644 --- a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py +++ b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py @@ -1,13 +1,14 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'}) + irn_cancelled_field = frappe.db.exists( + "Custom Field", {"dt": "Sales Invoice", "fieldname": "irn_cancelled"} + ) if irn_cancelled_field: - frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn') - frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0) + frappe.db.set_value("Custom Field", irn_cancelled_field, "depends_on", "eval: doc.irn") + frappe.db.set_value("Custom Field", irn_cancelled_field, "read_only", 0) diff --git a/erpnext/patches/v12_0/stock_entry_enhancements.py b/erpnext/patches/v12_0/stock_entry_enhancements.py index 94d8ff9cde3..db099a304cf 100644 --- a/erpnext/patches/v12_0/stock_entry_enhancements.py +++ b/erpnext/patches/v12_0/stock_entry_enhancements.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -10,44 +9,61 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): create_stock_entry_types() - company = frappe.db.get_value("Company", {'country': 'India'}, 'name') + company = frappe.db.get_value("Company", {"country": "India"}, "name") if company: add_gst_hsn_code_field() + def create_stock_entry_types(): - frappe.reload_doc('stock', 'doctype', 'stock_entry_type') - frappe.reload_doc('stock', 'doctype', 'stock_entry') + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.reload_doc("stock", "doctype", "stock_entry") - for purpose in ["Material Issue", "Material Receipt", "Material Transfer", - "Material Transfer for Manufacture", "Material Consumption for Manufacture", "Manufacture", - "Repack", "Send to Subcontractor"]: + for purpose in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + ]: - ste_type = frappe.get_doc({ - 'doctype': 'Stock Entry Type', - 'name': purpose, - 'purpose': purpose - }) + ste_type = frappe.get_doc({"doctype": "Stock Entry Type", "name": purpose, "purpose": purpose}) try: ste_type.insert() except frappe.DuplicateEntryError: pass - frappe.db.sql(" UPDATE `tabStock Entry` set purpose = 'Send to Subcontractor' where purpose = 'Subcontract'") + frappe.db.sql( + " UPDATE `tabStock Entry` set purpose = 'Send to Subcontractor' where purpose = 'Subcontract'" + ) frappe.db.sql(" UPDATE `tabStock Entry` set stock_entry_type = purpose ") + def add_gst_hsn_code_field(): custom_fields = { - 'Stock Entry Detail': [dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', fetch_from='item_code.gst_hsn_code', - insert_after='description', allow_on_submit=1, print_hide=0)] + "Stock Entry Detail": [ + dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Data", + fetch_from="item_code.gst_hsn_code", + insert_after="description", + allow_on_submit=1, + print_hide=0, + ) + ] } - create_custom_fields(custom_fields, ignore_validate = frappe.flags.in_patch, update=True) + create_custom_fields(custom_fields, ignore_validate=frappe.flags.in_patch, update=True) - frappe.db.sql(""" update `tabStock Entry Detail`, `tabItem` + frappe.db.sql( + """ update `tabStock Entry Detail`, `tabItem` SET `tabStock Entry Detail`.gst_hsn_code = `tabItem`.gst_hsn_code Where `tabItem`.name = `tabStock Entry Detail`.item_code and `tabItem`.gst_hsn_code is not null - """) + """ + ) diff --git a/erpnext/patches/v12_0/unhide_cost_center_field.py b/erpnext/patches/v12_0/unhide_cost_center_field.py index 72450212872..5f91ef6260a 100644 --- a/erpnext/patches/v12_0/unhide_cost_center_field.py +++ b/erpnext/patches/v12_0/unhide_cost_center_field.py @@ -6,9 +6,11 @@ import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabProperty Setter` WHERE doc_type in ('Sales Invoice', 'Purchase Invoice', 'Payment Entry') AND field_name = 'cost_center' AND property = 'hidden' - """) + """ + ) diff --git a/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py index 1a677f91672..332609b8466 100644 --- a/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py +++ b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py @@ -1,29 +1,30 @@ - import frappe def execute(): - """ - set proper customer and supplier details for item price - based on selling and buying values - """ + """ + set proper customer and supplier details for item price + based on selling and buying values + """ - # update for selling - frappe.db.sql( - """UPDATE `tabItem Price` ip, `tabPrice List` pl + # update for selling + frappe.db.sql( + """UPDATE `tabItem Price` ip, `tabPrice List` pl SET ip.`reference` = ip.`customer`, ip.`supplier` = NULL WHERE ip.`selling` = 1 AND ip.`buying` = 0 AND (ip.`supplier` IS NOT NULL OR ip.`supplier` = '') AND ip.`price_list` = pl.`name` - AND pl.`enabled` = 1""") + AND pl.`enabled` = 1""" + ) - # update for buying - frappe.db.sql( - """UPDATE `tabItem Price` ip, `tabPrice List` pl + # update for buying + frappe.db.sql( + """UPDATE `tabItem Price` ip, `tabPrice List` pl SET ip.`reference` = ip.`supplier`, ip.`customer` = NULL WHERE ip.`selling` = 0 AND ip.`buying` = 1 AND (ip.`customer` IS NOT NULL OR ip.`customer` = '') AND ip.`price_list` = pl.`name` - AND pl.`enabled` = 1""") + AND pl.`enabled` = 1""" + ) diff --git a/erpnext/patches/v12_0/update_address_template_for_india.py b/erpnext/patches/v12_0/update_address_template_for_india.py index 64a2e41587f..27b1bb6dc18 100644 --- a/erpnext/patches/v12_0/update_address_template_for_india.py +++ b/erpnext/patches/v12_0/update_address_template_for_india.py @@ -8,7 +8,7 @@ from erpnext.regional.address_template.setup import set_up_address_templates def execute(): - if frappe.db.get_value('Company', {'country': 'India'}, 'name'): - address_template = frappe.db.get_value('Address Template', 'India', 'template') + if frappe.db.get_value("Company", {"country": "India"}, "name"): + address_template = frappe.db.get_value("Address Template", "India", "template") if not address_template or "gstin" not in address_template: - set_up_address_templates(default_country='India') + set_up_address_templates(default_country="India") diff --git a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py index 024cb2b7630..725ff1e4797 100644 --- a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py +++ b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py @@ -2,7 +2,9 @@ import frappe def execute(): - job = frappe.db.exists('Scheduled Job Type', 'patient_appointment.send_appointment_reminder') + job = frappe.db.exists("Scheduled Job Type", "patient_appointment.send_appointment_reminder") if job: - method = 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder' - frappe.db.set_value('Scheduled Job Type', job, 'method', method) + method = ( + "erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" + ) + frappe.db.set_value("Scheduled Job Type", job, "method", method) diff --git a/erpnext/patches/v12_0/update_bom_in_so_mr.py b/erpnext/patches/v12_0/update_bom_in_so_mr.py index ee9f90df2a4..114f65d100e 100644 --- a/erpnext/patches/v12_0/update_bom_in_so_mr.py +++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py @@ -1,4 +1,3 @@ - import frappe @@ -11,11 +10,15 @@ def execute(): if doctype == "Material Request": condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'" - frappe.db.sql(""" UPDATE `tab{doc}` as doc, `tab{doc} Item` as child_doc, tabItem as item + frappe.db.sql( + """ UPDATE `tab{doc}` as doc, `tab{doc} Item` as child_doc, tabItem as item SET child_doc.bom_no = item.default_bom WHERE child_doc.item_code = item.name and child_doc.docstatus < 2 and child_doc.parent = doc.name and item.default_bom is not null and item.default_bom != '' {cond} - """.format(doc = doctype, cond = condition)) + """.format( + doc=doctype, cond=condition + ) + ) diff --git a/erpnext/patches/v12_0/update_due_date_in_gle.py b/erpnext/patches/v12_0/update_due_date_in_gle.py index 032c2bb9538..a1c4f51ad01 100644 --- a/erpnext/patches/v12_0/update_due_date_in_gle.py +++ b/erpnext/patches/v12_0/update_due_date_in_gle.py @@ -1,18 +1,20 @@ - import frappe def execute(): - frappe.reload_doc("accounts", "doctype", "gl_entry") + frappe.reload_doc("accounts", "doctype", "gl_entry") - for doctype in ["Sales Invoice", "Purchase Invoice", "Journal Entry"]: - frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype)) + for doctype in ["Sales Invoice", "Purchase Invoice", "Journal Entry"]: + frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype)) - frappe.db.sql(""" UPDATE `tabGL Entry`, `tab{doctype}` + frappe.db.sql( + """ UPDATE `tabGL Entry`, `tab{doctype}` SET `tabGL Entry`.due_date = `tab{doctype}`.due_date WHERE `tabGL Entry`.voucher_no = `tab{doctype}`.name and `tabGL Entry`.party is not null and `tabGL Entry`.voucher_type in ('Sales Invoice', 'Purchase Invoice', 'Journal Entry') - and `tabGL Entry`.account in (select name from `tabAccount` where account_type in ('Receivable', 'Payable'))""" #nosec - .format(doctype=doctype)) + and `tabGL Entry`.account in (select name from `tabAccount` where account_type in ('Receivable', 'Payable'))""".format( # nosec + doctype=doctype + ) + ) diff --git a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py index 48febc5aa45..570b77b88e1 100644 --- a/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py +++ b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py @@ -1,25 +1,24 @@ - import frappe from frappe.utils import add_days, getdate, today def execute(): - if frappe.db.exists('DocType', 'Email Campaign'): - email_campaign = frappe.get_all('Email Campaign') - for campaign in email_campaign: - doc = frappe.get_doc("Email Campaign",campaign["name"]) - send_after_days = [] + if frappe.db.exists("DocType", "Email Campaign"): + email_campaign = frappe.get_all("Email Campaign") + for campaign in email_campaign: + doc = frappe.get_doc("Email Campaign", campaign["name"]) + send_after_days = [] - camp = frappe.get_doc("Campaign", doc.campaign_name) - for entry in camp.get("campaign_schedules"): - send_after_days.append(entry.send_after_days) - if send_after_days: - end_date = add_days(getdate(doc.start_date), max(send_after_days)) - doc.db_set("end_date", end_date) - today_date = getdate(today()) - if doc.start_date > today_date: - doc.db_set("status", "Scheduled") - elif end_date >= today_date: - doc.db_set("status", "In Progress") - elif end_date < today_date: - doc.db_set("status", "Completed") + camp = frappe.get_doc("Campaign", doc.campaign_name) + for entry in camp.get("campaign_schedules"): + send_after_days.append(entry.send_after_days) + if send_after_days: + end_date = add_days(getdate(doc.start_date), max(send_after_days)) + doc.db_set("end_date", end_date) + today_date = getdate(today()) + if doc.start_date > today_date: + doc.db_set("status", "Scheduled") + elif end_date >= today_date: + doc.db_set("status", "In Progress") + elif end_date < today_date: + doc.db_set("status", "Completed") diff --git a/erpnext/patches/v12_0/update_ewaybill_field_position.py b/erpnext/patches/v12_0/update_ewaybill_field_position.py index ace3aceebba..24a834b05c7 100644 --- a/erpnext/patches/v12_0/update_ewaybill_field_position.py +++ b/erpnext/patches/v12_0/update_ewaybill_field_position.py @@ -1,9 +1,8 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return @@ -15,14 +14,16 @@ def execute(): ewaybill_field.flags.ignore_validate = True - ewaybill_field.update({ - 'fieldname': 'ewaybill', - 'label': 'e-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', - 'allow_on_submit': 1, - 'insert_after': 'tax_id', - 'translatable': 0 - }) + ewaybill_field.update( + { + "fieldname": "ewaybill", + "label": "e-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:(doc.docstatus === 1)", + "allow_on_submit": 1, + "insert_after": "tax_id", + "translatable": 0, + } + ) ewaybill_field.save() diff --git a/erpnext/patches/v12_0/update_gst_category.py b/erpnext/patches/v12_0/update_gst_category.py index 20dce94f2ff..16168f022cd 100644 --- a/erpnext/patches/v12_0/update_gst_category.py +++ b/erpnext/patches/v12_0/update_gst_category.py @@ -1,20 +1,23 @@ - import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company: - return + company = frappe.get_all("Company", filters={"country": "India"}) + if not company: + return - frappe.db.sql(""" UPDATE `tabSales Invoice` set gst_category = 'Unregistered' + frappe.db.sql( + """ UPDATE `tabSales Invoice` set gst_category = 'Unregistered' where gst_category = 'Registered Regular' and ifnull(customer_gstin, '')='' and ifnull(billing_address_gstin,'')='' - """) + """ + ) - frappe.db.sql(""" UPDATE `tabPurchase Invoice` set gst_category = 'Unregistered' + frappe.db.sql( + """ UPDATE `tabPurchase Invoice` set gst_category = 'Unregistered' where gst_category = 'Registered Regular' and ifnull(supplier_gstin, '')='' - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py index 4e24a638f98..5ca0d5d47d9 100644 --- a/erpnext/patches/v12_0/update_healthcare_refactored_changes.py +++ b/erpnext/patches/v12_0/update_healthcare_refactored_changes.py @@ -1,79 +1,81 @@ - import frappe from frappe.model.utils.rename_field import rename_field from frappe.modules import get_doctype_module, scrub field_rename_map = { - 'Healthcare Settings': [ - ['patient_master_name', 'patient_name_by'], - ['max_visit', 'max_visits'], - ['reg_sms', 'send_registration_msg'], - ['reg_msg', 'registration_msg'], - ['app_con', 'send_appointment_confirmation'], - ['app_con_msg', 'appointment_confirmation_msg'], - ['no_con', 'avoid_confirmation'], - ['app_rem', 'send_appointment_reminder'], - ['app_rem_msg', 'appointment_reminder_msg'], - ['rem_before', 'remind_before'], - ['manage_customer', 'link_customer_to_patient'], - ['create_test_on_si_submit', 'create_lab_test_on_si_submit'], - ['require_sample_collection', 'create_sample_collection_for_lab_test'], - ['require_test_result_approval', 'lab_test_approval_required'], - ['manage_appointment_invoice_automatically', 'automate_appointment_invoicing'] + "Healthcare Settings": [ + ["patient_master_name", "patient_name_by"], + ["max_visit", "max_visits"], + ["reg_sms", "send_registration_msg"], + ["reg_msg", "registration_msg"], + ["app_con", "send_appointment_confirmation"], + ["app_con_msg", "appointment_confirmation_msg"], + ["no_con", "avoid_confirmation"], + ["app_rem", "send_appointment_reminder"], + ["app_rem_msg", "appointment_reminder_msg"], + ["rem_before", "remind_before"], + ["manage_customer", "link_customer_to_patient"], + ["create_test_on_si_submit", "create_lab_test_on_si_submit"], + ["require_sample_collection", "create_sample_collection_for_lab_test"], + ["require_test_result_approval", "lab_test_approval_required"], + ["manage_appointment_invoice_automatically", "automate_appointment_invoicing"], ], - 'Drug Prescription':[ - ['use_interval', 'usage_interval'], - ['in_every', 'interval_uom'] + "Drug Prescription": [["use_interval", "usage_interval"], ["in_every", "interval_uom"]], + "Lab Test Template": [ + ["sample_quantity", "sample_qty"], + ["sample_collection_details", "sample_details"], ], - 'Lab Test Template':[ - ['sample_quantity', 'sample_qty'], - ['sample_collection_details', 'sample_details'] + "Sample Collection": [ + ["sample_quantity", "sample_qty"], + ["sample_collection_details", "sample_details"], ], - 'Sample Collection':[ - ['sample_quantity', 'sample_qty'], - ['sample_collection_details', 'sample_details'] - ], - 'Fee Validity': [ - ['max_visit', 'max_visits'] - ] + "Fee Validity": [["max_visit", "max_visits"]], } + def execute(): for dn in field_rename_map: - if frappe.db.exists('DocType', dn): - if dn == 'Healthcare Settings': - frappe.reload_doctype('Healthcare Settings') + if frappe.db.exists("DocType", dn): + if dn == "Healthcare Settings": + frappe.reload_doctype("Healthcare Settings") else: frappe.reload_doc(get_doctype_module(dn), "doctype", scrub(dn)) for dt, field_list in field_rename_map.items(): - if frappe.db.exists('DocType', dt): + if frappe.db.exists("DocType", dt): for field in field_list: - if dt == 'Healthcare Settings': + if dt == "Healthcare Settings": rename_field(dt, field[0], field[1]) elif frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) # first name mandatory in Patient - if frappe.db.exists('DocType', 'Patient'): + if frappe.db.exists("DocType", "Patient"): patients = frappe.db.sql("select name, patient_name from `tabPatient`", as_dict=1) - frappe.reload_doc('healthcare', 'doctype', 'patient') + frappe.reload_doc("healthcare", "doctype", "patient") for entry in patients: - name = entry.patient_name.split(' ') - frappe.db.set_value('Patient', entry.name, 'first_name', name[0]) + name = entry.patient_name.split(" ") + frappe.db.set_value("Patient", entry.name, "first_name", name[0]) # mark Healthcare Practitioner status as Disabled - if frappe.db.exists('DocType', 'Healthcare Practitioner'): - practitioners = frappe.db.sql("select name from `tabHealthcare Practitioner` where 'active'= 0", as_dict=1) + if frappe.db.exists("DocType", "Healthcare Practitioner"): + practitioners = frappe.db.sql( + "select name from `tabHealthcare Practitioner` where 'active'= 0", as_dict=1 + ) practitioners_lst = [p.name for p in practitioners] - frappe.reload_doc('healthcare', 'doctype', 'healthcare_practitioner') + frappe.reload_doc("healthcare", "doctype", "healthcare_practitioner") if practitioners_lst: - frappe.db.sql("update `tabHealthcare Practitioner` set status = 'Disabled' where name IN %(practitioners)s""", {"practitioners": practitioners_lst}) + frappe.db.sql( + "update `tabHealthcare Practitioner` set status = 'Disabled' where name IN %(practitioners)s" + "", + {"practitioners": practitioners_lst}, + ) # set Clinical Procedure status - if frappe.db.exists('DocType', 'Clinical Procedure'): - frappe.reload_doc('healthcare', 'doctype', 'clinical_procedure') - frappe.db.sql(""" + if frappe.db.exists("DocType", "Clinical Procedure"): + frappe.reload_doc("healthcare", "doctype", "clinical_procedure") + frappe.db.sql( + """ UPDATE `tabClinical Procedure` SET @@ -81,57 +83,49 @@ def execute(): WHEN status = 'Draft' THEN 0 ELSE 1 END) - """) + """ + ) # set complaints and diagnosis in table multiselect in Patient Encounter - if frappe.db.exists('DocType', 'Patient Encounter'): - field_list = [ - ['visit_department', 'medical_department'], - ['type', 'appointment_type'] - ] - encounter_details = frappe.db.sql("""select symptoms, diagnosis, name from `tabPatient Encounter`""", as_dict=True) - frappe.reload_doc('healthcare', 'doctype', 'patient_encounter') - frappe.reload_doc('healthcare', 'doctype', 'patient_encounter_symptom') - frappe.reload_doc('healthcare', 'doctype', 'patient_encounter_diagnosis') + if frappe.db.exists("DocType", "Patient Encounter"): + field_list = [["visit_department", "medical_department"], ["type", "appointment_type"]] + encounter_details = frappe.db.sql( + """select symptoms, diagnosis, name from `tabPatient Encounter`""", as_dict=True + ) + frappe.reload_doc("healthcare", "doctype", "patient_encounter") + frappe.reload_doc("healthcare", "doctype", "patient_encounter_symptom") + frappe.reload_doc("healthcare", "doctype", "patient_encounter_diagnosis") for field in field_list: if frappe.db.has_column(dt, field[0]): rename_field(dt, field[0], field[1]) for entry in encounter_details: - doc = frappe.get_doc('Patient Encounter', entry.name) - symptoms = entry.symptoms.split('\n') if entry.symptoms else [] + doc = frappe.get_doc("Patient Encounter", entry.name) + symptoms = entry.symptoms.split("\n") if entry.symptoms else [] for symptom in symptoms: - if not frappe.db.exists('Complaint', symptom): - frappe.get_doc({ - 'doctype': 'Complaint', - 'complaints': symptom - }).insert() - row = doc.append('symptoms', { - 'complaint': symptom - }) + if not frappe.db.exists("Complaint", symptom): + frappe.get_doc({"doctype": "Complaint", "complaints": symptom}).insert() + row = doc.append("symptoms", {"complaint": symptom}) row.db_update() - diagnosis = entry.diagnosis.split('\n') if entry.diagnosis else [] + diagnosis = entry.diagnosis.split("\n") if entry.diagnosis else [] for d in diagnosis: - if not frappe.db.exists('Diagnosis', d): - frappe.get_doc({ - 'doctype': 'Diagnosis', - 'diagnosis': d - }).insert() - row = doc.append('diagnosis', { - 'diagnosis': d - }) + if not frappe.db.exists("Diagnosis", d): + frappe.get_doc({"doctype": "Diagnosis", "diagnosis": d}).insert() + row = doc.append("diagnosis", {"diagnosis": d}) row.db_update() doc.db_update() - if frappe.db.exists('DocType', 'Fee Validity'): + if frappe.db.exists("DocType", "Fee Validity"): # update fee validity status - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabFee Validity` SET status = (CASE WHEN visited >= max_visits THEN 'Completed' ELSE 'Pending' END) - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py index 06b6673a5d2..b567823b062 100644 --- a/erpnext/patches/v12_0/update_is_cancelled_field.py +++ b/erpnext/patches/v12_0/update_is_cancelled_field.py @@ -1,30 +1,36 @@ - import frappe def execute(): - #handle type casting for is_cancelled field + # handle type casting for is_cancelled field module_doctypes = ( - ('stock', 'Stock Ledger Entry'), - ('stock', 'Serial No'), - ('accounts', 'GL Entry') + ("stock", "Stock Ledger Entry"), + ("stock", "Serial No"), + ("accounts", "GL Entry"), ) for module, doctype in module_doctypes: - if (not frappe.db.has_column(doctype, "is_cancelled") + if ( + not frappe.db.has_column(doctype, "is_cancelled") or frappe.db.get_column_type(doctype, "is_cancelled").lower() == "int(1)" ): continue - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{doctype}` SET is_cancelled = 0 - where is_cancelled in ('', NULL, 'No')""" - .format(doctype=doctype)) - frappe.db.sql(""" + where is_cancelled in ('', NULL, 'No')""".format( + doctype=doctype + ) + ) + frappe.db.sql( + """ UPDATE `tab{doctype}` SET is_cancelled = 1 - where is_cancelled = 'Yes'""" - .format(doctype=doctype)) + where is_cancelled = 'Yes'""".format( + doctype=doctype + ) + ) frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) diff --git a/erpnext/patches/v12_0/update_item_tax_template_company.py b/erpnext/patches/v12_0/update_item_tax_template_company.py index a737cb2dfa5..489f70d4497 100644 --- a/erpnext/patches/v12_0/update_item_tax_template_company.py +++ b/erpnext/patches/v12_0/update_item_tax_template_company.py @@ -1,14 +1,13 @@ - import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'item_tax_template') + frappe.reload_doc("accounts", "doctype", "item_tax_template") - item_tax_template_list = frappe.get_list('Item Tax Template') - for template in item_tax_template_list: - doc = frappe.get_doc('Item Tax Template', template.name) - for tax in doc.taxes: - doc.company = frappe.get_value('Account', tax.tax_type, 'company') - break - doc.save() + item_tax_template_list = frappe.get_list("Item Tax Template") + for template in item_tax_template_list: + doc = frappe.get_doc("Item Tax Template", template.name) + for tax in doc.taxes: + doc.company = frappe.get_value("Account", tax.tax_type, "company") + break + doc.save() diff --git a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py index 53e83216673..7dc0af9a1aa 100644 --- a/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py +++ b/erpnext/patches/v12_0/update_owner_fields_in_acc_dimension_custom_fields.py @@ -1,4 +1,3 @@ - import frappe from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -7,15 +6,21 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( def execute(): - accounting_dimensions = frappe.db.sql("""select fieldname from - `tabAccounting Dimension`""", as_dict=1) + accounting_dimensions = frappe.db.sql( + """select fieldname from + `tabAccounting Dimension`""", + as_dict=1, + ) doclist = get_doctypes_with_dimensions() for dimension in accounting_dimensions: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustom Field` SET owner = 'Administrator' WHERE fieldname = %s - AND dt IN (%s)""" % #nosec - ('%s', ', '.join(['%s']* len(doclist))), tuple([dimension.fieldname] + doclist)) + AND dt IN (%s)""" + % ("%s", ", ".join(["%s"] * len(doclist))), # nosec + tuple([dimension.fieldname] + doclist), + ) diff --git a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py index e0382d818ea..5710320e62d 100644 --- a/erpnext/patches/v12_0/update_price_list_currency_in_bom.py +++ b/erpnext/patches/v12_0/update_price_list_currency_in_bom.py @@ -1,4 +1,3 @@ - import frappe from frappe.utils import getdate @@ -9,16 +8,19 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "bom") frappe.reload_doc("manufacturing", "doctype", "bom_item") - frappe.db.sql(""" UPDATE `tabBOM`, `tabPrice List` + frappe.db.sql( + """ UPDATE `tabBOM`, `tabPrice List` SET `tabBOM`.price_list_currency = `tabPrice List`.currency, `tabBOM`.plc_conversion_rate = 1.0 WHERE `tabBOM`.buying_price_list = `tabPrice List`.name AND `tabBOM`.docstatus < 2 AND `tabBOM`.rm_cost_as_per = 'Price List' - """) + """ + ) - for d in frappe.db.sql(""" + for d in frappe.db.sql( + """ SELECT bom.creation, bom.name, bom.price_list_currency as currency, company.default_currency as company_currency @@ -26,8 +28,11 @@ def execute(): `tabBOM` as bom, `tabCompany` as company WHERE bom.company = company.name AND bom.rm_cost_as_per = 'Price List' AND - bom.price_list_currency != company.default_currency AND bom.docstatus < 2""", as_dict=1): - plc_conversion_rate = get_exchange_rate(d.currency, - d.company_currency, getdate(d.creation), "for_buying") + bom.price_list_currency != company.default_currency AND bom.docstatus < 2""", + as_dict=1, + ): + plc_conversion_rate = get_exchange_rate( + d.currency, d.company_currency, getdate(d.creation), "for_buying" + ) - frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) + frappe.db.set_value("BOM", d.name, "plc_conversion_rate", plc_conversion_rate) diff --git a/erpnext/patches/v12_0/update_price_or_product_discount.py b/erpnext/patches/v12_0/update_price_or_product_discount.py index 86105a469db..64344c8cd42 100644 --- a/erpnext/patches/v12_0/update_price_or_product_discount.py +++ b/erpnext/patches/v12_0/update_price_or_product_discount.py @@ -1,9 +1,10 @@ - import frappe def execute(): frappe.reload_doc("accounts", "doctype", "pricing_rule") - frappe.db.sql(""" UPDATE `tabPricing Rule` SET price_or_product_discount = 'Price' - WHERE ifnull(price_or_product_discount,'') = '' """) + frappe.db.sql( + """ UPDATE `tabPricing Rule` SET price_or_product_discount = 'Price' + WHERE ifnull(price_or_product_discount,'') = '' """ + ) diff --git a/erpnext/patches/v12_0/update_pricing_rule_fields.py b/erpnext/patches/v12_0/update_pricing_rule_fields.py index b7c36ae7780..8da06b0bda0 100644 --- a/erpnext/patches/v12_0/update_pricing_rule_fields.py +++ b/erpnext/patches/v12_0/update_pricing_rule_fields.py @@ -4,68 +4,120 @@ import frappe -parentfield = { - 'item_code': 'items', - 'item_group': 'item_groups', - 'brand': 'brands' -} +parentfield = {"item_code": "items", "item_group": "item_groups", "brand": "brands"} + def execute(): - if not frappe.get_all('Pricing Rule', limit=1): + if not frappe.get_all("Pricing Rule", limit=1): return - frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail') - doctypes = {'Supplier Quotation': 'buying', 'Purchase Order': 'buying', 'Purchase Invoice': 'accounts', - 'Purchase Receipt': 'stock', 'Quotation': 'selling', 'Sales Order': 'selling', - 'Sales Invoice': 'accounts', 'Delivery Note': 'stock'} + frappe.reload_doc("accounts", "doctype", "pricing_rule_detail") + doctypes = { + "Supplier Quotation": "buying", + "Purchase Order": "buying", + "Purchase Invoice": "accounts", + "Purchase Receipt": "stock", + "Quotation": "selling", + "Sales Order": "selling", + "Sales Invoice": "accounts", + "Delivery Note": "stock", + } for doctype, module in doctypes.items(): - frappe.reload_doc(module, 'doctype', frappe.scrub(doctype)) + frappe.reload_doc(module, "doctype", frappe.scrub(doctype)) - child_doc = frappe.scrub(doctype) + '_item' - frappe.reload_doc(module, 'doctype', child_doc, force=True) + child_doc = frappe.scrub(doctype) + "_item" + frappe.reload_doc(module, "doctype", child_doc, force=True) - child_doctype = doctype + ' Item' + child_doctype = doctype + " Item" - frappe.db.sql(""" UPDATE `tab{child_doctype}` SET pricing_rules = pricing_rule + frappe.db.sql( + """ UPDATE `tab{child_doctype}` SET pricing_rules = pricing_rule WHERE docstatus < 2 and pricing_rule is not null and pricing_rule != '' - """.format(child_doctype= child_doctype)) + """.format( + child_doctype=child_doctype + ) + ) - data = frappe.db.sql(""" SELECT pricing_rule, name, parent, + data = frappe.db.sql( + """ SELECT pricing_rule, name, parent, parenttype, creation, modified, docstatus, modified_by, owner, name FROM `tab{child_doc}` where docstatus < 2 and pricing_rule is not null - and pricing_rule != ''""".format(child_doc=child_doctype), as_dict=1) + and pricing_rule != ''""".format( + child_doc=child_doctype + ), + as_dict=1, + ) values = [] for d in data: - values.append((d.pricing_rule, d.name, d.parent, 'pricing_rules', d.parenttype, - d.creation, d.modified, d.docstatus, d.modified_by, d.owner, frappe.generate_hash("", 10))) + values.append( + ( + d.pricing_rule, + d.name, + d.parent, + "pricing_rules", + d.parenttype, + d.creation, + d.modified, + d.docstatus, + d.modified_by, + d.owner, + frappe.generate_hash("", 10), + ) + ) if values: - frappe.db.sql(""" INSERT INTO + frappe.db.sql( + """ INSERT INTO `tabPricing Rule Detail` (`pricing_rule`, `child_docname`, `parent`, `parentfield`, `parenttype`, `creation`, `modified`, `docstatus`, `modified_by`, `owner`, `name`) - VALUES {values} """.format(values=', '.join(['%s'] * len(values))), tuple(values)) + VALUES {values} """.format( + values=", ".join(["%s"] * len(values)) + ), + tuple(values), + ) - frappe.reload_doc('accounts', 'doctype', 'pricing_rule') + frappe.reload_doc("accounts", "doctype", "pricing_rule") - for doctype, apply_on in {'Pricing Rule Item Code': 'Item Code', - 'Pricing Rule Item Group': 'Item Group', 'Pricing Rule Brand': 'Brand'}.items(): - frappe.reload_doc('accounts', 'doctype', frappe.scrub(doctype)) + for doctype, apply_on in { + "Pricing Rule Item Code": "Item Code", + "Pricing Rule Item Group": "Item Group", + "Pricing Rule Brand": "Brand", + }.items(): + frappe.reload_doc("accounts", "doctype", frappe.scrub(doctype)) field = frappe.scrub(apply_on) - data = frappe.get_all('Pricing Rule', fields=[field, "name", "creation", "modified", - "owner", "modified_by"], filters= {'apply_on': apply_on}) + data = frappe.get_all( + "Pricing Rule", + fields=[field, "name", "creation", "modified", "owner", "modified_by"], + filters={"apply_on": apply_on}, + ) values = [] for d in data: - values.append((d.get(field), d.name, parentfield.get(field), 'Pricing Rule', - d.creation, d.modified, d.owner, d.modified_by, frappe.generate_hash("", 10))) + values.append( + ( + d.get(field), + d.name, + parentfield.get(field), + "Pricing Rule", + d.creation, + d.modified, + d.owner, + d.modified_by, + frappe.generate_hash("", 10), + ) + ) if values: - frappe.db.sql(""" INSERT INTO + frappe.db.sql( + """ INSERT INTO `tab{doctype}` ({field}, parent, parentfield, parenttype, creation, modified, owner, modified_by, name) - VALUES {values} """.format(doctype=doctype, - field=field, values=', '.join(['%s'] * len(values))), tuple(values)) + VALUES {values} """.format( + doctype=doctype, field=field, values=", ".join(["%s"] * len(values)) + ), + tuple(values), + ) diff --git a/erpnext/patches/v12_0/update_production_plan_status.py b/erpnext/patches/v12_0/update_production_plan_status.py index 06fc503a33f..dc65ec25f21 100644 --- a/erpnext/patches/v12_0/update_production_plan_status.py +++ b/erpnext/patches/v12_0/update_production_plan_status.py @@ -6,7 +6,8 @@ import frappe def execute(): frappe.reload_doc("manufacturing", "doctype", "production_plan") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabProduction Plan` ppl SET status = "Completed" WHERE ppl.name IN ( @@ -28,4 +29,5 @@ def execute(): HAVING should_set = 1 ) ss ) - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py index 25cf6b97e3b..d7e96fafd60 100644 --- a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py +++ b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py @@ -5,20 +5,22 @@ from erpnext.regional.india import states def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return # Update options in gst_state custom field - gst_state = frappe.get_doc('Custom Field', 'Address-gst_state') - gst_state.options = '\n'.join(states) + gst_state = frappe.get_doc("Custom Field", "Address-gst_state") + gst_state.options = "\n".join(states) gst_state.save() # Update gst_state and state code in existing address - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabAddress` SET gst_state = 'Dadra and Nagar Haveli and Daman and Diu', gst_state_number = 26 WHERE gst_state = 'Daman and Diu' - """) + """ + ) diff --git a/erpnext/patches/v12_0/update_uom_conversion_factor.py b/erpnext/patches/v12_0/update_uom_conversion_factor.py index 3184d1195f0..a09ac190e2e 100644 --- a/erpnext/patches/v12_0/update_uom_conversion_factor.py +++ b/erpnext/patches/v12_0/update_uom_conversion_factor.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py index 57fbaae9d8d..38a8500ac73 100644 --- a/erpnext/patches/v13_0/add_bin_unique_constraint.py +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -14,13 +14,16 @@ def execute(): delete_broken_bins() delete_and_patch_duplicate_bins() + def delete_broken_bins(): # delete useless bins frappe.db.sql("delete from `tabBin` where item_code is null or warehouse is null") + def delete_and_patch_duplicate_bins(): - duplicate_bins = frappe.db.sql(""" + duplicate_bins = frappe.db.sql( + """ SELECT item_code, warehouse, count(*) as bin_count FROM @@ -29,18 +32,19 @@ def delete_and_patch_duplicate_bins(): item_code, warehouse HAVING bin_count > 1 - """, as_dict=1) + """, + as_dict=1, + ) for duplicate_bin in duplicate_bins: item_code = duplicate_bin.item_code warehouse = duplicate_bin.warehouse - existing_bins = frappe.get_list("Bin", - filters={ - "item_code": item_code, - "warehouse": warehouse - }, - fields=["name"], - order_by="creation",) + existing_bins = frappe.get_list( + "Bin", + filters={"item_code": item_code, "warehouse": warehouse}, + fields=["name"], + order_by="creation", + ) # keep last one existing_bins.pop() @@ -53,7 +57,7 @@ def delete_and_patch_duplicate_bins(): "indented_qty": get_indented_qty(item_code, warehouse), "ordered_qty": get_ordered_qty(item_code, warehouse), "planned_qty": get_planned_qty(item_code, warehouse), - "actual_qty": get_balance_qty_from_sle(item_code, warehouse) + "actual_qty": get_balance_qty_from_sle(item_code, warehouse), } bin = get_bin(item_code, warehouse) diff --git a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py index b34b5c1801f..353376b6038 100644 --- a/erpnext/patches/v13_0/add_custom_field_for_south_africa.py +++ b/erpnext/patches/v13_0/add_custom_field_for_south_africa.py @@ -7,13 +7,13 @@ from erpnext.regional.south_africa.setup import add_permissions, make_custom_fie def execute(): - company = frappe.get_all('Company', filters = {'country': 'South Africa'}) + company = frappe.get_all("Company", filters={"country": "South Africa"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'south_africa_vat_settings') - frappe.reload_doc('regional', 'report', 'vat_audit_report') - frappe.reload_doc('accounts', 'doctype', 'south_africa_vat_account') + frappe.reload_doc("regional", "doctype", "south_africa_vat_settings") + frappe.reload_doc("regional", "report", "vat_audit_report") + frappe.reload_doc("accounts", "doctype", "south_africa_vat_account") make_custom_fields() add_permissions() diff --git a/erpnext/patches/v13_0/add_default_interview_notification_templates.py b/erpnext/patches/v13_0/add_default_interview_notification_templates.py index fa1b31c34bb..9a47efe8704 100644 --- a/erpnext/patches/v13_0/add_default_interview_notification_templates.py +++ b/erpnext/patches/v13_0/add_default_interview_notification_templates.py @@ -1,4 +1,3 @@ - import os import frappe @@ -6,32 +5,40 @@ from frappe import _ def execute(): - if not frappe.db.exists('Email Template', _('Interview Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html')) + if not frappe.db.exists("Email Template", _("Interview Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_reminder_notification_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Reminder'), - 'response': response, - 'subject': _('Interview Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Reminder"), + "response": response, + "subject": _("Interview Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')): - base_path = frappe.get_app_path('erpnext', 'hr', 'doctype') - response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html')) + if not frappe.db.exists("Email Template", _("Interview Feedback Reminder")): + base_path = frappe.get_app_path("erpnext", "hr", "doctype") + response = frappe.read_file( + os.path.join(base_path, "interview/interview_feedback_reminder_template.html") + ) - frappe.get_doc({ - 'doctype': 'Email Template', - 'name': _('Interview Feedback Reminder'), - 'response': response, - 'subject': _('Interview Feedback Reminder'), - 'owner': frappe.session.user, - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Email Template", + "name": _("Interview Feedback Reminder"), + "response": response, + "subject": _("Interview Feedback Reminder"), + "owner": frappe.session.user, + } + ).insert(ignore_permissions=True) - hr_settings = frappe.get_doc('HR Settings') - hr_settings.interview_reminder_template = _('Interview Reminder') - hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder') + hr_settings = frappe.get_doc("HR Settings") + hr_settings.interview_reminder_template = _("Interview Reminder") + hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") hr_settings.flags.ignore_links = True hr_settings.save() diff --git a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py index bd18b9bd173..517a14a8300 100644 --- a/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py +++ b/erpnext/patches/v13_0/add_missing_fg_item_for_stock_entry.py @@ -9,31 +9,34 @@ from erpnext.stock.stock_ledger import make_sl_entries def execute(): - if not frappe.db.has_column('Work Order', 'has_batch_no'): + if not frappe.db.has_column("Work Order", "has_batch_no"): return - frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings') - if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): + frappe.reload_doc("manufacturing", "doctype", "manufacturing_settings") + if cint( + frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") + ): return - frappe.reload_doc('manufacturing', 'doctype', 'work_order') + frappe.reload_doc("manufacturing", "doctype", "work_order") filters = { - 'docstatus': 1, - 'produced_qty': ('>', 0), - 'creation': ('>=', '2021-06-29 00:00:00'), - 'has_batch_no': 1 + "docstatus": 1, + "produced_qty": (">", 0), + "creation": (">=", "2021-06-29 00:00:00"), + "has_batch_no": 1, } - fields = ['name', 'production_item'] + fields = ["name", "production_item"] - work_orders = [d.name for d in frappe.get_all('Work Order', filters = filters, fields=fields)] + work_orders = [d.name for d in frappe.get_all("Work Order", filters=filters, fields=fields)] if not work_orders: return repost_stock_entries = [] - stock_entries = frappe.db.sql_list(''' + stock_entries = frappe.db.sql_list( + """ SELECT se.name FROM @@ -45,18 +48,20 @@ def execute(): ) ORDER BY se.posting_date, se.posting_time - ''', (work_orders,)) + """, + (work_orders,), + ) if stock_entries: - print('Length of stock entries', len(stock_entries)) + print("Length of stock entries", len(stock_entries)) for stock_entry in stock_entries: - doc = frappe.get_doc('Stock Entry', stock_entry) + doc = frappe.get_doc("Stock Entry", stock_entry) doc.set_work_order_details() doc.load_items_from_bom() doc.calculate_rate_and_amount() set_expense_account(doc) - doc.make_batches('t_warehouse') + doc.make_batches("t_warehouse") if doc.docstatus == 0: doc.save() @@ -67,10 +72,14 @@ def execute(): for repost_doc in repost_stock_entries: repost_future_sle_and_gle(repost_doc) + def set_expense_account(doc): for row in doc.items: if row.is_finished_item and not row.expense_account: - row.expense_account = frappe.get_cached_value('Company', doc.company, 'stock_adjustment_account') + row.expense_account = frappe.get_cached_value( + "Company", doc.company, "stock_adjustment_account" + ) + def repost_stock_entry(doc): doc.db_update() @@ -86,29 +95,36 @@ def repost_stock_entry(doc): try: make_sl_entries(sl_entries, True) except Exception: - print(f'SLE entries not posted for the stock entry {doc.name}') + print(f"SLE entries not posted for the stock entry {doc.name}") traceback = frappe.get_traceback() frappe.log_error(traceback) + def get_sle_for_target_warehouse(doc, sl_entries, finished_item_row): - for d in doc.get('items'): + for d in doc.get("items"): if cstr(d.t_warehouse) and finished_item_row and d.name == finished_item_row.name: - sle = doc.get_sl_entries(d, { - "warehouse": cstr(d.t_warehouse), - "actual_qty": flt(d.transfer_qty), - "incoming_rate": flt(d.valuation_rate) - }) + sle = doc.get_sl_entries( + d, + { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate), + }, + ) sle.recalculate_rate = 1 sl_entries.append(sle) + def repost_future_sle_and_gle(doc): - args = frappe._dict({ - "posting_date": doc.posting_date, - "posting_time": doc.posting_time, - "voucher_type": doc.doctype, - "voucher_no": doc.name, - "company": doc.company - }) + args = frappe._dict( + { + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "company": doc.company, + } + ) create_repost_item_valuation_entry(args) diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py index 3bf2762456e..7dce95c1b82 100644 --- a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py +++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py @@ -1,12 +1,13 @@ - import frappe def execute(): frappe.reload_doc("projects", "doctype", "project") - frappe.db.sql("""UPDATE `tabProject` + frappe.db.sql( + """UPDATE `tabProject` SET naming_series = 'PROJ-.####' WHERE - naming_series is NULL""") + naming_series is NULL""" + ) diff --git a/erpnext/patches/v13_0/add_po_to_global_search.py b/erpnext/patches/v13_0/add_po_to_global_search.py index 396d3343ba6..514cd343900 100644 --- a/erpnext/patches/v13_0/add_po_to_global_search.py +++ b/erpnext/patches/v13_0/add_po_to_global_search.py @@ -1,17 +1,14 @@ - import frappe def execute(): - global_search_settings = frappe.get_single("Global Search Settings") + global_search_settings = frappe.get_single("Global Search Settings") - if "Purchase Order" in ( - dt.document_type for dt in global_search_settings.allowed_in_global_search - ): - return + if "Purchase Order" in ( + dt.document_type for dt in global_search_settings.allowed_in_global_search + ): + return - global_search_settings.append( - "allowed_in_global_search", {"document_type": "Purchase Order"} - ) + global_search_settings.append("allowed_in_global_search", {"document_type": "Purchase Order"}) - global_search_settings.save(ignore_permissions=True) + global_search_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/add_standard_navbar_items.py b/erpnext/patches/v13_0/add_standard_navbar_items.py index c739a7a93f9..24141b7862d 100644 --- a/erpnext/patches/v13_0/add_standard_navbar_items.py +++ b/erpnext/patches/v13_0/add_standard_navbar_items.py @@ -1,4 +1,3 @@ - # import frappe from erpnext.setup.install import add_standard_navbar_items diff --git a/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py index 5eb6ff44702..cc424c6d3e0 100644 --- a/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py +++ b/erpnext/patches/v13_0/amazon_mws_deprecation_warning.py @@ -12,4 +12,4 @@ def execute(): "Amazon MWS Integration is moved to a separate app and will be removed from ERPNext in version-14.\n" "Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations", fg="yellow", - ) \ No newline at end of file + ) diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py index a95f822d281..ee23747cc04 100644 --- a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py +++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v13_0/change_default_pos_print_format.py b/erpnext/patches/v13_0/change_default_pos_print_format.py index 9664247ab41..be478a2b67f 100644 --- a/erpnext/patches/v13_0/change_default_pos_print_format.py +++ b/erpnext/patches/v13_0/change_default_pos_print_format.py @@ -1,4 +1,3 @@ - import frappe @@ -6,4 +5,5 @@ def execute(): frappe.db.sql( """UPDATE `tabPOS Profile` profile SET profile.`print_format` = 'POS Invoice' - WHERE profile.`print_format` = 'Point of Sale'""") + WHERE profile.`print_format` = 'Point of Sale'""" + ) diff --git a/erpnext/patches/v13_0/check_is_income_tax_component.py b/erpnext/patches/v13_0/check_is_income_tax_component.py index 5e1df14d4e0..0ae3a3e3bd7 100644 --- a/erpnext/patches/v13_0/check_is_income_tax_component.py +++ b/erpnext/patches/v13_0/check_is_income_tax_component.py @@ -10,33 +10,36 @@ import erpnext def execute(): - doctypes = ['salary_component', - 'Employee Tax Exemption Declaration', - 'Employee Tax Exemption Proof Submission', - 'Employee Tax Exemption Declaration Category', - 'Employee Tax Exemption Proof Submission Detail', - 'gratuity_rule', - 'gratuity_rule_slab', - 'gratuity_applicable_component' + doctypes = [ + "salary_component", + "Employee Tax Exemption Declaration", + "Employee Tax Exemption Proof Submission", + "Employee Tax Exemption Declaration Category", + "Employee Tax Exemption Proof Submission Detail", + "gratuity_rule", + "gratuity_rule_slab", + "gratuity_applicable_component", ] for doctype in doctypes: - frappe.reload_doc('Payroll', 'doctype', doctype, force=True) + frappe.reload_doc("Payroll", "doctype", doctype, force=True) - - reports = ['Professional Tax Deductions', 'Provident Fund Deductions', 'E-Invoice Summary'] + reports = ["Professional Tax Deductions", "Provident Fund Deductions", "E-Invoice Summary"] for report in reports: - frappe.reload_doc('Regional', 'Report', report) - frappe.reload_doc('Regional', 'Report', report) + frappe.reload_doc("Regional", "Report", report) + frappe.reload_doc("Regional", "Report", report) if erpnext.get_region() == "India": - create_custom_field('Salary Component', - dict(fieldname='component_type', - label='Component Type', - fieldtype='Select', - insert_after='description', - options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax', - depends_on='eval:doc.type == "Deduction"') + create_custom_field( + "Salary Component", + dict( + fieldname="component_type", + label="Component Type", + fieldtype="Select", + insert_after="description", + options="\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", + depends_on='eval:doc.type == "Deduction"', + ), ) if frappe.db.exists("Salary Component", "Income Tax"): @@ -44,7 +47,9 @@ def execute(): if frappe.db.exists("Salary Component", "TDS"): frappe.db.set_value("Salary Component", "TDS", "is_income_tax_component", 1) - components = frappe.db.sql("select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1) + components = frappe.db.sql( + "select name from `tabSalary Component` where variable_based_on_taxable_salary = 1", as_dict=1 + ) for component in components: frappe.db.set_value("Salary Component", component.name, "is_income_tax_component", 1) @@ -52,4 +57,6 @@ def execute(): if frappe.db.exists("Salary Component", "Provident Fund"): frappe.db.set_value("Salary Component", "Provident Fund", "component_type", "Provident Fund") if frappe.db.exists("Salary Component", "Professional Tax"): - frappe.db.set_value("Salary Component", "Professional Tax", "component_type", "Professional Tax") + frappe.db.set_value( + "Salary Component", "Professional Tax", "component_type", "Professional Tax" + ) diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py index 7ef154e6066..efbb96c100a 100644 --- a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py +++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py @@ -1,24 +1,25 @@ - import frappe def execute(): - frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter') + frappe.reload_doc("stock", "doctype", "quality_inspection_parameter") # get all distinct parameters from QI readigs table - reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"]) + reading_params = frappe.db.get_all( + "Quality Inspection Reading", fields=["distinct specification"] + ) reading_params = [d.specification for d in reading_params] # get all distinct parameters from QI Template as some may be unused in QI - template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"]) + template_params = frappe.db.get_all( + "Item Quality Inspection Parameter", fields=["distinct specification"] + ) template_params = [d.specification for d in template_params] params = list(set(reading_params + template_params)) for parameter in params: if not frappe.db.exists("Quality Inspection Parameter", parameter): - frappe.get_doc({ - "doctype": "Quality Inspection Parameter", - "parameter": parameter, - "description": parameter - }).insert(ignore_permissions=True) + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py index d3ee3f8276c..020521d5b95 100644 --- a/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py +++ b/erpnext/patches/v13_0/convert_to_website_item_in_item_card_group_template.py @@ -7,51 +7,57 @@ from erpnext.e_commerce.doctype.website_item.website_item import make_website_it def execute(): - """ - Convert all Item links to Website Item link values in - exisitng 'Item Card Group' Web Page Block data. - """ - frappe.reload_doc("e_commerce", "web_template", "item_card_group") + """ + Convert all Item links to Website Item link values in + exisitng 'Item Card Group' Web Page Block data. + """ + frappe.reload_doc("e_commerce", "web_template", "item_card_group") - blocks = frappe.db.get_all( - "Web Page Block", - filters={"web_template": "Item Card Group"}, - fields=["parent", "web_template_values", "name"] - ) + blocks = frappe.db.get_all( + "Web Page Block", + filters={"web_template": "Item Card Group"}, + fields=["parent", "web_template_values", "name"], + ) - fields = generate_fields_to_edit() + fields = generate_fields_to_edit() - for block in blocks: - web_template_value = json.loads(block.get('web_template_values')) + for block in blocks: + web_template_value = json.loads(block.get("web_template_values")) - for field in fields: - item = web_template_value.get(field) - if not item: - continue + for field in fields: + item = web_template_value.get(field) + if not item: + continue - if frappe.db.exists("Website Item", {"item_code": item}): - website_item = frappe.db.get_value("Website Item", {"item_code": item}) - else: - website_item = make_new_website_item(item) + if frappe.db.exists("Website Item", {"item_code": item}): + website_item = frappe.db.get_value("Website Item", {"item_code": item}) + else: + website_item = make_new_website_item(item) - if website_item: - web_template_value[field] = website_item + if website_item: + web_template_value[field] = website_item + + frappe.db.set_value( + "Web Page Block", block.name, "web_template_values", json.dumps(web_template_value) + ) - frappe.db.set_value("Web Page Block", block.name, "web_template_values", json.dumps(web_template_value)) def generate_fields_to_edit() -> List: - fields = [] - for i in range(1, 13): - fields.append(f"card_{i}_item") # fields like 'card_1_item', etc. + fields = [] + for i in range(1, 13): + fields.append(f"card_{i}_item") # fields like 'card_1_item', etc. + + return fields - return fields def make_new_website_item(item: str) -> Union[str, None]: - try: - doc = frappe.get_doc("Item", item) - web_item = make_website_item(doc) # returns [website_item.name, item_name] - return web_item[0] - except Exception: - title = f"{item}: Error while converting to Website Item " - frappe.log_error(title + "for Item Card Group Template" + "\n\n" + frappe.get_traceback(), title=title) - return None + try: + doc = frappe.get_doc("Item", item) + web_item = make_website_item(doc) # returns [website_item.name, item_name] + return web_item[0] + except Exception: + title = f"{item}: Error while converting to Website Item " + frappe.log_error( + title + "for Item Card Group Template" + "\n\n" + frappe.get_traceback(), title=title + ) + return None diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py b/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py index 44501088102..51ab0e8b65c 100644 --- a/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py +++ b/erpnext/patches/v13_0/create_accounting_dimensions_in_pos_doctypes.py @@ -3,9 +3,12 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): - frappe.reload_doc('accounts', 'doctype', 'accounting_dimension') - accounting_dimensions = frappe.db.sql("""select fieldname, label, document_type, disabled from - `tabAccounting Dimension`""", as_dict=1) + frappe.reload_doc("accounts", "doctype", "accounting_dimension") + accounting_dimensions = frappe.db.sql( + """select fieldname, label, document_type, disabled from + `tabAccounting Dimension`""", + as_dict=1, + ) if not accounting_dimensions: return @@ -14,9 +17,9 @@ def execute(): for d in accounting_dimensions: if count % 2 == 0: - insert_after_field = 'dimension_col_break' + insert_after_field = "dimension_col_break" else: - insert_after_field = 'accounting_dimensions_section' + insert_after_field = "accounting_dimensions_section" for doctype in ["POS Invoice", "POS Invoice Item"]: @@ -32,10 +35,10 @@ def execute(): "label": d.label, "fieldtype": "Link", "options": d.document_type, - "insert_after": insert_after_field + "insert_after": insert_after_field, } - if df['fieldname'] not in fieldnames: + if df["fieldname"] not in fieldnames: create_custom_field(doctype, df) frappe.clear_cache(doctype=doctype) diff --git a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py index 313b0e9a2eb..2b8666d21b6 100644 --- a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py +++ b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py @@ -3,18 +3,18 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return custom_field = { - 'Finance Book': [ + "Finance Book": [ { - 'fieldname': 'for_income_tax', - 'label': 'For Income Tax', - 'fieldtype': 'Check', - 'insert_after': 'finance_book_name', - 'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.' + "fieldname": "for_income_tax", + "label": "For Income Tax", + "fieldtype": "Check", + "insert_after": "finance_book_name", + "description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.", } ] } diff --git a/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py index 840b8fd830f..3217eab43d6 100644 --- a/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py +++ b/erpnext/patches/v13_0/create_gst_custom_fields_in_quotation.py @@ -3,27 +3,51 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "India"}, fields=["name"]) if not company: return sales_invoice_gst_fields = [ - dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', read_only=1, - fetch_from='customer_address.gstin', print_hide=1, length=15), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='shipping_address_name', - fetch_from='shipping_address_name.gstin', print_hide=1, length=15), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=1, length=50), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15), - ] + dict( + fieldname="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + length=50, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + length=15, + ), + ] - custom_fields = { - 'Quotation': sales_invoice_gst_fields - } + custom_fields = {"Quotation": sales_invoice_gst_fields} - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) 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 416694559cb..bef2516f6d0 100644 --- a/erpnext/patches/v13_0/create_gst_payment_entry_fields.py +++ b/erpnext/patches/v13_0/create_gst_payment_entry_fields.py @@ -6,32 +6,75 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - frappe.reload_doc('accounts', 'doctype', 'advance_taxes_and_charges') - frappe.reload_doc('accounts', 'doctype', 'payment_entry') + frappe.reload_doc("accounts", "doctype", "advance_taxes_and_charges") + frappe.reload_doc("accounts", "doctype", "payment_entry") - if frappe.db.exists('Company', {'country': 'India'}): + 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) + "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) else: - fields = ['gst_section', 'company_address', 'company_gstin', 'place_of_supply', 'customer_address', 'customer_gstin'] + 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 + frappe.delete_doc_if_exists("Custom Field", f"Payment Entry-{field}") diff --git a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py index 543faeb74ac..3fe27b5c1a9 100644 --- a/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py +++ b/erpnext/patches/v13_0/create_healthcare_custom_fields_in_stock_entry_detail.py @@ -5,8 +5,8 @@ from erpnext.domains.healthcare import data def execute(): - if 'Healthcare' not in frappe.get_active_domains(): + if "Healthcare" not in frappe.get_active_domains(): return - if data['custom_fields']: - create_custom_fields(data['custom_fields']) + if data["custom_fields"]: + create_custom_fields(data["custom_fields"]) diff --git a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py b/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py index f33b4b3ea0d..093463a12e9 100644 --- a/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py +++ b/erpnext/patches/v13_0/create_ksa_vat_custom_fields.py @@ -4,9 +4,8 @@ from erpnext.regional.saudi_arabia.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) if not company: return make_custom_fields() - diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py index 6f9031fc500..59b17eea9fe 100644 --- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -6,69 +6,89 @@ import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment') - frappe.reload_doc('hr', 'doctype', 'employee_grade') - employee_with_assignment = [] - leave_policy = [] + frappe.reload_doc("hr", "doctype", "leave_policy_assignment") + frappe.reload_doc("hr", "doctype", "employee_grade") + employee_with_assignment = [] + leave_policy = [] - if "leave_policy" in frappe.db.get_table_columns("Employee"): - employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1) + if "leave_policy" in frappe.db.get_table_columns("Employee"): + employees_with_leave_policy = frappe.db.sql( + "SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", + as_dict=1, + ) - for employee in employees_with_leave_policy: - alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}) - if not alloc: - create_assignment(employee.name, employee.leave_policy) + for employee in employees_with_leave_policy: + alloc = frappe.db.exists( + "Leave Allocation", + {"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}, + ) + if not alloc: + create_assignment(employee.name, employee.leave_policy) - employee_with_assignment.append(employee.name) - leave_policy.append(employee.leave_policy) + employee_with_assignment.append(employee.name) + leave_policy.append(employee.leave_policy) - if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"): - employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1) + if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"): + employee_grade_with_leave_policy = frappe.db.sql( + "SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", + as_dict=1, + ) - #for whole employee Grade - for grade in employee_grade_with_leave_policy: - employees = get_employee_with_grade(grade.name) - for employee in employees: + # for whole employee Grade + for grade in employee_grade_with_leave_policy: + employees = get_employee_with_grade(grade.name) + for employee in employees: - if employee not in employee_with_assignment: #Will ensure no duplicate - alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}) - if not alloc: - create_assignment(employee.name, grade.default_leave_policy) - leave_policy.append(grade.default_leave_policy) + if employee not in employee_with_assignment: # Will ensure no duplicate + alloc = frappe.db.exists( + "Leave Allocation", + {"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}, + ) + if not alloc: + create_assignment(employee.name, grade.default_leave_policy) + leave_policy.append(grade.default_leave_policy) - #for old Leave allocation and leave policy from allocation, which may got updated in employee grade. - leave_allocations = frappe.db.sql("SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", as_dict = 1) + # for old Leave allocation and leave policy from allocation, which may got updated in employee grade. + leave_allocations = frappe.db.sql( + "SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", + as_dict=1, + ) - for allocation in leave_allocations: - if allocation.leave_policy not in leave_policy: - create_assignment(allocation.employee, allocation.leave_policy, leave_period=allocation.leave_period, - allocation_exists=True) + for allocation in leave_allocations: + if allocation.leave_policy not in leave_policy: + create_assignment( + allocation.employee, + allocation.leave_policy, + leave_period=allocation.leave_period, + allocation_exists=True, + ) -def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False): - if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2: - return - filters = {"employee":employee, "leave_policy": leave_policy} - if leave_period: - filters["leave_period"] = leave_period +def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False): + if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2: + return - if not frappe.db.exists("Leave Policy Assignment" , filters): - lpa = frappe.new_doc("Leave Policy Assignment") - lpa.employee = employee - lpa.leave_policy = leave_policy + filters = {"employee": employee, "leave_policy": leave_policy} + if leave_period: + filters["leave_period"] = leave_period - lpa.flags.ignore_mandatory = True - if allocation_exists: - lpa.assignment_based_on = 'Leave Period' - lpa.leave_period = leave_period - lpa.leaves_allocated = 1 + if not frappe.db.exists("Leave Policy Assignment", filters): + lpa = frappe.new_doc("Leave Policy Assignment") + lpa.employee = employee + lpa.leave_policy = leave_policy - lpa.save() - if allocation_exists: - lpa.submit() - #Updating old Leave Allocation - frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) + lpa.flags.ignore_mandatory = True + if allocation_exists: + lpa.assignment_based_on = "Leave Period" + lpa.leave_period = leave_period + lpa.leaves_allocated = 1 + + lpa.save() + if allocation_exists: + lpa.submit() + # Updating old Leave Allocation + frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) def get_employee_with_grade(grade): - return frappe.get_list("Employee", filters = {"grade": grade}) + return frappe.get_list("Employee", filters={"grade": grade}) diff --git a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py index 87c9cf1ebd5..66aae9a30af 100644 --- a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py +++ b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py @@ -8,12 +8,13 @@ from erpnext.regional.united_arab_emirates.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': ['in', ['Saudi Arabia', 'United Arab Emirates']]}) + company = frappe.get_all( + "Company", filters={"country": ["in", ["Saudi Arabia", "United Arab Emirates"]]} + ) if not company: return - - frappe.reload_doc('accounts', 'doctype', 'pos_invoice') - frappe.reload_doc('accounts', 'doctype', 'pos_invoice_item') + frappe.reload_doc("accounts", "doctype", "pos_invoice") + frappe.reload_doc("accounts", "doctype", "pos_invoice_item") make_custom_fields() diff --git a/erpnext/patches/v13_0/create_website_items.py b/erpnext/patches/v13_0/create_website_items.py index 3baa34b71c0..1a1d79ca828 100644 --- a/erpnext/patches/v13_0/create_website_items.py +++ b/erpnext/patches/v13_0/create_website_items.py @@ -11,14 +11,31 @@ def execute(): frappe.reload_doc("e_commerce", "doctype", "e_commerce_settings") frappe.reload_doc("stock", "doctype", "item") - item_fields = ["item_code", "item_name", "item_group", "stock_uom", "brand", "image", - "has_variants", "variant_of", "description", "weightage"] - web_fields_to_map = ["route", "slideshow", "website_image_alt", - "website_warehouse", "web_long_description", "website_content", "thumbnail"] + item_fields = [ + "item_code", + "item_name", + "item_group", + "stock_uom", + "brand", + "image", + "has_variants", + "variant_of", + "description", + "weightage", + ] + web_fields_to_map = [ + "route", + "slideshow", + "website_image_alt", + "website_warehouse", + "web_long_description", + "website_content", + "thumbnail", + ] # get all valid columns (fields) from Item master DB schema item_table_fields = frappe.db.sql("desc `tabItem`", as_dict=1) - item_table_fields = [d.get('Field') for d in item_table_fields] + item_table_fields = [d.get("Field") for d in item_table_fields] # prepare fields to query from Item, check if the web field exists in Item master web_query_fields = [] @@ -38,11 +55,7 @@ def execute(): # most likely a fresh installation that doesnt need this patch return - items = frappe.db.get_all( - "Item", - fields=item_fields, - or_filters=or_filters - ) + items = frappe.db.get_all("Item", fields=item_fields, or_filters=or_filters) total_count = len(items) for count, item in enumerate(items, start=1): @@ -62,11 +75,11 @@ def execute(): for doctype in ("Website Item Group", "Item Website Specification"): frappe.db.set_value( doctype, - {"parenttype": "Item", "parent": item.item_code}, # filters - {"parenttype": "Website Item", "parent": website_item.name} # value dict + {"parenttype": "Item", "parent": item.item_code}, # filters + {"parenttype": "Website Item", "parent": website_item.name}, # value dict ) - if count % 20 == 0: # commit after every 20 items + if count % 20 == 0: # commit after every 20 items frappe.db.commit() - frappe.utils.update_progress_bar('Creating Website Items', count, total_count) \ No newline at end of file + frappe.utils.update_progress_bar("Creating Website Items", count, total_count) diff --git a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py index ed46e7a60a5..5cbd0b5fcb5 100644 --- a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py +++ b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -6,35 +5,68 @@ from erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings import def execute(): - company = frappe.get_all('Company', filters = {'country': 'United States'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "United States"}, fields=["name"]) if not company: return - TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") + 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") - if (not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE): + if not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE: return custom_fields = { - 'Sales Invoice Item': [ - dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category', - label='Product Tax Category', fetch_from='item_code.product_tax_category'), - dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount', - label='Tax Collectable', read_only=1, options='currency'), - dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable', - label='Taxable Amount', read_only=1, options='currency') + "Sales Invoice Item": [ + dict( + fieldname="product_tax_category", + fieldtype="Link", + insert_after="description", + options="Product Tax Category", + label="Product Tax Category", + fetch_from="item_code.product_tax_category", + ), + dict( + fieldname="tax_collectable", + fieldtype="Currency", + insert_after="net_amount", + label="Tax Collectable", + read_only=1, + options="currency", + ), + dict( + fieldname="taxable_amount", + fieldtype="Currency", + insert_after="tax_collectable", + label="Taxable Amount", + read_only=1, + options="currency", + ), ], - 'Item': [ - dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category', - label='Product Tax Category') + "Item": [ + dict( + fieldname="product_tax_category", + fieldtype="Link", + insert_after="item_group", + options="Product Tax Category", + label="Product Tax Category", + ) + ], + "TaxJar Settings": [ + dict( + fieldname="company", + fieldtype="Link", + insert_after="configuration", + options="Company", + label="Company", + ) ], - 'TaxJar Settings': [ - dict(fieldname='company', fieldtype='Link', insert_after='configuration', options='Company', - label='Company') - ] } create_custom_fields(custom_fields, update=True) add_permissions() - frappe.enqueue('erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories', now=True) + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories", + now=True, + ) diff --git a/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py index 75953b0e304..c53eb794378 100644 --- a/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py +++ b/erpnext/patches/v13_0/delete_bank_reconciliation_detail.py @@ -7,7 +7,8 @@ import frappe def execute(): - if frappe.db.exists('DocType', 'Bank Reconciliation Detail') and \ - frappe.db.exists('DocType', 'Bank Clearance Detail'): + if frappe.db.exists("DocType", "Bank Reconciliation Detail") and frappe.db.exists( + "DocType", "Bank Clearance Detail" + ): - frappe.delete_doc("DocType", 'Bank Reconciliation Detail', force=1) + frappe.delete_doc("DocType", "Bank Reconciliation Detail", force=1) diff --git a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py index 2c5c577978e..3755315813f 100644 --- a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py +++ b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py @@ -22,7 +22,7 @@ def execute(): frappe.delete_doc("Page", "bank-reconciliation", force=1) - frappe.reload_doc('accounts', 'doctype', 'bank_transaction') + frappe.reload_doc("accounts", "doctype", "bank_transaction") rename_field("Bank Transaction", "debit", "deposit") rename_field("Bank Transaction", "credit", "withdrawal") diff --git a/erpnext/patches/v13_0/delete_old_purchase_reports.py b/erpnext/patches/v13_0/delete_old_purchase_reports.py index e57d6d0d3e2..987f53f37c1 100644 --- a/erpnext/patches/v13_0/delete_old_purchase_reports.py +++ b/erpnext/patches/v13_0/delete_old_purchase_reports.py @@ -8,9 +8,12 @@ from erpnext.accounts.utils import check_and_delete_linked_reports def execute(): - reports_to_delete = ["Requested Items To Be Ordered", - "Purchase Order Items To Be Received or Billed","Purchase Order Items To Be Received", - "Purchase Order Items To Be Billed"] + reports_to_delete = [ + "Requested Items To Be Ordered", + "Purchase Order Items To Be Received or Billed", + "Purchase Order Items To Be Received", + "Purchase Order Items To Be Billed", + ] for report in reports_to_delete: if frappe.db.exists("Report", report): @@ -19,8 +22,9 @@ def execute(): frappe.delete_doc("Report", report) + def delete_auto_email_reports(report): - """ Check for one or multiple Auto Email Reports and delete """ + """Check for one or multiple Auto Email Reports and delete""" auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index e6eba0a6085..b31c9d17d71 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -18,14 +18,16 @@ def execute(): frappe.delete_doc("Report", report) + def delete_auto_email_reports(report): - """ Check for one or multiple Auto Email Reports and delete """ + """Check for one or multiple Auto Email Reports and delete""" auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) + def delete_links_from_desktop_icons(report): - """ Check for one or multiple Desktop Icons and delete """ + """Check for one or multiple Desktop Icons and delete""" desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"]) for desktop_icon in desktop_icons: - frappe.delete_doc("Desktop Icon", desktop_icon[0]) \ No newline at end of file + frappe.delete_doc("Desktop Icon", desktop_icon[0]) diff --git a/erpnext/patches/v13_0/delete_orphaned_tables.py b/erpnext/patches/v13_0/delete_orphaned_tables.py index c32f83067bc..794be098a9f 100644 --- a/erpnext/patches/v13_0/delete_orphaned_tables.py +++ b/erpnext/patches/v13_0/delete_orphaned_tables.py @@ -7,63 +7,66 @@ from frappe.utils import getdate def execute(): - frappe.reload_doc('setup', 'doctype', 'transaction_deletion_record') + frappe.reload_doc("setup", "doctype", "transaction_deletion_record") - if has_deleted_company_transactions(): - child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected() + if has_deleted_company_transactions(): + child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected() - for doctype in child_doctypes: - docs = frappe.get_all(doctype, fields=['name', 'parent', 'parenttype', 'creation']) + for doctype in child_doctypes: + docs = frappe.get_all(doctype, fields=["name", "parent", "parenttype", "creation"]) - for doc in docs: - if not frappe.db.exists(doc['parenttype'], doc['parent']): - frappe.db.delete(doctype, {'name': doc['name']}) + for doc in docs: + if not frappe.db.exists(doc["parenttype"], doc["parent"]): + frappe.db.delete(doctype, {"name": doc["name"]}) + + elif check_for_new_doc_with_same_name_as_deleted_parent(doc): + frappe.db.delete(doctype, {"name": doc["name"]}) - elif check_for_new_doc_with_same_name_as_deleted_parent(doc): - frappe.db.delete(doctype, {'name': doc['name']}) def has_deleted_company_transactions(): - return frappe.get_all('Transaction Deletion Record') + return frappe.get_all("Transaction Deletion Record") + def get_child_doctypes_whose_parent_doctypes_were_affected(): - parent_doctypes = get_affected_doctypes() - child_doctypes = frappe.get_all( - 'DocField', - filters={ - 'fieldtype': 'Table', - 'parent':['in', parent_doctypes] - }, pluck='options') + parent_doctypes = get_affected_doctypes() + child_doctypes = frappe.get_all( + "DocField", filters={"fieldtype": "Table", "parent": ["in", parent_doctypes]}, pluck="options" + ) + + return child_doctypes - return child_doctypes def get_affected_doctypes(): - affected_doctypes = [] - tdr_docs = frappe.get_all('Transaction Deletion Record', pluck="name") + affected_doctypes = [] + tdr_docs = frappe.get_all("Transaction Deletion Record", pluck="name") - for tdr in tdr_docs: - tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr) + for tdr in tdr_docs: + tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr) - for doctype in tdr_doc.doctypes: - if is_not_child_table(doctype.doctype_name): - affected_doctypes.append(doctype.doctype_name) + for doctype in tdr_doc.doctypes: + if is_not_child_table(doctype.doctype_name): + affected_doctypes.append(doctype.doctype_name) + + affected_doctypes = remove_duplicate_items(affected_doctypes) + return affected_doctypes - affected_doctypes = remove_duplicate_items(affected_doctypes) - return affected_doctypes def is_not_child_table(doctype): - return not bool(frappe.get_value('DocType', doctype, 'istable')) + return not bool(frappe.get_value("DocType", doctype, "istable")) + def remove_duplicate_items(affected_doctypes): - return list(set(affected_doctypes)) + return list(set(affected_doctypes)) + def check_for_new_doc_with_same_name_as_deleted_parent(doc): - """ - Compares creation times of parent and child docs. - Since Transaction Deletion Record resets the naming series after deletion, - it allows the creation of new docs with the same names as the deleted ones. - """ + """ + Compares creation times of parent and child docs. + Since Transaction Deletion Record resets the naming series after deletion, + it allows the creation of new docs with the same names as the deleted ones. + """ - parent_creation_time = frappe.db.get_value(doc['parenttype'], doc['parent'], 'creation') - child_creation_time = doc['creation'] + parent_creation_time = frappe.db.get_value(doc["parenttype"], doc["parent"], "creation") + child_creation_time = doc["creation"] - return getdate(parent_creation_time) > getdate(child_creation_time) + return getdate(parent_creation_time) > getdate(child_creation_time) diff --git a/erpnext/patches/v13_0/delete_report_requested_items_to_order.py b/erpnext/patches/v13_0/delete_report_requested_items_to_order.py index 87565f0fe42..430a3056cd1 100644 --- a/erpnext/patches/v13_0/delete_report_requested_items_to_order.py +++ b/erpnext/patches/v13_0/delete_report_requested_items_to_order.py @@ -2,12 +2,16 @@ import frappe def execute(): - """ Check for one or multiple Auto Email Reports and delete """ - auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": "Requested Items to Order"}, ["name"]) + """Check for one or multiple Auto Email Reports and delete""" + auto_email_reports = frappe.db.get_values( + "Auto Email Report", {"report": "Requested Items to Order"}, ["name"] + ) for auto_email_report in auto_email_reports: frappe.delete_doc("Auto Email Report", auto_email_report[0]) - frappe.db.sql(""" + frappe.db.sql( + """ DELETE FROM `tabReport` WHERE name = 'Requested Items to Order' - """) + """ + ) 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 index aa2a2d3b785..84b6c37dd9d 100644 --- a/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py +++ b/erpnext/patches/v13_0/disable_ksa_print_format_for_others.py @@ -7,13 +7,13 @@ from erpnext.regional.saudi_arabia.setup import add_print_formats def execute(): - company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) if company: add_print_formats() return - if frappe.db.exists('DocType', 'Print Format'): + 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'): + 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/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py index aea498d8d30..ca166cee74f 100644 --- a/erpnext/patches/v13_0/drop_razorpay_payload_column.py +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -1,8 +1,7 @@ - import frappe def execute(): if frappe.db.exists("DocType", "Membership"): - if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + if "webhook_payload" in frappe.db.get_table_columns("Membership"): frappe.db.sql("alter table `tabMembership` drop column webhook_payload") diff --git a/erpnext/patches/v13_0/enable_ksa_vat_docs.py b/erpnext/patches/v13_0/enable_ksa_vat_docs.py index 3f482620e16..4adf4d71db7 100644 --- a/erpnext/patches/v13_0/enable_ksa_vat_docs.py +++ b/erpnext/patches/v13_0/enable_ksa_vat_docs.py @@ -4,9 +4,9 @@ from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_forma def execute(): - company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + company = frappe.get_all("Company", filters={"country": "Saudi Arabia"}) if not company: return add_print_formats() - add_permissions() \ No newline at end of file + add_permissions() diff --git a/erpnext/patches/v13_0/enable_provisional_accounting.py b/erpnext/patches/v13_0/enable_provisional_accounting.py index 85bbaed89df..7212146e458 100644 --- a/erpnext/patches/v13_0/enable_provisional_accounting.py +++ b/erpnext/patches/v13_0/enable_provisional_accounting.py @@ -9,14 +9,11 @@ def execute(): company = frappe.qb.DocType("Company") - frappe.qb.update( - company - ).set( - company.enable_provisional_accounting_for_non_stock_items, company.enable_perpetual_inventory_for_non_stock_items - ).set( - company.default_provisional_account, company.service_received_but_not_billed - ).where( + frappe.qb.update(company).set( + company.enable_provisional_accounting_for_non_stock_items, + company.enable_perpetual_inventory_for_non_stock_items, + ).set(company.default_provisional_account, company.service_received_but_not_billed).where( company.enable_perpetual_inventory_for_non_stock_items == 1 ).where( company.service_received_but_not_billed.isnotnull() - ).run() \ No newline at end of file + ).run() diff --git a/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py b/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py index 7a51b432117..68b5cde9203 100644 --- a/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py +++ b/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py @@ -2,7 +2,6 @@ import frappe def execute(): - frappe.reload_doc('core', 'doctype', 'scheduled_job_type') - if frappe.db.exists('Scheduled Job Type', 'repost_item_valuation.repost_entries'): - frappe.db.set_value('Scheduled Job Type', - 'repost_item_valuation.repost_entries', 'stopped', 0) + frappe.reload_doc("core", "doctype", "scheduled_job_type") + if frappe.db.exists("Scheduled Job Type", "repost_item_valuation.repost_entries"): + frappe.db.set_value("Scheduled Job Type", "repost_item_valuation.repost_entries", "stopped", 0) diff --git a/erpnext/patches/v13_0/enable_uoms.py b/erpnext/patches/v13_0/enable_uoms.py index 4d3f6376303..8efd67e2806 100644 --- a/erpnext/patches/v13_0/enable_uoms.py +++ b/erpnext/patches/v13_0/enable_uoms.py @@ -2,12 +2,12 @@ import frappe def execute(): - frappe.reload_doc('setup', 'doctype', 'uom') + frappe.reload_doc("setup", "doctype", "uom") uom = frappe.qb.DocType("UOM") - (frappe.qb - .update(uom) + ( + frappe.qb.update(uom) .set(uom.enabled, 1) .where(uom.creation >= "2021-10-18") # date when this field was released ).run() diff --git a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py index 32ad542cf88..9197d86058d 100644 --- a/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py +++ b/erpnext/patches/v13_0/fetch_thumbnail_in_website_items.py @@ -2,15 +2,10 @@ import frappe def execute(): - if frappe.db.has_column("Item", "thumbnail"): - website_item = frappe.qb.DocType("Website Item").as_("wi") - item = frappe.qb.DocType("Item") + if frappe.db.has_column("Item", "thumbnail"): + website_item = frappe.qb.DocType("Website Item").as_("wi") + item = frappe.qb.DocType("Item") - frappe.qb.update(website_item).inner_join(item).on( - website_item.item_code == item.item_code - ).set( - website_item.thumbnail, item.thumbnail - ).where( - website_item.website_image.notnull() - & website_item.thumbnail.isnull() - ).run() + frappe.qb.update(website_item).inner_join(item).on(website_item.item_code == item.item_code).set( + website_item.thumbnail, item.thumbnail + ).where(website_item.website_image.notnull() & website_item.thumbnail.isnull()).run() diff --git a/erpnext/patches/v13_0/fix_invoice_statuses.py b/erpnext/patches/v13_0/fix_invoice_statuses.py index 4395757159f..253b425c58b 100644 --- a/erpnext/patches/v13_0/fix_invoice_statuses.py +++ b/erpnext/patches/v13_0/fix_invoice_statuses.py @@ -8,6 +8,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( TODAY = getdate() + def execute(): # This fix is not related to Party Specific Item, # but it is needed for code introduced after Party Specific Item was @@ -35,39 +36,28 @@ def execute(): fields=fields, filters={ "docstatus": 1, - "status": ("in", ( - "Overdue", - "Overdue and Discounted", - "Partly Paid", - "Partly Paid and Discounted" - )), + "status": ( + "in", + ("Overdue", "Overdue and Discounted", "Partly Paid", "Partly Paid and Discounted"), + ), "outstanding_amount": (">", 0), "modified": (">", "2021-01-01") # an assumption is being made that only invoices modified # after 2021 got affected as incorrectly overdue. # required for performance reasons. - } + }, ) - invoices_to_update = { - invoice.name: invoice for invoice in invoices_to_update - } + invoices_to_update = {invoice.name: invoice for invoice in invoices_to_update} payment_schedule_items = frappe.get_all( "Payment Schedule", - fields=( - "due_date", - "payment_amount", - "base_payment_amount", - "parent" - ), - filters={"parent": ("in", invoices_to_update)} + fields=("due_date", "payment_amount", "base_payment_amount", "parent"), + filters={"parent": ("in", invoices_to_update)}, ) for item in payment_schedule_items: - invoices_to_update[item.parent].setdefault( - "payment_schedule", [] - ).append(item) + invoices_to_update[item.parent].setdefault("payment_schedule", []).append(item) status_map = {} @@ -81,19 +71,11 @@ def execute(): status_map.setdefault(correct_status, []).append(doc.name) for status, docs in status_map.items(): - frappe.db.set_value( - doctype, {"name": ("in", docs)}, - "status", - status, - update_modified=False - ) - + frappe.db.set_value(doctype, {"name": ("in", docs)}, "status", status, update_modified=False) def get_correct_status(doc): - outstanding_amount = flt( - doc.outstanding_amount, doc.precision("outstanding_amount") - ) + outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount")) total = get_total_in_party_account_currency(doc) status = "" diff --git a/erpnext/patches/v13_0/fix_non_unique_represents_company.py b/erpnext/patches/v13_0/fix_non_unique_represents_company.py index e91c1db4dd4..c604f9cb24f 100644 --- a/erpnext/patches/v13_0/fix_non_unique_represents_company.py +++ b/erpnext/patches/v13_0/fix_non_unique_represents_company.py @@ -2,8 +2,10 @@ import frappe def execute(): - frappe.db.sql(""" + frappe.db.sql( + """ update tabCustomer set represents_company = NULL where represents_company = '' - """) + """ + ) diff --git a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py index 72cda751e6c..fc3e68ac677 100644 --- a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py +++ b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py @@ -13,18 +13,24 @@ def execute(): "DATEV". This is no longer necessary. The reference ID for DATEV will be stored in a new custom field "debtor_creditor_number". """ - company_list = frappe.get_all('Company', filters={'country': 'Germany'}) + company_list = frappe.get_all("Company", filters={"country": "Germany"}) for company in company_list: - party_account_list = frappe.get_all('Party Account', filters={'company': company.name}, fields=['name', 'account', 'debtor_creditor_number']) + party_account_list = frappe.get_all( + "Party Account", + filters={"company": company.name}, + fields=["name", "account", "debtor_creditor_number"], + ) for party_account in party_account_list: if (not party_account.account) or party_account.debtor_creditor_number: # account empty or debtor_creditor_number already filled continue - account_number = frappe.db.get_value('Account', party_account.account, 'account_number') + account_number = frappe.db.get_value("Account", party_account.account, "account_number") if not account_number: continue - frappe.db.set_value('Party Account', party_account.name, 'debtor_creditor_number', account_number) - frappe.db.set_value('Party Account', party_account.name, 'account', '') + frappe.db.set_value( + "Party Account", party_account.name, "debtor_creditor_number", account_number + ) + frappe.db.set_value("Party Account", party_account.name, "account", "") diff --git a/erpnext/patches/v13_0/germany_make_custom_fields.py b/erpnext/patches/v13_0/germany_make_custom_fields.py index 80b6a3954a6..cc358135acd 100644 --- a/erpnext/patches/v13_0/germany_make_custom_fields.py +++ b/erpnext/patches/v13_0/germany_make_custom_fields.py @@ -13,7 +13,7 @@ def execute(): It is usually run once at setup of a new company. Since it's new, run it once for existing companies as well. """ - company_list = frappe.get_all('Company', filters = {'country': 'Germany'}) + company_list = frappe.get_all("Company", filters={"country": "Germany"}) if not company_list: return diff --git a/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py b/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py index cb3df3c5ddc..efd2c21d6a0 100644 --- a/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py +++ b/erpnext/patches/v13_0/gst_fields_for_pos_invoice.py @@ -1,43 +1,87 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name']) + company = frappe.get_all("Company", filters={"country": "India"}, fields=["name"]) if not company: return - hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description', - allow_on_submit=1, print_hide=1, fetch_if_empty=1) - nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code', - print_hide=1) - is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', - fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', - print_hide=1) - taxable_value = dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) + hsn_sac_field = dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Data", + fetch_from="item_code.gst_hsn_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + fetch_if_empty=1, + ) + nil_rated_exempt = dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + fetch_from="item_code.is_nil_exempt", + insert_after="gst_hsn_code", + print_hide=1, + ) + is_non_gst = dict( + fieldname="is_non_gst", + label="Is Non GST", + fieldtype="Check", + fetch_from="item_code.is_non_gst", + insert_after="is_nil_exempt", + print_hide=1, + ) + taxable_value = dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) sales_invoice_gst_fields = [ - dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', read_only=1, - fetch_from='customer_address.gstin', print_hide=1), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='shipping_address_name', - fetch_from='shipping_address_name.gstin', print_hide=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=1), - 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="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + ), + ] custom_fields = { - 'POS Invoice': sales_invoice_gst_fields, - 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "POS Invoice": sales_invoice_gst_fields, + "POS Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], } - create_custom_fields(custom_fields, update=True) \ No newline at end of file + create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py b/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py index d43e793b9a9..30b84accf3f 100644 --- a/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py +++ b/erpnext/patches/v13_0/healthcare_lab_module_rename_doctypes.py @@ -1,91 +1,94 @@ - import frappe from frappe.model.utils.rename_field import rename_field def execute(): - if frappe.db.exists('DocType', 'Lab Test') and frappe.db.exists('DocType', 'Lab Test Template'): + if frappe.db.exists("DocType", "Lab Test") and frappe.db.exists("DocType", "Lab Test Template"): # rename child doctypes doctypes = { - 'Lab Test Groups': 'Lab Test Group Template', - 'Normal Test Items': 'Normal Test Result', - 'Sensitivity Test Items': 'Sensitivity Test Result', - 'Special Test Items': 'Descriptive Test Result', - 'Special Test Template': 'Descriptive Test Template' + "Lab Test Groups": "Lab Test Group Template", + "Normal Test Items": "Normal Test Result", + "Sensitivity Test Items": "Sensitivity Test Result", + "Special Test Items": "Descriptive Test Result", + "Special Test Template": "Descriptive Test Template", } - frappe.reload_doc('healthcare', 'doctype', 'lab_test') - frappe.reload_doc('healthcare', 'doctype', 'lab_test_template') + frappe.reload_doc("healthcare", "doctype", "lab_test") + frappe.reload_doc("healthcare", "doctype", "lab_test_template") for old_dt, new_dt in doctypes.items(): frappe.flags.link_fields = {} - should_rename = ( - frappe.db.table_exists(old_dt) - and not frappe.db.table_exists(new_dt) - ) + should_rename = frappe.db.table_exists(old_dt) and not frappe.db.table_exists(new_dt) if should_rename: - frappe.reload_doc('healthcare', 'doctype', frappe.scrub(old_dt)) - frappe.rename_doc('DocType', old_dt, new_dt, force=True) - frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt)) - frappe.delete_doc_if_exists('DocType', old_dt) + frappe.reload_doc("healthcare", "doctype", frappe.scrub(old_dt)) + frappe.rename_doc("DocType", old_dt, new_dt, force=True) + frappe.reload_doc("healthcare", "doctype", frappe.scrub(new_dt)) + frappe.delete_doc_if_exists("DocType", old_dt) parent_fields = { - 'Lab Test Group Template': 'lab_test_groups', - 'Descriptive Test Template': 'descriptive_test_templates', - 'Normal Test Result': 'normal_test_items', - 'Sensitivity Test Result': 'sensitivity_test_items', - 'Descriptive Test Result': 'descriptive_test_items' + "Lab Test Group Template": "lab_test_groups", + "Descriptive Test Template": "descriptive_test_templates", + "Normal Test Result": "normal_test_items", + "Sensitivity Test Result": "sensitivity_test_items", + "Descriptive Test Result": "descriptive_test_items", } for doctype, parentfield in parent_fields.items(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{0}` SET parentfield = %(parentfield)s - """.format(doctype), {'parentfield': parentfield}) + """.format( + doctype + ), + {"parentfield": parentfield}, + ) # copy renamed child table fields (fields were already renamed in old doctype json, hence sql) rename_fields = { - 'lab_test_name': 'test_name', - 'lab_test_event': 'test_event', - 'lab_test_uom': 'test_uom', - 'lab_test_comment': 'test_comment' + "lab_test_name": "test_name", + "lab_test_event": "test_event", + "lab_test_uom": "test_uom", + "lab_test_comment": "test_comment", } for new, old in rename_fields.items(): - if frappe.db.has_column('Normal Test Result', old): - frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}""" - .format(new, old)) + if frappe.db.has_column("Normal Test Result", old): + frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}""".format(new, old)) - if frappe.db.has_column('Normal Test Template', 'test_event'): + if frappe.db.has_column("Normal Test Template", "test_event"): frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""") - if frappe.db.has_column('Normal Test Template', 'test_uom'): + if frappe.db.has_column("Normal Test Template", "test_uom"): frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""") - if frappe.db.has_column('Descriptive Test Result', 'test_particulars'): - frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""") + if frappe.db.has_column("Descriptive Test Result", "test_particulars"): + frappe.db.sql( + """UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""" + ) rename_fields = { - 'lab_test_template': 'test_template', - 'lab_test_description': 'test_description', - 'lab_test_rate': 'test_rate' + "lab_test_template": "test_template", + "lab_test_description": "test_description", + "lab_test_rate": "test_rate", } for new, old in rename_fields.items(): - if frappe.db.has_column('Lab Test Group Template', old): - frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}""" - .format(new, old)) + if frappe.db.has_column("Lab Test Group Template", old): + frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}""".format(new, old)) # rename field - frappe.reload_doc('healthcare', 'doctype', 'lab_test') - if frappe.db.has_column('Lab Test', 'special_toggle'): - rename_field('Lab Test', 'special_toggle', 'descriptive_toggle') + frappe.reload_doc("healthcare", "doctype", "lab_test") + if frappe.db.has_column("Lab Test", "special_toggle"): + rename_field("Lab Test", "special_toggle", "descriptive_toggle") - if frappe.db.exists('DocType', 'Lab Test Group Template'): + if frappe.db.exists("DocType", "Lab Test Group Template"): # fix select field option - frappe.reload_doc('healthcare', 'doctype', 'lab_test_group_template') - frappe.db.sql(""" + frappe.reload_doc("healthcare", "doctype", "lab_test_group_template") + frappe.db.sql( + """ UPDATE `tabLab Test Group Template` SET template_or_new_line = 'Add New Line' WHERE template_or_new_line = 'Add new line' - """) + """ + ) diff --git a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py index 5fe85a48308..33fb8f963c5 100644 --- a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py +++ b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py @@ -7,5 +7,10 @@ def execute(): stock_settings = frappe.get_doc("Stock Settings") - set_by_naming_series("Item", "item_code", - stock_settings.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0) + set_by_naming_series( + "Item", + "item_code", + stock_settings.get("item_naming_by") == "Naming Series", + hide_name_field=True, + make_mandatory=0, + ) diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index 0f2ac4b4514..f6427ca55a6 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -7,18 +7,18 @@ from erpnext.stock.stock_ledger import update_entries_after def execute(): doctypes_to_reload = [ - ("stock", "repost_item_valuation"), - ("stock", "stock_entry_detail"), - ("stock", "purchase_receipt_item"), - ("stock", "delivery_note_item"), - ("stock", "packed_item"), - ("accounts", "sales_invoice_item"), - ("accounts", "purchase_invoice_item"), - ("buying", "purchase_receipt_item_supplied") - ] + ("stock", "repost_item_valuation"), + ("stock", "stock_entry_detail"), + ("stock", "purchase_receipt_item"), + ("stock", "delivery_note_item"), + ("stock", "packed_item"), + ("accounts", "sales_invoice_item"), + ("accounts", "purchase_invoice_item"), + ("buying", "purchase_receipt_item_supplied"), + ] for module, doctype in doctypes_to_reload: - frappe.reload_doc(module, 'doctype', doctype) + frappe.reload_doc(module, "doctype", doctype) reposting_project_deployed_on = get_creation_time() posting_date = getdate(reposting_project_deployed_on) @@ -32,7 +32,8 @@ def execute(): company_list = [] - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company FROM @@ -41,7 +42,10 @@ def execute(): creation > %s and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - ''', reposting_project_deployed_on, as_dict=1) + """, + reposting_project_deployed_on, + as_dict=1, + ) frappe.db.auto_commit_on_many_writes = 1 print("Reposting Stock Ledger Entries...") @@ -51,30 +55,36 @@ def execute(): if d.company not in company_list: company_list.append(d.company) - update_entries_after({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": d.posting_date, - "posting_time": d.posting_time, - "voucher_type": d.voucher_type, - "voucher_no": d.voucher_no, - "sle_id": d.name - }, allow_negative_stock=True) + update_entries_after( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": d.posting_date, + "posting_time": d.posting_time, + "voucher_type": d.voucher_type, + "voucher_no": d.voucher_no, + "sle_id": d.name, + }, + allow_negative_stock=True, + ) i += 1 - if i%100 == 0: + if i % 100 == 0: print(i, "/", total_sle) - print("Reposting General Ledger Entries...") if data: - for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + for row in frappe.get_all("Company", filters={"enable_perpetual_inventory": 1}): if row.name in company_list: update_gl_entries_after(posting_date, posting_time, company=row.name) frappe.db.auto_commit_on_many_writes = 0 + def get_creation_time(): - return frappe.db.sql(''' SELECT create_time FROM - INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0] + return frappe.db.sql( + """ SELECT create_time FROM + INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" """, + as_list=1, + )[0][0] diff --git a/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py index 68bcd8a8da5..69a695ef301 100644 --- a/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py +++ b/erpnext/patches/v13_0/loyalty_points_entry_for_pos_invoice.py @@ -6,15 +6,16 @@ import frappe def execute(): - '''`sales_invoice` field from loyalty point entry is splitted into `invoice_type` & `invoice` fields''' + """`sales_invoice` field from loyalty point entry is splitted into `invoice_type` & `invoice` fields""" frappe.reload_doc("Accounts", "doctype", "loyalty_point_entry") - if not frappe.db.has_column('Loyalty Point Entry', 'sales_invoice'): + if not frappe.db.has_column("Loyalty Point Entry", "sales_invoice"): return frappe.db.sql( """UPDATE `tabLoyalty Point Entry` lpe SET lpe.`invoice_type` = 'Sales Invoice', lpe.`invoice` = lpe.`sales_invoice` WHERE lpe.`sales_invoice` IS NOT NULL - AND (lpe.`invoice` IS NULL OR lpe.`invoice` = '')""") + AND (lpe.`invoice` IS NULL OR lpe.`invoice` = '')""" + ) diff --git a/erpnext/patches/v13_0/make_homepage_products_website_items.py b/erpnext/patches/v13_0/make_homepage_products_website_items.py index 3ca20e2da86..3f23e9cdd5d 100644 --- a/erpnext/patches/v13_0/make_homepage_products_website_items.py +++ b/erpnext/patches/v13_0/make_homepage_products_website_items.py @@ -1,4 +1,3 @@ - import frappe @@ -15,4 +14,4 @@ def execute(): homepage.flags.ignore_mandatory = True homepage.flags.ignore_links = True - homepage.save() \ No newline at end of file + homepage.save() diff --git a/erpnext/patches/v13_0/make_non_standard_user_type.py b/erpnext/patches/v13_0/make_non_standard_user_type.py index 9faf298c262..ba34cb98172 100644 --- a/erpnext/patches/v13_0/make_non_standard_user_type.py +++ b/erpnext/patches/v13_0/make_non_standard_user_type.py @@ -10,15 +10,24 @@ from erpnext.setup.install import add_non_standard_user_types def execute(): doctype_dict = { - 'projects': ['Timesheet'], - 'payroll': ['Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission'], - 'hr': ['Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request'] + "projects": ["Timesheet"], + "payroll": [ + "Salary Slip", + "Employee Tax Exemption Declaration", + "Employee Tax Exemption Proof Submission", + ], + "hr": [ + "Employee", + "Expense Claim", + "Leave Application", + "Attendance Request", + "Compensatory Leave Request", + ], } for module, doctypes in iteritems(doctype_dict): for doctype in doctypes: - frappe.reload_doc(module, 'doctype', doctype) - + frappe.reload_doc(module, "doctype", doctype) frappe.flags.ignore_select_perm = True frappe.flags.update_select_perm_after_migrate = True diff --git a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py index ad79c811c06..492e0403ec4 100644 --- a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py +++ b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py @@ -1,14 +1,14 @@ - import json import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'purchase_invoice_advance') - frappe.reload_doc('accounts', 'doctype', 'sales_invoice_advance') + frappe.reload_doc("accounts", "doctype", "purchase_invoice_advance") + frappe.reload_doc("accounts", "doctype", "sales_invoice_advance") - purchase_invoices = frappe.db.sql(""" + purchase_invoices = frappe.db.sql( + """ select parenttype as type, parent as name from @@ -19,9 +19,12 @@ def execute(): and ifnull(exchange_gain_loss, 0) != 0 group by parent - """, as_dict=1) + """, + as_dict=1, + ) - sales_invoices = frappe.db.sql(""" + sales_invoices = frappe.db.sql( + """ select parenttype as type, parent as name from @@ -32,14 +35,16 @@ def execute(): and ifnull(exchange_gain_loss, 0) != 0 group by parent - """, as_dict=1) + """, + as_dict=1, + ) if purchase_invoices + sales_invoices: frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log") - acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto') + acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto") if acc_frozen_upto: - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) for invoice in purchase_invoices + sales_invoices: try: @@ -48,13 +53,13 @@ def execute(): doc.make_gl_entries() for advance in doc.advances: if advance.ref_exchange_rate == 1: - advance.db_set('exchange_gain_loss', 0, False) + advance.db_set("exchange_gain_loss", 0, False) doc.docstatus = 1 doc.make_gl_entries() frappe.db.commit() except Exception: frappe.db.rollback() - print(f'Failed to correct gl entries of {invoice.name}') + print(f"Failed to correct gl entries of {invoice.name}") if acc_frozen_upto: - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', acc_frozen_upto) \ No newline at end of file + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", acc_frozen_upto) diff --git a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py index 350744fd41f..24061271484 100644 --- a/erpnext/patches/v13_0/move_branch_code_to_bank_account.py +++ b/erpnext/patches/v13_0/move_branch_code_to_bank_account.py @@ -7,11 +7,15 @@ import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'bank_account') - frappe.reload_doc('accounts', 'doctype', 'bank') + frappe.reload_doc("accounts", "doctype", "bank_account") + frappe.reload_doc("accounts", "doctype", "bank") - if frappe.db.has_column('Bank', 'branch_code') and frappe.db.has_column('Bank Account', 'branch_code'): - frappe.db.sql("""UPDATE `tabBank` b, `tabBank Account` ba + if frappe.db.has_column("Bank", "branch_code") and frappe.db.has_column( + "Bank Account", "branch_code" + ): + frappe.db.sql( + """UPDATE `tabBank` b, `tabBank Account` ba SET ba.branch_code = b.branch_code WHERE ba.bank = b.name AND - ifnull(b.branch_code, '') != '' AND ifnull(ba.branch_code, '') = ''""") + ifnull(b.branch_code, '') != '' AND ifnull(ba.branch_code, '') = ''""" + ) diff --git a/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py b/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py index c07caaef661..0290af0f73e 100644 --- a/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py +++ b/erpnext/patches/v13_0/move_doctype_reports_and_notification_from_hr_to_payroll.py @@ -6,47 +6,47 @@ import frappe def execute(): - frappe.db.sql("""UPDATE `tabPrint Format` + frappe.db.sql( + """UPDATE `tabPrint Format` SET module = 'Payroll' WHERE name IN ('Salary Slip Based On Timesheet', 'Salary Slip Standard')""" - ) + ) - frappe.db.sql("""UPDATE `tabNotification` SET module='Payroll' WHERE name='Retention Bonus';""" - ) + frappe.db.sql("""UPDATE `tabNotification` SET module='Payroll' WHERE name='Retention Bonus';""") - doctypes_moved = [ - 'Employee Benefit Application Detail', - 'Employee Tax Exemption Declaration Category', - 'Salary Component', - 'Employee Tax Exemption Proof Submission Detail', - 'Income Tax Slab Other Charges', - 'Taxable Salary Slab', - 'Payroll Period Date', - 'Salary Slip Timesheet', - 'Payroll Employee Detail', - 'Salary Detail', - 'Employee Tax Exemption Sub Category', - 'Employee Tax Exemption Category', - 'Employee Benefit Claim', - 'Employee Benefit Application', - 'Employee Other Income', - 'Employee Tax Exemption Proof Submission', - 'Employee Tax Exemption Declaration', - 'Employee Incentive', - 'Retention Bonus', - 'Additional Salary', - 'Income Tax Slab', - 'Payroll Period', - 'Salary Slip', - 'Payroll Entry', - 'Salary Structure Assignment', - 'Salary Structure' - ] + doctypes_moved = [ + "Employee Benefit Application Detail", + "Employee Tax Exemption Declaration Category", + "Salary Component", + "Employee Tax Exemption Proof Submission Detail", + "Income Tax Slab Other Charges", + "Taxable Salary Slab", + "Payroll Period Date", + "Salary Slip Timesheet", + "Payroll Employee Detail", + "Salary Detail", + "Employee Tax Exemption Sub Category", + "Employee Tax Exemption Category", + "Employee Benefit Claim", + "Employee Benefit Application", + "Employee Other Income", + "Employee Tax Exemption Proof Submission", + "Employee Tax Exemption Declaration", + "Employee Incentive", + "Retention Bonus", + "Additional Salary", + "Income Tax Slab", + "Payroll Period", + "Salary Slip", + "Payroll Entry", + "Salary Structure Assignment", + "Salary Structure", + ] - for doctype in doctypes_moved: - frappe.delete_doc_if_exists("DocType", doctype) + for doctype in doctypes_moved: + frappe.delete_doc_if_exists("DocType", doctype) - reports = ["Salary Register", "Bank Remittance"] + reports = ["Salary Register", "Bank Remittance"] - for report in reports: - frappe.delete_doc_if_exists("Report", report) + for report in reports: + frappe.delete_doc_if_exists("Report", report) diff --git a/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py b/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py index fca7c09c91a..37a3c357b32 100644 --- a/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py +++ b/erpnext/patches/v13_0/move_payroll_setting_separately_from_hr_settings.py @@ -6,7 +6,8 @@ import frappe def execute(): - data = frappe.db.sql('''SELECT * + data = frappe.db.sql( + """SELECT * FROM `tabSingles` WHERE doctype = "HR Settings" @@ -21,7 +22,9 @@ def execute(): "payroll_based_on", "password_policy" ) - ''', as_dict=1) + """, + as_dict=1, + ) - for d in data: - frappe.db.set_value("Payroll Settings", None, d.field, d.value) + for d in data: + frappe.db.set_value("Payroll Settings", None, d.field, d.value) diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index d1ea22f7f26..f84a739d741 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -6,28 +6,42 @@ import frappe def execute(): - if not (frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab")): + if not ( + frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab") + ): return - for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): + for doctype in ( + "income_tax_slab", + "salary_structure_assignment", + "employee_other_income", + "income_tax_slab_other_charges", + ): frappe.reload_doc("Payroll", "doctype", doctype) - - standard_tax_exemption_amount_exists = frappe.db.has_column("Payroll Period", "standard_tax_exemption_amount") + standard_tax_exemption_amount_exists = frappe.db.has_column( + "Payroll Period", "standard_tax_exemption_amount" + ) select_fields = "name, start_date, end_date" if standard_tax_exemption_amount_exists: select_fields = "name, start_date, end_date, standard_tax_exemption_amount" for company in frappe.get_all("Company"): - payroll_periods = frappe.db.sql(""" + payroll_periods = frappe.db.sql( + """ SELECT {0} FROM `tabPayroll Period` WHERE company=%s ORDER BY start_date DESC - """.format(select_fields), company.name, as_dict = 1) + """.format( + select_fields + ), + company.name, + as_dict=1, + ) for i, period in enumerate(payroll_periods): income_tax_slab = frappe.new_doc("Income Tax Slab") @@ -48,13 +62,17 @@ def execute(): income_tax_slab.submit() frappe.db.sql( - """ UPDATE `tabTaxable Salary Slab` + """ UPDATE `tabTaxable Salary Slab` SET parent = %s , parentfield = 'slabs' , parenttype = "Income Tax Slab" WHERE parent = %s - """, (income_tax_slab.name, period.name), as_dict = 1) + """, + (income_tax_slab.name, period.name), + as_dict=1, + ) if i == 0: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSalary Structure Assignment` set @@ -63,16 +81,19 @@ def execute(): company = %s and from_date >= %s and docstatus < 2 - """, (income_tax_slab.name, company.name, period.start_date)) + """, + (income_tax_slab.name, company.name, period.start_date), + ) # move other incomes to separate document if not frappe.db.table_exists("Employee Tax Exemption Proof Submission"): return migrated = [] - proofs = frappe.get_all("Employee Tax Exemption Proof Submission", - filters = {'docstatus': 1}, - fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + proofs = frappe.get_all( + "Employee Tax Exemption Proof Submission", + filters={"docstatus": 1}, + fields=["payroll_period", "employee", "company", "income_from_other_sources"], ) for proof in proofs: if proof.income_from_other_sources: @@ -91,14 +112,17 @@ def execute(): if not frappe.db.table_exists("Employee Tax Exemption Declaration"): return - declerations = frappe.get_all("Employee Tax Exemption Declaration", - filters = {'docstatus': 1}, - fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + declerations = frappe.get_all( + "Employee Tax Exemption Declaration", + filters={"docstatus": 1}, + fields=["payroll_period", "employee", "company", "income_from_other_sources"], ) for declaration in declerations: - if declaration.income_from_other_sources \ - and [declaration.employee, declaration.payroll_period] not in migrated: + if ( + declaration.income_from_other_sources + and [declaration.employee, declaration.payroll_period] not in migrated + ): employee_other_income = frappe.new_doc("Employee Other Income") employee_other_income.employee = declaration.employee employee_other_income.payroll_period = declaration.payroll_period diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py index 3d999bf3240..edd0a9706b9 100644 --- a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py +++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py @@ -1,4 +1,3 @@ - import frappe @@ -11,43 +10,54 @@ def execute(): frappe.reload_doc("hr", "doctype", "Leave Encashment") - additional_salaries = frappe.get_all("Additional Salary", - fields = ['name', "salary_slip", "type", "salary_component"], - filters = {'salary_slip': ['!=', '']}, - group_by = 'salary_slip' + additional_salaries = frappe.get_all( + "Additional Salary", + fields=["name", "salary_slip", "type", "salary_component"], + filters={"salary_slip": ["!=", ""]}, + group_by="salary_slip", ) - leave_encashments = frappe.get_all("Leave Encashment", - fields = ["name","additional_salary"], - filters = {'additional_salary': ['!=', '']} + leave_encashments = frappe.get_all( + "Leave Encashment", + fields=["name", "additional_salary"], + filters={"additional_salary": ["!=", ""]}, ) - employee_incentives = frappe.get_all("Employee Incentive", - fields= ["name", "additional_salary"], - filters = {'additional_salary': ['!=', '']} + employee_incentives = frappe.get_all( + "Employee Incentive", + fields=["name", "additional_salary"], + filters={"additional_salary": ["!=", ""]}, ) for incentive in employee_incentives: - frappe.db.sql(""" UPDATE `tabAdditional Salary` + frappe.db.sql( + """ UPDATE `tabAdditional Salary` SET ref_doctype = 'Employee Incentive', ref_docname = %s WHERE name = %s - """, (incentive['name'], incentive['additional_salary'])) - + """, + (incentive["name"], incentive["additional_salary"]), + ) for leave_encashment in leave_encashments: - frappe.db.sql(""" UPDATE `tabAdditional Salary` + frappe.db.sql( + """ UPDATE `tabAdditional Salary` SET ref_doctype = 'Leave Encashment', ref_docname = %s WHERE name = %s - """, (leave_encashment['name'], leave_encashment['additional_salary'])) + """, + (leave_encashment["name"], leave_encashment["additional_salary"]), + ) salary_slips = [sal["salary_slip"] for sal in additional_salaries] for salary in additional_salaries: - comp_type = "earnings" if salary['type'] == 'Earning' else 'deductions' + comp_type = "earnings" if salary["type"] == "Earning" else "deductions" if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSalary Detail` SET additional_salary = %s WHERE parenttype = 'Salary Slip' and parentfield = %s and parent = %s and salary_component = %s - """, (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"])) + """, + (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]), + ) diff --git a/erpnext/patches/v13_0/populate_e_commerce_settings.py b/erpnext/patches/v13_0/populate_e_commerce_settings.py index 024619c4dba..29427da2bc5 100644 --- a/erpnext/patches/v13_0/populate_e_commerce_settings.py +++ b/erpnext/patches/v13_0/populate_e_commerce_settings.py @@ -1,4 +1,3 @@ - import frappe from frappe.utils import cint @@ -9,23 +8,37 @@ def execute(): frappe.reload_doc("portal", "doctype", "website_attribute") products_settings_fields = [ - "hide_variants", "products_per_page", - "enable_attribute_filters", "enable_field_filters" + "hide_variants", + "products_per_page", + "enable_attribute_filters", + "enable_field_filters", ] shopping_cart_settings_fields = [ - "enabled", "show_attachments", "show_price", - "show_stock_availability", "enable_variants", "show_contact_us_button", - "show_quantity_in_website", "show_apply_coupon_code_in_website", - "allow_items_not_in_stock", "company", "price_list", "default_customer_group", - "quotation_series", "enable_checkout", "payment_success_url", - "payment_gateway_account", "save_quotations_as_draft" + "enabled", + "show_attachments", + "show_price", + "show_stock_availability", + "enable_variants", + "show_contact_us_button", + "show_quantity_in_website", + "show_apply_coupon_code_in_website", + "allow_items_not_in_stock", + "company", + "price_list", + "default_customer_group", + "quotation_series", + "enable_checkout", + "payment_success_url", + "payment_gateway_account", + "save_quotations_as_draft", ] settings = frappe.get_doc("E Commerce Settings") def map_into_e_commerce_settings(doctype, fields): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ Select field, value from `tabSingles` @@ -33,9 +46,11 @@ def execute(): doctype='{doctype}' and field in ({fields}) """.format( - doctype=doctype, - fields=(",").join(['%s'] * len(fields)) - ), tuple(fields), as_dict=1) + doctype=doctype, fields=(",").join(["%s"] * len(fields)) + ), + tuple(fields), + as_dict=1, + ) # {'enable_attribute_filters': '1', ...} mapper = {row.field: row.value for row in data} @@ -52,10 +67,14 @@ def execute(): # move filters and attributes tables to E Commerce Settings from Products Settings for doctype in ("Website Filter Field", "Website Attribute"): - frappe.db.sql("""Update `tab{doctype}` + frappe.db.sql( + """Update `tab{doctype}` set parenttype = 'E Commerce Settings', parent = 'E Commerce Settings' where parent = 'Products Settings' - """.format(doctype=doctype)) \ No newline at end of file + """.format( + doctype=doctype + ) + ) diff --git a/erpnext/patches/v13_0/print_uom_after_quantity_patch.py b/erpnext/patches/v13_0/print_uom_after_quantity_patch.py index 3da6f749aff..a16f909fc38 100644 --- a/erpnext/patches/v13_0/print_uom_after_quantity_patch.py +++ b/erpnext/patches/v13_0/print_uom_after_quantity_patch.py @@ -6,4 +6,4 @@ from erpnext.setup.install import create_print_uom_after_qty_custom_field def execute(): - create_print_uom_after_qty_custom_field() + create_print_uom_after_qty_custom_field() diff --git a/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py b/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py index bbe3eb5815b..4efbe4df96a 100644 --- a/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py +++ b/erpnext/patches/v13_0/remove_attribute_field_from_item_variant_setting.py @@ -5,5 +5,7 @@ def execute(): """Remove has_variants and attribute fields from item variant settings.""" frappe.reload_doc("stock", "doctype", "Item Variant Settings") - frappe.db.sql("""delete from `tabVariant Field` - where field_name in ('attributes', 'has_variants')""") + frappe.db.sql( + """delete from `tabVariant Field` + where field_name in ('attributes', 'has_variants')""" + ) diff --git a/erpnext/patches/v13_0/remove_bad_selling_defaults.py b/erpnext/patches/v13_0/remove_bad_selling_defaults.py index 381c3902da0..efd2098d9ed 100644 --- a/erpnext/patches/v13_0/remove_bad_selling_defaults.py +++ b/erpnext/patches/v13_0/remove_bad_selling_defaults.py @@ -12,5 +12,5 @@ def execute(): if selling_settings.territory in (_("All Territories"), "All Territories"): selling_settings.territory = None - selling_settings.flags.ignore_mandatory=True + selling_settings.flags.ignore_mandatory = True selling_settings.save(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py index 0284097e281..35c93805c8b 100644 --- a/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py +++ b/erpnext/patches/v13_0/remove_unknown_links_to_prod_plan_items.py @@ -11,16 +11,16 @@ def execute(): pp_item = frappe.qb.DocType("Production Plan Item") broken_work_orders = ( - frappe.qb - .from_(work_order) - .left_join(pp_item).on(work_order.production_plan_item == pp_item.name) - .select(work_order.name) - .where( - (work_order.docstatus == 0) - & (work_order.production_plan_item.notnull()) - & (work_order.production_plan_item.like("new-production-plan%")) - & (pp_item.name.isnull()) - ) + frappe.qb.from_(work_order) + .left_join(pp_item) + .on(work_order.production_plan_item == pp_item.name) + .select(work_order.name) + .where( + (work_order.docstatus == 0) + & (work_order.production_plan_item.notnull()) + & (work_order.production_plan_item.like("new-production-plan%")) + & (pp_item.name.isnull()) + ) ).run() if not broken_work_orders: @@ -28,10 +28,8 @@ def execute(): broken_work_order_names = [d[0] for d in broken_work_orders] - (frappe.qb - .update(work_order) + ( + frappe.qb.update(work_order) .set(work_order.production_plan_item, None) .where(work_order.name.isin(broken_work_order_names)) ).run() - - diff --git a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py index e9778043229..3bd717d77b8 100644 --- a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py +++ b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field diff --git a/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py index 002166409dc..7010f47275b 100644 --- a/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py +++ b/erpnext/patches/v13_0/rename_discharge_ordered_date_in_ip_record.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py index 80d51652abe..a9b6df70985 100644 --- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -7,63 +7,78 @@ from frappe.model.utils.rename_field import rename_field def execute(): - if frappe.db.exists('DocType', 'Issue'): - issues = frappe.db.get_all('Issue', fields=['name', 'response_by_variance', 'resolution_by_variance', 'mins_to_first_response'], - order_by='creation desc') - frappe.reload_doc('support', 'doctype', 'issue') + if frappe.db.exists("DocType", "Issue"): + issues = frappe.db.get_all( + "Issue", + fields=["name", "response_by_variance", "resolution_by_variance", "mins_to_first_response"], + order_by="creation desc", + ) + frappe.reload_doc("support", "doctype", "issue") # rename fields rename_map = { - 'agreement_fulfilled': 'agreement_status', - 'mins_to_first_response': 'first_response_time' + "agreement_fulfilled": "agreement_status", + "mins_to_first_response": "first_response_time", } for old, new in rename_map.items(): - rename_field('Issue', old, new) + rename_field("Issue", old, new) # change fieldtype to duration count = 0 for entry in issues: - response_by_variance = convert_to_seconds(entry.response_by_variance, 'Hours') - resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, 'Hours') - mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') - frappe.db.set_value('Issue', entry.name, { - 'response_by_variance': response_by_variance, - 'resolution_by_variance': resolution_by_variance, - 'first_response_time': mins_to_first_response - }, update_modified=False) + response_by_variance = convert_to_seconds(entry.response_by_variance, "Hours") + resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, "Hours") + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, "Minutes") + frappe.db.set_value( + "Issue", + entry.name, + { + "response_by_variance": response_by_variance, + "resolution_by_variance": resolution_by_variance, + "first_response_time": mins_to_first_response, + }, + update_modified=False, + ) # commit after every 100 updates count += 1 - if count%100 == 0: + if count % 100 == 0: frappe.db.commit() - if frappe.db.exists('DocType', 'Opportunity'): - opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') - frappe.reload_doctype('Opportunity', force=True) - rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') + if frappe.db.exists("DocType", "Opportunity"): + opportunities = frappe.db.get_all( + "Opportunity", fields=["name", "mins_to_first_response"], order_by="creation desc" + ) + frappe.reload_doctype("Opportunity", force=True) + rename_field("Opportunity", "mins_to_first_response", "first_response_time") # change fieldtype to duration - frappe.reload_doc('crm', 'doctype', 'opportunity', force=True) + frappe.reload_doc("crm", "doctype", "opportunity", force=True) count = 0 for entry in opportunities: - mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') - frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response, update_modified=False) + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, "Minutes") + frappe.db.set_value( + "Opportunity", entry.name, "first_response_time", mins_to_first_response, update_modified=False + ) # commit after every 100 updates count += 1 - if count%100 == 0: + if count % 100 == 0: frappe.db.commit() # renamed reports from "Minutes to First Response for Issues" to "First Response Time for Issues". Same for Opportunity - for report in ['Minutes to First Response for Issues', 'Minutes to First Response for Opportunity']: - if frappe.db.exists('Report', report): - frappe.delete_doc('Report', report, ignore_permissions=True) + for report in [ + "Minutes to First Response for Issues", + "Minutes to First Response for Opportunity", + ]: + if frappe.db.exists("Report", report): + frappe.delete_doc("Report", report, ignore_permissions=True) def convert_to_seconds(value, unit): seconds = 0 if not value: return seconds - if unit == 'Hours': + if unit == "Hours": seconds = value * 3600 - if unit == 'Minutes': + if unit == "Minutes": seconds = value * 60 return seconds diff --git a/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py b/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py index b129cbe80bc..d3896dd0a84 100644 --- a/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py +++ b/erpnext/patches/v13_0/rename_issue_status_hold_to_on_hold.py @@ -6,16 +6,19 @@ import frappe def execute(): - if frappe.db.exists('DocType', 'Issue'): + if frappe.db.exists("DocType", "Issue"): frappe.reload_doc("support", "doctype", "issue") rename_status() + def rename_status(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabIssue` SET status = 'On Hold' WHERE status = 'Hold' - """) + """ + ) diff --git a/erpnext/patches/v13_0/rename_ksa_qr_field.py b/erpnext/patches/v13_0/rename_ksa_qr_field.py index f4f9b17fb81..e4b91412ee1 100644 --- a/erpnext/patches/v13_0/rename_ksa_qr_field.py +++ b/erpnext/patches/v13_0/rename_ksa_qr_field.py @@ -7,26 +7,30 @@ from frappe.model.utils.rename_field import rename_field def execute(): - company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'}) + 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.exists("DocType", "Sales Invoice"): + frappe.reload_doc("accounts", "doctype", "sales_invoice", force=True) # rename_field method assumes that the field already exists or the doc is synced - if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'): - create_custom_fields({ - 'Sales Invoice': [ - dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 - ) - ] - }) + if not frappe.db.has_column("Sales Invoice", "ksa_einv_qr"): + create_custom_fields( + { + "Sales Invoice": [ + dict( + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, + ) + ] + } + ) - if frappe.db.has_column('Sales Invoice', 'qr_code'): - rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr') + if frappe.db.has_column("Sales Invoice", "qr_code"): + rename_field("Sales Invoice", "qr_code", "ksa_einv_qr") frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code") diff --git a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py index ecd03441e0a..0e5423444aa 100644 --- a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py +++ b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -16,7 +15,7 @@ def execute(): "enable_razorpay": "enable_razorpay_for_memberships", "debit_account": "membership_debit_account", "payment_account": "membership_payment_account", - "webhook_secret": "membership_webhook_secret" + "webhook_secret": "membership_webhook_secret", } for old_name, new_name in rename_fields_map.items(): diff --git a/erpnext/patches/v13_0/rename_non_profit_fields.py b/erpnext/patches/v13_0/rename_non_profit_fields.py index b6fc0a72c10..285403c004f 100644 --- a/erpnext/patches/v13_0/rename_non_profit_fields.py +++ b/erpnext/patches/v13_0/rename_non_profit_fields.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -14,4 +13,4 @@ def execute(): frappe.reload_doc("regional", "doctype", "Tax Exemption 80G Certificate Detail") rename_field("Tax Exemption 80G Certificate", "razorpay_payment_id", "payment_id") - rename_field("Tax Exemption 80G Certificate Detail", "razorpay_payment_id", "payment_id") \ No newline at end of file + rename_field("Tax Exemption 80G Certificate Detail", "razorpay_payment_id", "payment_id") diff --git a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py index 28054317ad3..434dbb47e76 100644 --- a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py +++ b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py @@ -3,22 +3,19 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('hr', 'doctype', 'hr_settings') + frappe.reload_doc("hr", "doctype", "hr_settings") try: # Rename the field - rename_field('HR Settings', 'stop_birthday_reminders', 'send_birthday_reminders') + rename_field("HR Settings", "stop_birthday_reminders", "send_birthday_reminders") # Reverse the value - old_value = frappe.db.get_single_value('HR Settings', 'send_birthday_reminders') + old_value = frappe.db.get_single_value("HR Settings", "send_birthday_reminders") frappe.db.set_value( - 'HR Settings', - 'HR Settings', - 'send_birthday_reminders', - 1 if old_value == 0 else 0 + "HR Settings", "HR Settings", "send_birthday_reminders", 1 if old_value == 0 else 0 ) except Exception as e: if e.args[0] != 1054: - raise \ No newline at end of file + raise diff --git a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py index f49b1e4bd8c..7d757b7a0fb 100644 --- a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py +++ b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py @@ -1,4 +1,3 @@ - import frappe diff --git a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py index a2c960c8f37..ba2feb3a4d0 100644 --- a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py +++ b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py @@ -10,9 +10,13 @@ def execute(): pos_profiles = frappe.get_all("POS Profile") for pos_profile in pos_profiles: - payments = frappe.db.sql(""" + payments = frappe.db.sql( + """ select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s - """, pos_profile.name, as_dict=1) + """, + pos_profile.name, + as_dict=1, + ) if payments: for payment_mode in payments: pos_payment_method = frappe.new_doc("POS Payment Method") diff --git a/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py b/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py index ba96fdd2266..bf82f443082 100644 --- a/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py +++ b/erpnext/patches/v13_0/replace_supplier_item_group_with_party_specific_item.py @@ -5,7 +5,7 @@ import frappe def execute(): - if frappe.db.table_exists('Supplier Item Group'): + if frappe.db.table_exists("Supplier Item Group"): frappe.reload_doc("selling", "doctype", "party_specific_item") sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"]) for item in sig: diff --git a/erpnext/patches/v13_0/requeue_failed_reposts.py b/erpnext/patches/v13_0/requeue_failed_reposts.py index 213cb9e26e4..752490da304 100644 --- a/erpnext/patches/v13_0/requeue_failed_reposts.py +++ b/erpnext/patches/v13_0/requeue_failed_reposts.py @@ -4,9 +4,11 @@ from frappe.utils import cstr def execute(): - reposts = frappe.get_all("Repost Item Valuation", - {"status": "Failed", "modified": [">", "2021-10-05"] }, - ["name", "modified", "error_log"]) + reposts = frappe.get_all( + "Repost Item Valuation", + {"status": "Failed", "modified": [">", "2021-10-05"]}, + ["name", "modified", "error_log"], + ) for repost in reposts: if "check_freezing_date" in cstr(repost.error_log): diff --git a/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py index e6717c57db3..5dfea5eaed7 100644 --- a/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py +++ b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py @@ -6,40 +6,35 @@ import frappe def execute(): - """ - Reset Clearance Date for Payment Entries of type Internal Transfer that have only been reconciled with one Bank Transaction. - This will allow the Payment Entries to be reconciled with the second Bank Transaction using the Bank Reconciliation Tool. - """ + """ + Reset Clearance Date for Payment Entries of type Internal Transfer that have only been reconciled with one Bank Transaction. + This will allow the Payment Entries to be reconciled with the second Bank Transaction using the Bank Reconciliation Tool. + """ - intra_company_pe = get_intra_company_payment_entries_with_clearance_dates() - reconciled_bank_transactions = get_reconciled_bank_transactions(intra_company_pe) + intra_company_pe = get_intra_company_payment_entries_with_clearance_dates() + reconciled_bank_transactions = get_reconciled_bank_transactions(intra_company_pe) + + for payment_entry in reconciled_bank_transactions: + if len(reconciled_bank_transactions[payment_entry]) == 1: + frappe.db.set_value("Payment Entry", payment_entry, "clearance_date", None) - for payment_entry in reconciled_bank_transactions: - if len(reconciled_bank_transactions[payment_entry]) == 1: - frappe.db.set_value('Payment Entry', payment_entry, 'clearance_date', None) def get_intra_company_payment_entries_with_clearance_dates(): - return frappe.get_all( - 'Payment Entry', - filters = { - 'payment_type': 'Internal Transfer', - 'clearance_date': ["not in", None] - }, - pluck = 'name' - ) + return frappe.get_all( + "Payment Entry", + filters={"payment_type": "Internal Transfer", "clearance_date": ["not in", None]}, + pluck="name", + ) + def get_reconciled_bank_transactions(intra_company_pe): - """Returns dictionary where each key:value pair is Payment Entry : List of Bank Transactions reconciled with Payment Entry""" + """Returns dictionary where each key:value pair is Payment Entry : List of Bank Transactions reconciled with Payment Entry""" - reconciled_bank_transactions = {} + reconciled_bank_transactions = {} - for payment_entry in intra_company_pe: - reconciled_bank_transactions[payment_entry] = frappe.get_all( - 'Bank Transaction Payments', - filters = { - 'payment_entry': payment_entry - }, - pluck='parent' - ) + for payment_entry in intra_company_pe: + reconciled_bank_transactions[payment_entry] = frappe.get_all( + "Bank Transaction Payments", filters={"payment_entry": payment_entry}, pluck="parent" + ) - return reconciled_bank_transactions \ No newline at end of file + return reconciled_bank_transactions diff --git a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py index b955686a17e..bc2d1b94f79 100644 --- a/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py +++ b/erpnext/patches/v13_0/set_company_field_in_healthcare_doctypes.py @@ -1,11 +1,25 @@ - import frappe def execute(): - company = frappe.db.get_single_value('Global Defaults', 'default_company') - doctypes = ['Clinical Procedure', 'Inpatient Record', 'Lab Test', 'Sample Collection', 'Patient Appointment', 'Patient Encounter', 'Vital Signs', 'Therapy Session', 'Therapy Plan', 'Patient Assessment'] + company = frappe.db.get_single_value("Global Defaults", "default_company") + doctypes = [ + "Clinical Procedure", + "Inpatient Record", + "Lab Test", + "Sample Collection", + "Patient Appointment", + "Patient Encounter", + "Vital Signs", + "Therapy Session", + "Therapy Plan", + "Patient Assessment", + ] for entry in doctypes: - if frappe.db.exists('DocType', entry): - frappe.reload_doc('Healthcare', 'doctype', entry) - frappe.db.sql("update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format(dt=entry, company=frappe.db.escape(company))) + if frappe.db.exists("DocType", entry): + frappe.reload_doc("Healthcare", "doctype", entry) + frappe.db.sql( + "update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format( + dt=entry, company=frappe.db.escape(company) + ) + ) diff --git a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py index c744f35b72f..adc8784b5ba 100644 --- a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py +++ b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py @@ -2,7 +2,11 @@ import frappe def execute(): - frappe.reload_doc('HR', 'doctype', 'Leave Allocation') - frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry') - frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""") - frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""") + frappe.reload_doc("HR", "doctype", "Leave Allocation") + frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") + frappe.db.sql( + """update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""" + ) + frappe.db.sql( + """update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""" + ) diff --git a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py index 0366d4902dc..ef02e250c89 100644 --- a/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py +++ b/erpnext/patches/v13_0/set_operation_time_based_on_operating_cost.py @@ -2,10 +2,11 @@ import frappe def execute(): - frappe.reload_doc('manufacturing', 'doctype', 'bom') - frappe.reload_doc('manufacturing', 'doctype', 'bom_operation') + frappe.reload_doc("manufacturing", "doctype", "bom") + frappe.reload_doc("manufacturing", "doctype", "bom_operation") - frappe.db.sql(''' + frappe.db.sql( + """ UPDATE `tabBOM Operation` SET @@ -13,4 +14,5 @@ def execute(): WHERE time_in_mins = 0 AND operating_cost > 0 AND hour_rate > 0 AND docstatus = 1 AND parenttype = "BOM" - ''') \ No newline at end of file + """ + ) diff --git a/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py b/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py index 3b751415eed..0cefa028ec4 100644 --- a/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py +++ b/erpnext/patches/v13_0/set_payment_channel_in_payment_gateway_account.py @@ -1,4 +1,3 @@ - import frappe @@ -11,8 +10,11 @@ def execute(): frappe.reload_doc("Accounts", "doctype", "Payment Gateway Account") set_payment_channel_as_email() + def set_payment_channel_as_email(): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabPayment Gateway Account` SET `payment_channel` = "Email" - """) + """ + ) diff --git a/erpnext/patches/v13_0/set_pos_closing_as_failed.py b/erpnext/patches/v13_0/set_pos_closing_as_failed.py index a838ce07b93..e2226c1cf0a 100644 --- a/erpnext/patches/v13_0/set_pos_closing_as_failed.py +++ b/erpnext/patches/v13_0/set_pos_closing_as_failed.py @@ -1,8 +1,7 @@ - import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'pos_closing_entry') + frappe.reload_doc("accounts", "doctype", "pos_closing_entry") - frappe.db.sql("update `tabPOS Closing Entry` set `status` = 'Failed' where `status` = 'Queued'") + frappe.db.sql("update `tabPOS Closing Entry` set `status` = 'Failed' where `status` = 'Queued'") diff --git a/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py index 9887ad9df0c..e1c8526f10d 100644 --- a/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py +++ b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py @@ -3,8 +3,10 @@ import frappe def execute(): frappe.reload_doc("maintenance", "doctype", "Maintenance Schedule Detail") - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabMaintenance Schedule Detail` SET completion_status = 'Pending' WHERE docstatus < 2 - """) + """ + ) diff --git a/erpnext/patches/v13_0/set_training_event_attendance.py b/erpnext/patches/v13_0/set_training_event_attendance.py index 27a9d3ff089..7b557589723 100644 --- a/erpnext/patches/v13_0/set_training_event_attendance.py +++ b/erpnext/patches/v13_0/set_training_event_attendance.py @@ -1,10 +1,11 @@ - import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'training_event') - frappe.reload_doc('hr', 'doctype', 'training_event_employee') + frappe.reload_doc("hr", "doctype", "training_event") + frappe.reload_doc("hr", "doctype", "training_event_employee") - frappe.db.sql("update `tabTraining Event Employee` set `attendance` = 'Present'") - frappe.db.sql("update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'") + frappe.db.sql("update `tabTraining Event Employee` set `attendance` = 'Present'") + frappe.db.sql( + "update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'" + ) diff --git a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py index f097ab9297f..1adf0d84538 100644 --- a/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py +++ b/erpnext/patches/v13_0/set_work_order_qty_in_so_from_mr.py @@ -2,35 +2,37 @@ import frappe def execute(): - """ - 1. Get submitted Work Orders with MR, MR Item and SO set - 2. Get SO Item detail from MR Item detail in WO, and set in WO - 3. Update work_order_qty in SO - """ - work_order = frappe.qb.DocType("Work Order") - query = ( - frappe.qb.from_(work_order) - .select( - work_order.name, work_order.produced_qty, - work_order.material_request, - work_order.material_request_item, - work_order.sales_order - ).where( - (work_order.material_request.isnotnull()) - & (work_order.material_request_item.isnotnull()) - & (work_order.sales_order.isnotnull()) - & (work_order.docstatus == 1) - & (work_order.produced_qty > 0) - ) - ) - results = query.run(as_dict=True) + """ + 1. Get submitted Work Orders with MR, MR Item and SO set + 2. Get SO Item detail from MR Item detail in WO, and set in WO + 3. Update work_order_qty in SO + """ + work_order = frappe.qb.DocType("Work Order") + query = ( + frappe.qb.from_(work_order) + .select( + work_order.name, + work_order.produced_qty, + work_order.material_request, + work_order.material_request_item, + work_order.sales_order, + ) + .where( + (work_order.material_request.isnotnull()) + & (work_order.material_request_item.isnotnull()) + & (work_order.sales_order.isnotnull()) + & (work_order.docstatus == 1) + & (work_order.produced_qty > 0) + ) + ) + results = query.run(as_dict=True) - for row in results: - so_item = frappe.get_value( - "Material Request Item", row.material_request_item, "sales_order_item" - ) - frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) + for row in results: + so_item = frappe.get_value( + "Material Request Item", row.material_request_item, "sales_order_item" + ) + frappe.db.set_value("Work Order", row.name, "sales_order_item", so_item) - if so_item: - wo = frappe.get_doc("Work Order", row.name) - wo.update_work_order_qty_in_so() + if so_item: + wo = frappe.get_doc("Work Order", row.name) + wo.update_work_order_qty_in_so() diff --git a/erpnext/patches/v13_0/set_youtube_video_id.py b/erpnext/patches/v13_0/set_youtube_video_id.py index 76aaaea279c..9766bb871cf 100644 --- a/erpnext/patches/v13_0/set_youtube_video_id.py +++ b/erpnext/patches/v13_0/set_youtube_video_id.py @@ -1,11 +1,10 @@ - import frappe from erpnext.utilities.doctype.video.video import get_id_from_url def execute(): - frappe.reload_doc("utilities", "doctype","video") + frappe.reload_doc("utilities", "doctype", "video") for video in frappe.get_all("Video", fields=["name", "url", "youtube_video_id"]): if video.url and not video.youtube_video_id: diff --git a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py index 36bedf4f9ba..40c10f30354 100644 --- a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py +++ b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py @@ -1,12 +1,11 @@ - import frappe from erpnext.regional.india.setup import add_custom_roles_for_reports def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) - if not company: - return + company = frappe.get_all("Company", filters={"country": "India"}) + if not company: + return - add_custom_roles_for_reports() + add_custom_roles_for_reports() diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py index 2d35ea34587..1c36b536841 100644 --- a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py +++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py @@ -4,15 +4,13 @@ from erpnext.regional.india.setup import make_custom_fields def execute(): - if frappe.get_all('Company', filters = {'country': 'India'}): - frappe.reload_doc('accounts', 'doctype', 'POS Invoice') - frappe.reload_doc('accounts', 'doctype', 'POS Invoice Item') + if frappe.get_all("Company", filters={"country": "India"}): + frappe.reload_doc("accounts", "doctype", "POS Invoice") + frappe.reload_doc("accounts", "doctype", "POS Invoice Item") make_custom_fields() - if not frappe.db.exists('Party Type', 'Donor'): - frappe.get_doc({ - 'doctype': 'Party Type', - 'party_type': 'Donor', - 'account_type': 'Receivable' - }).insert(ignore_permissions=True) + if not frappe.db.exists("Party Type", "Donor"): + frappe.get_doc( + {"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"} + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py index 82cc1ff771c..093e8a7646f 100644 --- a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py +++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py @@ -6,12 +6,14 @@ import frappe def execute(): - frappe.reload_doc('payroll', 'doctype', 'gratuity_rule') - frappe.reload_doc('payroll', 'doctype', 'gratuity_rule_slab') - frappe.reload_doc('payroll', 'doctype', 'gratuity_applicable_component') - if frappe.db.exists("Company", {"country": "India"}): - from erpnext.regional.india.setup import create_gratuity_rule - create_gratuity_rule() - if frappe.db.exists("Company", {"country": "United Arab Emirates"}): - from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule - create_gratuity_rule() + frappe.reload_doc("payroll", "doctype", "gratuity_rule") + frappe.reload_doc("payroll", "doctype", "gratuity_rule_slab") + frappe.reload_doc("payroll", "doctype", "gratuity_applicable_component") + if frappe.db.exists("Company", {"country": "India"}): + from erpnext.regional.india.setup import create_gratuity_rule + + create_gratuity_rule() + if frappe.db.exists("Company", {"country": "United Arab Emirates"}): + from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule + + create_gratuity_rule() diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py index ee8f96d0070..339acfe426b 100644 --- a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py +++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py @@ -1,4 +1,3 @@ - import frappe from erpnext.healthcare.setup import setup_patient_history_settings diff --git a/erpnext/patches/v13_0/setup_uae_vat_fields.py b/erpnext/patches/v13_0/setup_uae_vat_fields.py index d89e0521d8d..70466465e42 100644 --- a/erpnext/patches/v13_0/setup_uae_vat_fields.py +++ b/erpnext/patches/v13_0/setup_uae_vat_fields.py @@ -7,12 +7,12 @@ from erpnext.regional.united_arab_emirates.setup import setup def execute(): - company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'}) + company = frappe.get_all("Company", filters={"country": "United Arab Emirates"}) if not company: return - frappe.reload_doc('regional', 'report', 'uae_vat_201') - frappe.reload_doc('regional', 'doctype', 'uae_vat_settings') - frappe.reload_doc('regional', 'doctype', 'uae_vat_account') + frappe.reload_doc("regional", "report", "uae_vat_201") + frappe.reload_doc("regional", "doctype", "uae_vat_settings") + frappe.reload_doc("regional", "doctype", "uae_vat_account") setup() diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py index 968a83a4212..005980e80a5 100644 --- a/erpnext/patches/v13_0/stock_entry_enhancements.py +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -6,27 +6,31 @@ import frappe def execute(): - frappe.reload_doc("stock", "doctype", "stock_entry") - if frappe.db.has_column("Stock Entry", "add_to_transit"): - frappe.db.sql(""" + frappe.reload_doc("stock", "doctype", "stock_entry") + if frappe.db.has_column("Stock Entry", "add_to_transit"): + frappe.db.sql( + """ UPDATE `tabStock Entry` SET stock_entry_type = 'Material Transfer', purpose = 'Material Transfer', add_to_transit = 1 WHERE stock_entry_type = 'Send to Warehouse' - """) + """ + ) - frappe.db.sql("""UPDATE `tabStock Entry` SET + frappe.db.sql( + """UPDATE `tabStock Entry` SET stock_entry_type = 'Material Transfer', purpose = 'Material Transfer' WHERE stock_entry_type = 'Receive at Warehouse' - """) + """ + ) - frappe.reload_doc("stock", "doctype", "warehouse_type") - if not frappe.db.exists('Warehouse Type', 'Transit'): - doc = frappe.new_doc('Warehouse Type') - doc.name = 'Transit' - doc.insert() + frappe.reload_doc("stock", "doctype", "warehouse_type") + if not frappe.db.exists("Warehouse Type", "Transit"): + doc = frappe.new_doc("Warehouse Type") + doc.name = "Transit" + doc.insert() - frappe.reload_doc("stock", "doctype", "stock_entry_type") - frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") - frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") + frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") diff --git a/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py b/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py index fd48c0d902d..5f3fc5761ac 100644 --- a/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py +++ b/erpnext/patches/v13_0/trim_sales_invoice_custom_field_length.py @@ -7,12 +7,10 @@ from erpnext.regional.india.setup import create_custom_fields, get_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - custom_fields = { - 'Sales Invoice': get_custom_fields().get('Sales Invoice') - } + custom_fields = {"Sales Invoice": get_custom_fields().get("Sales Invoice")} create_custom_fields(custom_fields, update=True) diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py index 4ec22e9d0e1..b69a408e65b 100644 --- a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -4,7 +4,8 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos def execute(): - broken_sles = frappe.db.sql(""" + broken_sles = frappe.db.sql( + """ select name, serial_no from `tabStock Ledger Entry` where @@ -12,15 +13,15 @@ def execute(): and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s or serial_no = %s ) """, - ( - " %", # leading whitespace - "% ", # trailing whitespace - "%\n %", # leading whitespace on newline - "% \n%", # trailing whitespace on newline - "\n", # just new line - ), - as_dict=True, - ) + ( + " %", # leading whitespace + "% ", # trailing whitespace + "%\n %", # leading whitespace on newline + "% \n%", # trailing whitespace on newline + "\n", # just new line + ), + as_dict=True, + ) frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles) @@ -37,7 +38,9 @@ def execute(): if correct_sr_no == sle.serial_no: continue - frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False) + frappe.db.set_value( + "Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False + ) broken_serial_nos.update(serial_no_list) if not broken_serial_nos: @@ -45,14 +48,15 @@ def execute(): # Patch serial No documents if they don't have purchase info # Purchase info is used for fetching incoming rate - broken_sr_no_records = frappe.get_list("Serial No", - filters={ - "status":"Active", - "name": ("in", broken_serial_nos), - "purchase_document_type": ("is", "not set") - }, - pluck="name", - ) + broken_sr_no_records = frappe.get_list( + "Serial No", + filters={ + "status": "Active", + "name": ("in", broken_serial_nos), + "purchase_document_type": ("is", "not set"), + }, + pluck="name", + ) frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records) diff --git a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py index 0b26f55e002..f593dc28410 100644 --- a/erpnext/patches/v13_0/update_accounts_in_loan_docs.py +++ b/erpnext/patches/v13_0/update_accounts_in_loan_docs.py @@ -3,39 +3,21 @@ import frappe def execute(): - frappe.reload_doc('loan_management', 'doctype', 'loan') - frappe.reload_doc('loan_management', 'doctype', 'loan_disbursement') - frappe.reload_doc('loan_management', 'doctype', 'loan_repayment') + frappe.reload_doc("loan_management", "doctype", "loan") + frappe.reload_doc("loan_management", "doctype", "loan_disbursement") + frappe.reload_doc("loan_management", "doctype", "loan_repayment") ld = frappe.qb.DocType("Loan Disbursement").as_("ld") lr = frappe.qb.DocType("Loan Repayment").as_("lr") loan = frappe.qb.DocType("Loan") - frappe.qb.update( - ld - ).inner_join( - loan - ).on( - loan.name == ld.against_loan - ).set( + frappe.qb.update(ld).inner_join(loan).on(loan.name == ld.against_loan).set( ld.disbursement_account, loan.disbursement_account - ).set( - ld.loan_account, loan.loan_account - ).where( - ld.docstatus < 2 - ).run() + ).set(ld.loan_account, loan.loan_account).where(ld.docstatus < 2).run() - frappe.qb.update( - lr - ).inner_join( - loan - ).on( - loan.name == lr.against_loan - ).set( + frappe.qb.update(lr).inner_join(loan).on(loan.name == lr.against_loan).set( lr.payment_account, loan.payment_account - ).set( - lr.loan_account, loan.loan_account - ).set( + ).set(lr.loan_account, loan.loan_account).set( lr.penalty_income_account, loan.penalty_income_account ).where( lr.docstatus < 2 diff --git a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py index 9993063e485..3c6c5b5b75f 100644 --- a/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py +++ b/erpnext/patches/v13_0/update_actual_start_and_end_date_in_wo.py @@ -1,4 +1,3 @@ - # Copyright (c) 2019, Frappe and Contributors # License: GNU General Public License v3. See license.txt @@ -12,11 +11,9 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "work_order_item") frappe.reload_doc("manufacturing", "doctype", "job_card") - data = frappe.get_all("Work Order", - filters = { - "docstatus": 1, - "status": ("in", ["In Process", "Completed"]) - }) + data = frappe.get_all( + "Work Order", filters={"docstatus": 1, "status": ("in", ["In Process", "Completed"])} + ) for d in data: doc = frappe.get_doc("Work Order", d.name) @@ -24,18 +21,22 @@ def execute(): doc.db_set("actual_start_date", doc.actual_start_date, update_modified=False) if doc.status == "Completed": - frappe.db.set_value("Work Order", d.name, { - "actual_end_date": doc.actual_end_date, - "lead_time": doc.lead_time - }, update_modified=False) + frappe.db.set_value( + "Work Order", + d.name, + {"actual_end_date": doc.actual_end_date, "lead_time": doc.lead_time}, + update_modified=False, + ) if not doc.planned_end_date: planned_end_date = add_to_date(doc.planned_start_date, minutes=doc.lead_time) doc.db_set("planned_end_date", doc.actual_start_date, update_modified=False) - frappe.db.sql(""" UPDATE `tabJob Card` as jc, `tabWork Order` as wo + frappe.db.sql( + """ UPDATE `tabJob Card` as jc, `tabWork Order` as wo SET jc.production_item = wo.production_item, jc.item_name = wo.item_name WHERE jc.work_order = wo.name and IFNULL(jc.production_item, "") = "" - """) \ No newline at end of file + """ + ) diff --git a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py index dc973a9d451..e37f291233e 100644 --- a/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py +++ b/erpnext/patches/v13_0/update_amt_in_work_order_required_items.py @@ -2,7 +2,7 @@ import frappe def execute(): - """ Correct amount in child table of required items table.""" + """Correct amount in child table of required items table.""" frappe.reload_doc("manufacturing", "doctype", "work_order") frappe.reload_doc("manufacturing", "doctype", "work_order_item") diff --git a/erpnext/patches/v13_0/update_category_in_ltds_certificate.py b/erpnext/patches/v13_0/update_category_in_ltds_certificate.py index a5f5a23449a..5a0873e0e53 100644 --- a/erpnext/patches/v13_0/update_category_in_ltds_certificate.py +++ b/erpnext/patches/v13_0/update_category_in_ltds_certificate.py @@ -2,19 +2,15 @@ import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'lower_deduction_certificate') + frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") ldc = frappe.qb.DocType("Lower Deduction Certificate").as_("ldc") supplier = frappe.qb.DocType("Supplier") - frappe.qb.update(ldc).inner_join(supplier).on( - ldc.supplier == supplier.name - ).set( + frappe.qb.update(ldc).inner_join(supplier).on(ldc.supplier == supplier.name).set( ldc.tax_withholding_category, supplier.tax_withholding_category - ).where( - ldc.tax_withholding_category.isnull() - ).run() \ No newline at end of file + ).where(ldc.tax_withholding_category.isnull()).run() diff --git a/erpnext/patches/v13_0/update_custom_fields_for_shopify.py b/erpnext/patches/v13_0/update_custom_fields_for_shopify.py index 8c724a8cb31..53eb6e3c788 100644 --- a/erpnext/patches/v13_0/update_custom_fields_for_shopify.py +++ b/erpnext/patches/v13_0/update_custom_fields_for_shopify.py @@ -10,5 +10,5 @@ from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings impo def execute(): - if frappe.db.get_single_value('Shopify Settings', 'enable_shopify'): + if frappe.db.get_single_value("Shopify Settings", "enable_shopify"): setup_custom_fields() diff --git a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py index 90fb50fb42c..c538476edb3 100644 --- a/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py +++ b/erpnext/patches/v13_0/update_dates_in_tax_withholding_category.py @@ -5,22 +5,23 @@ import frappe def execute(): - frappe.reload_doc('accounts', 'doctype', 'Tax Withholding Rate') + frappe.reload_doc("accounts", "doctype", "Tax Withholding Rate") - if frappe.db.has_column('Tax Withholding Rate', 'fiscal_year'): - tds_category_rates = frappe.get_all('Tax Withholding Rate', fields=['name', 'fiscal_year']) + if frappe.db.has_column("Tax Withholding Rate", "fiscal_year"): + tds_category_rates = frappe.get_all("Tax Withholding Rate", fields=["name", "fiscal_year"]) fiscal_year_map = {} - fiscal_year_details = frappe.get_all('Fiscal Year', fields=['name', 'year_start_date', 'year_end_date']) + fiscal_year_details = frappe.get_all( + "Fiscal Year", fields=["name", "year_start_date", "year_end_date"] + ) for d in fiscal_year_details: fiscal_year_map.setdefault(d.name, d) for rate in tds_category_rates: - from_date = fiscal_year_map.get(rate.fiscal_year).get('year_start_date') - to_date = fiscal_year_map.get(rate.fiscal_year).get('year_end_date') + from_date = fiscal_year_map.get(rate.fiscal_year).get("year_start_date") + to_date = fiscal_year_map.get(rate.fiscal_year).get("year_end_date") - frappe.db.set_value('Tax Withholding Rate', rate.name, { - 'from_date': from_date, - 'to_date': to_date - }) \ No newline at end of file + frappe.db.set_value( + "Tax Withholding Rate", rate.name, {"from_date": from_date, "to_date": to_date} + ) diff --git a/erpnext/patches/v13_0/update_deferred_settings.py b/erpnext/patches/v13_0/update_deferred_settings.py index 1b63635b678..03fe66ffe89 100644 --- a/erpnext/patches/v13_0/update_deferred_settings.py +++ b/erpnext/patches/v13_0/update_deferred_settings.py @@ -5,8 +5,8 @@ import frappe def execute(): - accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - accounts_settings.book_deferred_entries_based_on = 'Days' + accounts_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + accounts_settings.book_deferred_entries_based_on = "Days" accounts_settings.book_deferred_entries_via_journal_entry = 0 accounts_settings.submit_journal_entries = 0 accounts_settings.save() diff --git a/erpnext/patches/v13_0/update_disbursement_account.py b/erpnext/patches/v13_0/update_disbursement_account.py index c56fa8fdc62..d6aba4720ee 100644 --- a/erpnext/patches/v13_0/update_disbursement_account.py +++ b/erpnext/patches/v13_0/update_disbursement_account.py @@ -9,14 +9,6 @@ def execute(): loan_type = frappe.qb.DocType("Loan Type") loan = frappe.qb.DocType("Loan") - frappe.qb.update( - loan_type - ).set( - loan_type.disbursement_account, loan_type.payment_account - ).run() + frappe.qb.update(loan_type).set(loan_type.disbursement_account, loan_type.payment_account).run() - frappe.qb.update( - loan - ).set( - loan.disbursement_account, loan.payment_account - ).run() \ No newline at end of file + frappe.qb.update(loan).set(loan.disbursement_account, loan.payment_account).run() diff --git a/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py b/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py index 063de1637d0..2bc17ae86bd 100644 --- a/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py +++ b/erpnext/patches/v13_0/update_expense_claim_status_for_paid_advances.py @@ -4,18 +4,21 @@ import frappe def execute(): """ Update Expense Claim status to Paid if: - - the entire required amount is already covered via linked advances - - the claim is partially paid via advances and the rest is reimbursed + - the entire required amount is already covered via linked advances + - the claim is partially paid via advances and the rest is reimbursed """ - ExpenseClaim = frappe.qb.DocType('Expense Claim') + ExpenseClaim = frappe.qb.DocType("Expense Claim") - (frappe.qb - .update(ExpenseClaim) - .set(ExpenseClaim.status, 'Paid') + ( + frappe.qb.update(ExpenseClaim) + .set(ExpenseClaim.status, "Paid") .where( - ((ExpenseClaim.grand_total == 0) | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed)) - & (ExpenseClaim.approval_status == 'Approved') + ( + (ExpenseClaim.grand_total == 0) + | (ExpenseClaim.grand_total == ExpenseClaim.total_amount_reimbursed) + ) + & (ExpenseClaim.approval_status == "Approved") & (ExpenseClaim.docstatus == 1) & (ExpenseClaim.total_sanctioned_amount > 0) ) diff --git a/erpnext/patches/v13_0/update_export_type_for_gst.py b/erpnext/patches/v13_0/update_export_type_for_gst.py index de578612f7d..62368584dc3 100644 --- a/erpnext/patches/v13_0/update_export_type_for_gst.py +++ b/erpnext/patches/v13_0/update_export_type_for_gst.py @@ -2,32 +2,39 @@ import frappe def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return # Update custom fields - fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'}) + fieldname = frappe.db.get_value("Custom Field", {"dt": "Customer", "fieldname": "export_type"}) if fieldname: - frappe.db.set_value('Custom Field', fieldname, + frappe.db.set_value( + "Custom Field", + fieldname, { - 'default': '', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' - }) + "default": "", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + }, + ) - fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'}) + fieldname = frappe.db.get_value("Custom Field", {"dt": "Supplier", "fieldname": "export_type"}) if fieldname: - frappe.db.set_value('Custom Field', fieldname, - { - 'default': '', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' - }) + frappe.db.set_value( + "Custom Field", + fieldname, + {"default": "", "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)'}, + ) # Update Customer/Supplier Masters - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export') - """) + """ + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas') - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py index 12f9006b76e..73baecf0dfa 100644 --- a/erpnext/patches/v13_0/update_job_card_details.py +++ b/erpnext/patches/v13_0/update_job_card_details.py @@ -10,8 +10,10 @@ def execute(): frappe.reload_doc("manufacturing", "doctype", "job_card_item") frappe.reload_doc("manufacturing", "doctype", "work_order_operation") - frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo + frappe.db.sql( + """ update `tabJob Card` jc, `tabWork Order Operation` wo SET jc.hour_rate = wo.hour_rate WHERE jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0 - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_job_card_status.py b/erpnext/patches/v13_0/update_job_card_status.py index 797a3e2ae35..f2d12da119a 100644 --- a/erpnext/patches/v13_0/update_job_card_status.py +++ b/erpnext/patches/v13_0/update_job_card_status.py @@ -7,8 +7,8 @@ import frappe def execute(): job_card = frappe.qb.DocType("Job Card") - (frappe.qb - .update(job_card) + ( + frappe.qb.update(job_card) .set(job_card.status, "Completed") .where( (job_card.docstatus == 1) diff --git a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py index 43096991943..b631c0bab73 100644 --- a/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py +++ b/erpnext/patches/v13_0/update_maintenance_schedule_field_in_visit.py @@ -1,4 +1,3 @@ - import frappe @@ -7,18 +6,14 @@ def execute(): # Updates the Maintenance Schedule link to fetch serial nos from frappe.query_builder.functions import Coalesce - mvp = frappe.qb.DocType('Maintenance Visit Purpose') - mv = frappe.qb.DocType('Maintenance Visit') - frappe.qb.update( - mv - ).join( - mvp - ).on(mvp.parent == mv.name).set( - mv.maintenance_schedule, - Coalesce(mvp.prevdoc_docname, '') + mvp = frappe.qb.DocType("Maintenance Visit Purpose") + mv = frappe.qb.DocType("Maintenance Visit") + + frappe.qb.update(mv).join(mvp).on(mvp.parent == mv.name).set( + mv.maintenance_schedule, Coalesce(mvp.prevdoc_docname, "") ).where( - (mv.maintenance_type == "Scheduled") - & (mvp.prevdoc_docname.notnull()) - & (mv.docstatus < 2) - ).run(as_dict=1) + (mv.maintenance_type == "Scheduled") & (mvp.prevdoc_docname.notnull()) & (mv.docstatus < 2) + ).run( + as_dict=1 + ) diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index bcd80d4c5c4..a1d40b739eb 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.model.naming import make_autoname @@ -17,69 +16,110 @@ def execute(): # Create a penalty account for loan types - frappe.reload_doc('loan_management', 'doctype', 'loan_type') - frappe.reload_doc('loan_management', 'doctype', 'loan') - frappe.reload_doc('loan_management', 'doctype', 'repayment_schedule') - frappe.reload_doc('loan_management', 'doctype', 'process_loan_interest_accrual') - frappe.reload_doc('loan_management', 'doctype', 'loan_repayment') - frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail') - frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual') - frappe.reload_doc('accounts', 'doctype', 'gl_entry') - frappe.reload_doc('accounts', 'doctype', 'journal_entry_account') + frappe.reload_doc("loan_management", "doctype", "loan_type") + frappe.reload_doc("loan_management", "doctype", "loan") + frappe.reload_doc("loan_management", "doctype", "repayment_schedule") + frappe.reload_doc("loan_management", "doctype", "process_loan_interest_accrual") + frappe.reload_doc("loan_management", "doctype", "loan_repayment") + frappe.reload_doc("loan_management", "doctype", "loan_repayment_detail") + frappe.reload_doc("loan_management", "doctype", "loan_interest_accrual") + frappe.reload_doc("accounts", "doctype", "gl_entry") + frappe.reload_doc("accounts", "doctype", "journal_entry_account") updated_loan_types = [] loans_to_close = [] # Update old loan status as closed - if frappe.db.has_column('Repayment Schedule', 'paid'): - loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule` - where paid = 0 and docstatus = 1""", as_dict=1) + if frappe.db.has_column("Repayment Schedule", "paid"): + loans_list = frappe.db.sql( + """SELECT distinct parent from `tabRepayment Schedule` + where paid = 0 and docstatus = 1""", + as_dict=1, + ) loans_to_close = [d.parent for d in loans_list] if loans_to_close: - frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close)) + frappe.db.sql( + "UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" + % (", ".join(["%s"] * len(loans_to_close))), + tuple(loans_to_close), + ) - loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment', - 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'], - filters={'docstatus': 1, 'status': ('!=', 'Closed')}) + loans = frappe.get_all( + "Loan", + fields=[ + "name", + "loan_type", + "company", + "status", + "mode_of_payment", + "applicant_type", + "applicant", + "loan_account", + "payment_account", + "interest_income_account", + ], + filters={"docstatus": 1, "status": ("!=", "Closed")}, + ) for loan in loans: # Update details in Loan Types and Loan - loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company') + loan_type_company = frappe.db.get_value("Loan Type", loan.loan_type, "company") loan_type = loan.loan_type - group_income_account = frappe.get_value('Account', {'company': loan.company, - 'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')}) + group_income_account = frappe.get_value( + "Account", + { + "company": loan.company, + "is_group": 1, + "root_type": "Income", + "account_name": _("Indirect Income"), + }, + ) if not group_income_account: - group_income_account = frappe.get_value('Account', {'company': loan.company, - 'is_group': 1, 'root_type': 'Income'}) + group_income_account = frappe.get_value( + "Account", {"company": loan.company, "is_group": 1, "root_type": "Income"} + ) - penalty_account = create_account(company=loan.company, account_type='Income Account', - account_name='Penalty Account', parent_account=group_income_account) + penalty_account = create_account( + company=loan.company, + account_type="Income Account", + account_name="Penalty Account", + parent_account=group_income_account, + ) # Same loan type used for multiple companies if loan_type_company and loan_type_company != loan.company: # get loan type for appropriate company - loan_type_name = frappe.get_value('Loan Type', {'company': loan.company, - 'mode_of_payment': loan.mode_of_payment, 'loan_account': loan.loan_account, - 'payment_account': loan.payment_account, 'interest_income_account': loan.interest_income_account, - 'penalty_income_account': loan.penalty_income_account}, 'name') + loan_type_name = frappe.get_value( + "Loan Type", + { + "company": loan.company, + "mode_of_payment": loan.mode_of_payment, + "loan_account": loan.loan_account, + "payment_account": loan.payment_account, + "interest_income_account": loan.interest_income_account, + "penalty_income_account": loan.penalty_income_account, + }, + "name", + ) if not loan_type_name: loan_type_name = create_loan_type(loan, loan_type_name, penalty_account) # update loan type in loan - frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, - loan.name)) + frappe.db.sql( + "UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name, loan.name) + ) loan_type = loan_type_name if loan_type_name not in updated_loan_types: updated_loan_types.append(loan_type_name) elif not loan_type_company: - loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type) + loan_type_doc = frappe.get_doc("Loan Type", loan.loan_type) loan_type_doc.is_term_loan = 1 loan_type_doc.company = loan.company loan_type_doc.mode_of_payment = loan.mode_of_payment @@ -92,26 +132,29 @@ def execute(): loan_type = loan.loan_type if loan_type in updated_loan_types: - if loan.status == 'Fully Disbursed': - status = 'Disbursed' - elif loan.status == 'Repaid/Closed': - status = 'Closed' + if loan.status == "Fully Disbursed": + status = "Disbursed" + elif loan.status == "Repaid/Closed": + status = "Closed" else: status = loan.status - frappe.db.set_value('Loan', loan.name, { - 'is_term_loan': 1, - 'penalty_income_account': penalty_account, - 'status': status - }) + frappe.db.set_value( + "Loan", + loan.name, + {"is_term_loan": 1, "penalty_income_account": penalty_account, "status": status}, + ) - process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type, - loan=loan.name) + process_loan_interest_accrual_for_term_loans( + posting_date=nowdate(), loan_type=loan_type, loan=loan.name + ) - - if frappe.db.has_column('Repayment Schedule', 'paid'): - total_principal, total_interest = frappe.db.get_value('Repayment Schedule', {'paid': 1, 'parent': loan.name}, - ['sum(principal_amount) as total_principal', 'sum(interest_amount) as total_interest']) + if frappe.db.has_column("Repayment Schedule", "paid"): + total_principal, total_interest = frappe.db.get_value( + "Repayment Schedule", + {"paid": 1, "parent": loan.name}, + ["sum(principal_amount) as total_principal", "sum(interest_amount) as total_interest"], + ) accrued_entries = get_accrued_interest_entries(loan.name) for entry in accrued_entries: @@ -128,17 +171,20 @@ def execute(): else: principal_paid = flt(total_principal) - frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` + frappe.db.sql( + """ UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` + %s, paid_interest_amount = `paid_interest_amount` + %s WHERE name = %s""", - (principal_paid, interest_paid, entry.name)) + (principal_paid, interest_paid, entry.name), + ) total_principal = flt(total_principal) - principal_paid total_interest = flt(total_interest) - interest_paid + def create_loan_type(loan, loan_type_name, penalty_account): - loan_type_doc = frappe.new_doc('Loan Type') + loan_type_doc = frappe.new_doc("Loan Type") loan_type_doc.loan_name = make_autoname("Loan Type-.####") loan_type_doc.is_term_loan = 1 loan_type_doc.company = loan.company diff --git a/erpnext/patches/v13_0/update_payment_terms_outstanding.py b/erpnext/patches/v13_0/update_payment_terms_outstanding.py index aea09ad7a37..d0c25f37ad7 100644 --- a/erpnext/patches/v13_0/update_payment_terms_outstanding.py +++ b/erpnext/patches/v13_0/update_payment_terms_outstanding.py @@ -7,10 +7,12 @@ import frappe def execute(): frappe.reload_doc("accounts", "doctype", "Payment Schedule") - if frappe.db.count('Payment Schedule'): - frappe.db.sql(''' + if frappe.db.count("Payment Schedule"): + frappe.db.sql( + """ UPDATE `tabPayment Schedule` ps SET ps.outstanding = (ps.payment_amount - ps.paid_amount) - ''') + """ + ) diff --git a/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py index b2e35591970..49826dfd261 100644 --- a/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py +++ b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py @@ -8,8 +8,9 @@ import frappe def execute(): frappe.reload_doc("accounts", "doctype", "POS Invoice Merge Log") frappe.reload_doc("accounts", "doctype", "POS Closing Entry") - if frappe.db.count('POS Invoice Merge Log'): - frappe.db.sql(''' + if frappe.db.count("POS Invoice Merge Log"): + frappe.db.sql( + """ UPDATE `tabPOS Invoice Merge Log` log, `tabPOS Invoice Reference` log_ref SET @@ -20,7 +21,8 @@ def execute(): ) WHERE log_ref.parent = log.name - ''') + """ + ) - frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1''') - frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2''') + frappe.db.sql("""UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1""") + frappe.db.sql("""UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2""") diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py index 29debc6ad14..c9a23222424 100644 --- a/erpnext/patches/v13_0/update_project_template_tasks.py +++ b/erpnext/patches/v13_0/update_project_template_tasks.py @@ -11,38 +11,39 @@ def execute(): frappe.reload_doc("projects", "doctype", "task") # Update property setter status if any - property_setter = frappe.db.get_value('Property Setter', {'doc_type': 'Task', - 'field_name': 'status', 'property': 'options'}) + property_setter = frappe.db.get_value( + "Property Setter", {"doc_type": "Task", "field_name": "status", "property": "options"} + ) if property_setter: - property_setter_doc = frappe.get_doc('Property Setter', {'doc_type': 'Task', - 'field_name': 'status', 'property': 'options'}) + property_setter_doc = frappe.get_doc( + "Property Setter", {"doc_type": "Task", "field_name": "status", "property": "options"} + ) property_setter_doc.value += "\nTemplate" property_setter_doc.save() - for template_name in frappe.get_all('Project Template'): + for template_name in frappe.get_all("Project Template"): template = frappe.get_doc("Project Template", template_name.name) replace_tasks = False new_tasks = [] for task in template.tasks: if task.subject: replace_tasks = True - new_task = frappe.get_doc(dict( - doctype = "Task", - subject = task.subject, - start = task.start, - duration = task.duration, - task_weight = task.task_weight, - description = task.description, - is_template = 1 - )).insert() + new_task = frappe.get_doc( + dict( + doctype="Task", + subject=task.subject, + start=task.start, + duration=task.duration, + task_weight=task.task_weight, + description=task.description, + is_template=1, + ) + ).insert() new_tasks.append(new_task) if replace_tasks: template.tasks = [] for tsk in new_tasks: - template.append("tasks", { - "task": tsk.name, - "subject": tsk.subject - }) + template.append("tasks", {"task": tsk.name, "subject": tsk.subject}) template.save() diff --git a/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py b/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py index f9bfc54502f..31aa29274d1 100644 --- a/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py +++ b/erpnext/patches/v13_0/update_reason_for_resignation_in_employee.py @@ -6,10 +6,12 @@ import frappe def execute(): - frappe.reload_doc("hr", "doctype", "employee") + frappe.reload_doc("hr", "doctype", "employee") - if frappe.db.has_column("Employee", "reason_for_resignation"): - frappe.db.sql(""" UPDATE `tabEmployee` + if frappe.db.has_column("Employee", "reason_for_resignation"): + frappe.db.sql( + """ UPDATE `tabEmployee` SET reason_for_leaving = reason_for_resignation WHERE status = 'Left' and reason_for_leaving is null and reason_for_resignation is not null - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_recipient_email_digest.py b/erpnext/patches/v13_0/update_recipient_email_digest.py index e3528cc06bb..69961a87f78 100644 --- a/erpnext/patches/v13_0/update_recipient_email_digest.py +++ b/erpnext/patches/v13_0/update_recipient_email_digest.py @@ -8,16 +8,18 @@ import frappe def execute(): frappe.reload_doc("setup", "doctype", "Email Digest") frappe.reload_doc("setup", "doctype", "Email Digest Recipient") - email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list']) + email_digests = frappe.db.get_list("Email Digest", fields=["name", "recipient_list"]) for email_digest in email_digests: if email_digest.recipient_list: for recipient in email_digest.recipient_list.split("\n"): - if frappe.db.exists('User', recipient): - doc = frappe.get_doc({ - 'doctype': 'Email Digest Recipient', - 'parenttype': 'Email Digest', - 'parentfield': 'recipients', - 'parent': email_digest.name, - 'recipient': recipient - }) + if frappe.db.exists("User", recipient): + doc = frappe.get_doc( + { + "doctype": "Email Digest Recipient", + "parenttype": "Email Digest", + "parentfield": "recipients", + "parent": email_digest.name, + "recipient": recipient, + } + ) doc.insert() diff --git a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py index 00926b09241..72e77fe2161 100644 --- a/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py +++ b/erpnext/patches/v13_0/update_reserved_qty_closed_wo.py @@ -9,15 +9,12 @@ def execute(): wo_item = frappe.qb.DocType("Work Order Item") incorrect_item_wh = ( - frappe.qb - .from_(wo) - .join(wo_item).on(wo.name == wo_item.parent) - .select(wo_item.item_code, wo.source_warehouse).distinct() - .where( - (wo.status == "Closed") - & (wo.docstatus == 1) - & (wo.source_warehouse.notnull()) - ) + frappe.qb.from_(wo) + .join(wo_item) + .on(wo.name == wo_item.parent) + .select(wo_item.item_code, wo.source_warehouse) + .distinct() + .where((wo.status == "Closed") & (wo.docstatus == 1) & (wo.source_warehouse.notnull())) ).run() for item_code, warehouse in incorrect_item_wh: diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py index dd64e05ec16..9b5845f494d 100644 --- a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py +++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py @@ -6,14 +6,16 @@ from erpnext.controllers.status_updater import OverAllowanceError def execute(): - frappe.reload_doc('stock', 'doctype', 'purchase_receipt') - frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item') - frappe.reload_doc('stock', 'doctype', 'delivery_note') - frappe.reload_doc('stock', 'doctype', 'delivery_note_item') - frappe.reload_doc('stock', 'doctype', 'stock_settings') + frappe.reload_doc("stock", "doctype", "purchase_receipt") + frappe.reload_doc("stock", "doctype", "purchase_receipt_item") + frappe.reload_doc("stock", "doctype", "delivery_note") + frappe.reload_doc("stock", "doctype", "delivery_note_item") + frappe.reload_doc("stock", "doctype", "stock_settings") def update_from_return_docs(doctype): - for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1, 'return_against': ('!=', '')}): + for return_doc in frappe.get_all( + doctype, filters={"is_return": 1, "docstatus": 1, "return_against": ("!=", "")} + ): # Update original receipt/delivery document from return return_doc = frappe.get_cached_doc(doctype, return_doc.name) try: @@ -27,9 +29,11 @@ def execute(): frappe.db.commit() # Set received qty in stock uom in PR, as returned qty is checked against it - frappe.db.sql(""" update `tabPurchase Receipt Item` + frappe.db.sql( + """ update `tabPurchase Receipt Item` set received_stock_qty = received_qty * conversion_factor - where docstatus = 1 """) + where docstatus = 1 """ + ) - for doctype in ('Purchase Receipt', 'Delivery Note'): + for doctype in ("Purchase Receipt", "Delivery Note"): update_from_return_docs(doctype) diff --git a/erpnext/patches/v13_0/update_sane_transfer_against.py b/erpnext/patches/v13_0/update_sane_transfer_against.py index a163d385843..45691e2ded7 100644 --- a/erpnext/patches/v13_0/update_sane_transfer_against.py +++ b/erpnext/patches/v13_0/update_sane_transfer_against.py @@ -4,8 +4,8 @@ import frappe def execute(): bom = frappe.qb.DocType("BOM") - (frappe.qb - .update(bom) + ( + frappe.qb.update(bom) .set(bom.transfer_material_against, "Work Order") .where(bom.with_operations == 0) ).run() diff --git a/erpnext/patches/v13_0/update_shipment_status.py b/erpnext/patches/v13_0/update_shipment_status.py index f2d7d1d1e3f..d21caf70add 100644 --- a/erpnext/patches/v13_0/update_shipment_status.py +++ b/erpnext/patches/v13_0/update_shipment_status.py @@ -5,11 +5,15 @@ def execute(): frappe.reload_doc("stock", "doctype", "shipment") # update submitted status - frappe.db.sql("""UPDATE `tabShipment` + frappe.db.sql( + """UPDATE `tabShipment` SET status = "Submitted" - WHERE status = "Draft" AND docstatus = 1""") + WHERE status = "Draft" AND docstatus = 1""" + ) # update cancelled status - frappe.db.sql("""UPDATE `tabShipment` + frappe.db.sql( + """UPDATE `tabShipment` SET status = "Cancelled" - WHERE status = "Draft" AND docstatus = 2""") + WHERE status = "Draft" AND docstatus = 2""" + ) diff --git a/erpnext/patches/v13_0/update_sla_enhancements.py b/erpnext/patches/v13_0/update_sla_enhancements.py index 7f61020309d..84c683acd2c 100644 --- a/erpnext/patches/v13_0/update_sla_enhancements.py +++ b/erpnext/patches/v13_0/update_sla_enhancements.py @@ -8,78 +8,94 @@ import frappe def execute(): # add holiday list and employee group fields in SLA # change response and resolution time in priorities child table - if frappe.db.exists('DocType', 'Service Level Agreement'): - sla_details = frappe.db.get_all('Service Level Agreement', fields=['name', 'service_level']) - priorities = frappe.db.get_all('Service Level Priority', fields=['*'], filters={ - 'parenttype': ('in', ['Service Level Agreement', 'Service Level']) - }) + if frappe.db.exists("DocType", "Service Level Agreement"): + sla_details = frappe.db.get_all("Service Level Agreement", fields=["name", "service_level"]) + priorities = frappe.db.get_all( + "Service Level Priority", + fields=["*"], + filters={"parenttype": ("in", ["Service Level Agreement", "Service Level"])}, + ) - frappe.reload_doc('support', 'doctype', 'service_level_agreement') - frappe.reload_doc('support', 'doctype', 'pause_sla_on_status') - frappe.reload_doc('support', 'doctype', 'service_level_priority') - frappe.reload_doc('support', 'doctype', 'service_day') + frappe.reload_doc("support", "doctype", "service_level_agreement") + frappe.reload_doc("support", "doctype", "pause_sla_on_status") + frappe.reload_doc("support", "doctype", "service_level_priority") + frappe.reload_doc("support", "doctype", "service_day") for entry in sla_details: - values = frappe.db.get_value('Service Level', entry.service_level, ['holiday_list', 'employee_group']) + values = frappe.db.get_value( + "Service Level", entry.service_level, ["holiday_list", "employee_group"] + ) if values: holiday_list = values[0] employee_group = values[1] - frappe.db.set_value('Service Level Agreement', entry.name, { - 'holiday_list': holiday_list, - 'employee_group': employee_group - }) + frappe.db.set_value( + "Service Level Agreement", + entry.name, + {"holiday_list": holiday_list, "employee_group": employee_group}, + ) priority_dict = {} for priority in priorities: - if priority.parenttype == 'Service Level Agreement': + if priority.parenttype == "Service Level Agreement": response_time = convert_to_seconds(priority.response_time, priority.response_time_period) resolution_time = convert_to_seconds(priority.resolution_time, priority.resolution_time_period) - frappe.db.set_value('Service Level Priority', priority.name, { - 'response_time': response_time, - 'resolution_time': resolution_time - }) - if priority.parenttype == 'Service Level': + frappe.db.set_value( + "Service Level Priority", + priority.name, + {"response_time": response_time, "resolution_time": resolution_time}, + ) + if priority.parenttype == "Service Level": if not priority.parent in priority_dict: priority_dict[priority.parent] = [] priority_dict[priority.parent].append(priority) - # copy Service Levels to Service Level Agreements sl = [entry.service_level for entry in sla_details] - if frappe.db.exists('DocType', 'Service Level'): - service_levels = frappe.db.get_all('Service Level', filters={'service_level': ('not in', sl)}, fields=['*']) + if frappe.db.exists("DocType", "Service Level"): + service_levels = frappe.db.get_all( + "Service Level", filters={"service_level": ("not in", sl)}, fields=["*"] + ) for entry in service_levels: - sla = frappe.new_doc('Service Level Agreement') + sla = frappe.new_doc("Service Level Agreement") sla.service_level = entry.service_level sla.holiday_list = entry.holiday_list sla.employee_group = entry.employee_group sla.flags.ignore_validate = True sla = sla.insert(ignore_mandatory=True) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabService Day` SET parent = %(new_parent)s , parentfield = 'support_and_resolution', parenttype = 'Service Level Agreement' WHERE parent = %(old_parent)s - """, {'new_parent': sla.name, 'old_parent': entry.name}, as_dict = 1) + """, + {"new_parent": sla.name, "old_parent": entry.name}, + as_dict=1, + ) priority_list = priority_dict.get(entry.name) if priority_list: - sla = frappe.get_doc('Service Level Agreement', sla.name) + sla = frappe.get_doc("Service Level Agreement", sla.name) for priority in priority_list: - row = sla.append('priorities', { - 'priority': priority.priority, - 'default_priority': priority.default_priority, - 'response_time': convert_to_seconds(priority.response_time, priority.response_time_period), - 'resolution_time': convert_to_seconds(priority.resolution_time, priority.resolution_time_period) - }) + row = sla.append( + "priorities", + { + "priority": priority.priority, + "default_priority": priority.default_priority, + "response_time": convert_to_seconds(priority.response_time, priority.response_time_period), + "resolution_time": convert_to_seconds( + priority.resolution_time, priority.resolution_time_period + ), + }, + ) row.db_update() sla.db_update() - frappe.delete_doc_if_exists('DocType', 'Service Level') + frappe.delete_doc_if_exists("DocType", "Service Level") def convert_to_seconds(value, unit): diff --git a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py index 665cc39923a..6d26ac543fc 100644 --- a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py +++ b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py @@ -6,8 +6,10 @@ import frappe def execute(): - frappe.reload_doc('hr', 'doctype', 'shift_assignment') - if frappe.db.has_column('Shift Assignment', 'date'): - frappe.db.sql("""update `tabShift Assignment` + frappe.reload_doc("hr", "doctype", "shift_assignment") + if frappe.db.has_column("Shift Assignment", "date"): + frappe.db.sql( + """update `tabShift Assignment` set end_date=date, start_date=date - where date IS NOT NULL and start_date IS NULL and end_date IS NULL;""") + where date IS NOT NULL and start_date IS NULL and end_date IS NULL;""" + ) diff --git a/erpnext/patches/v13_0/update_subscription.py b/erpnext/patches/v13_0/update_subscription.py index b0bb1c86eea..95783849ab6 100644 --- a/erpnext/patches/v13_0/update_subscription.py +++ b/erpnext/patches/v13_0/update_subscription.py @@ -8,12 +8,13 @@ from six import iteritems def execute(): - frappe.reload_doc('accounts', 'doctype', 'subscription') - frappe.reload_doc('accounts', 'doctype', 'subscription_invoice') - frappe.reload_doc('accounts', 'doctype', 'subscription_plan') + frappe.reload_doc("accounts", "doctype", "subscription") + frappe.reload_doc("accounts", "doctype", "subscription_invoice") + frappe.reload_doc("accounts", "doctype", "subscription_plan") - if frappe.db.has_column('Subscription', 'customer'): - frappe.db.sql(""" + if frappe.db.has_column("Subscription", "customer"): + frappe.db.sql( + """ UPDATE `tabSubscription` SET start_date = start, @@ -21,22 +22,28 @@ def execute(): party = customer, sales_tax_template = tax_template WHERE IFNULL(party,'') = '' - """) + """ + ) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSubscription Invoice` SET document_type = 'Sales Invoice' WHERE IFNULL(document_type, '') = '' - """) + """ + ) price_determination_map = { - 'Fixed rate': 'Fixed Rate', - 'Based on price list': 'Based On Price List' + "Fixed rate": "Fixed Rate", + "Based on price list": "Based On Price List", } for key, value in iteritems(price_determination_map): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabSubscription Plan` SET price_determination = %s WHERE price_determination = %s - """, (value, key)) + """, + (value, key), + ) diff --git a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py index e21fe578212..d7c849956e9 100644 --- a/erpnext/patches/v13_0/update_subscription_status_in_memberships.py +++ b/erpnext/patches/v13_0/update_subscription_status_in_memberships.py @@ -2,9 +2,11 @@ import frappe def execute(): - if frappe.db.exists('DocType', 'Member'): - frappe.reload_doc('Non Profit', 'doctype', 'Member') + if frappe.db.exists("DocType", "Member"): + frappe.reload_doc("Non Profit", "doctype", "Member") - if frappe.db.has_column('Member', 'subscription_activated'): - frappe.db.sql('UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1') - frappe.db.sql_ddl('ALTER table `tabMember` DROP COLUMN subscription_activated') + if frappe.db.has_column("Member", "subscription_activated"): + frappe.db.sql( + 'UPDATE `tabMember` SET subscription_status = "Active" WHERE subscription_activated = 1' + ) + frappe.db.sql_ddl("ALTER table `tabMember` DROP COLUMN subscription_activated") diff --git a/erpnext/patches/v13_0/update_tax_category_for_rcm.py b/erpnext/patches/v13_0/update_tax_category_for_rcm.py index 7af2366bf0a..8ac95348894 100644 --- a/erpnext/patches/v13_0/update_tax_category_for_rcm.py +++ b/erpnext/patches/v13_0/update_tax_category_for_rcm.py @@ -5,27 +5,46 @@ from erpnext.regional.india import states def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - create_custom_fields({ - 'Tax Category': [ - dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1), - dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check', - insert_after='is_inter_state', print_hide=1), - dict(fieldname='tax_category_column_break', fieldtype='Column Break', - insert_after='is_reverse_charge'), - dict(fieldname='gst_state', label='Source State', fieldtype='Select', - options='\n'.join(states), insert_after='company') - ] - }, update=True) + create_custom_fields( + { + "Tax Category": [ + dict( + fieldname="is_inter_state", + label="Is Inter State", + fieldtype="Check", + insert_after="disabled", + print_hide=1, + ), + dict( + fieldname="is_reverse_charge", + label="Is Reverse Charge", + fieldtype="Check", + insert_after="is_inter_state", + print_hide=1, + ), + dict( + fieldname="tax_category_column_break", + fieldtype="Column Break", + insert_after="is_reverse_charge", + ), + dict( + fieldname="gst_state", + label="Source State", + fieldtype="Select", + options="\n".join(states), + insert_after="company", + ), + ] + }, + update=True, + ) tax_category = frappe.qb.DocType("Tax Category") - frappe.qb.update(tax_category).set( - tax_category.is_reverse_charge, 1 - ).where( - tax_category.name.isin(['Reverse Charge Out-State', 'Reverse Charge In-State']) - ).run() \ No newline at end of file + frappe.qb.update(tax_category).set(tax_category.is_reverse_charge, 1).where( + tax_category.name.isin(["Reverse Charge Out-State", "Reverse Charge In-State"]) + ).run() diff --git a/erpnext/patches/v13_0/update_tds_check_field.py b/erpnext/patches/v13_0/update_tds_check_field.py index 436d2e6a6da..0505266b3c4 100644 --- a/erpnext/patches/v13_0/update_tds_check_field.py +++ b/erpnext/patches/v13_0/update_tds_check_field.py @@ -2,9 +2,12 @@ import frappe def execute(): - if frappe.db.has_table("Tax Withholding Category") \ - and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"): - frappe.db.sql(""" + if frappe.db.has_table("Tax Withholding Category") and frappe.db.has_column( + "Tax Withholding Category", "round_off_tax_amount" + ): + frappe.db.sql( + """ UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0 WHERE round_off_tax_amount IS NULL - """) + """ + ) diff --git a/erpnext/patches/v13_0/update_timesheet_changes.py b/erpnext/patches/v13_0/update_timesheet_changes.py index cc38a0c9a16..02654c11d30 100644 --- a/erpnext/patches/v13_0/update_timesheet_changes.py +++ b/erpnext/patches/v13_0/update_timesheet_changes.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.utils.rename_field import rename_field @@ -10,17 +9,23 @@ def execute(): if frappe.db.has_column("Timesheet Detail", "billable"): rename_field("Timesheet Detail", "billable", "is_billable") - base_currency = frappe.defaults.get_global_default('currency') + base_currency = frappe.defaults.get_global_default("currency") - frappe.db.sql("""UPDATE `tabTimesheet Detail` + frappe.db.sql( + """UPDATE `tabTimesheet Detail` SET base_billing_rate = billing_rate, base_billing_amount = billing_amount, base_costing_rate = costing_rate, - base_costing_amount = costing_amount""") + base_costing_amount = costing_amount""" + ) - frappe.db.sql("""UPDATE `tabTimesheet` + frappe.db.sql( + """UPDATE `tabTimesheet` SET currency = '{0}', exchange_rate = 1.0, base_total_billable_amount = total_billable_amount, base_total_billed_amount = total_billed_amount, - base_total_costing_amount = total_costing_amount""".format(base_currency)) + base_total_costing_amount = total_costing_amount""".format( + base_currency + ) + ) diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py index 902707b4b66..326fc579f4c 100644 --- a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py +++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py @@ -2,10 +2,10 @@ import frappe def execute(): - frappe.reload_doc('custom', 'doctype', 'custom_field', force=True) - company = frappe.get_all('Company', filters = {'country': 'India'}) + frappe.reload_doc("custom", "doctype", "custom_field", force=True) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }): - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '') + if frappe.db.exists("Custom Field", {"fieldname": "vehicle_no"}): + frappe.db.set_value("Custom Field", {"fieldname": "vehicle_no"}, "mandatory_depends_on", "") diff --git a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py index c760a6a52f1..b395c01c1df 100644 --- a/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py +++ b/erpnext/patches/v13_0/updates_for_multi_currency_payroll.py @@ -8,122 +8,116 @@ from frappe.model.utils.rename_field import rename_field def execute(): - frappe.reload_doc('Accounts', 'doctype', 'Salary Component Account') - if frappe.db.has_column('Salary Component Account', 'default_account'): + frappe.reload_doc("Accounts", "doctype", "Salary Component Account") + if frappe.db.has_column("Salary Component Account", "default_account"): rename_field("Salary Component Account", "default_account", "account") doctype_list = [ - { - 'module':'HR', - 'doctype':'Employee Advance' - }, - { - 'module':'HR', - 'doctype':'Leave Encashment' - }, - { - 'module':'Payroll', - 'doctype':'Additional Salary' - }, - { - 'module':'Payroll', - 'doctype':'Employee Benefit Application' - }, - { - 'module':'Payroll', - 'doctype':'Employee Benefit Claim' - }, - { - 'module':'Payroll', - 'doctype':'Employee Incentive' - }, - { - 'module':'Payroll', - 'doctype':'Employee Tax Exemption Declaration' - }, - { - 'module':'Payroll', - 'doctype':'Employee Tax Exemption Proof Submission' - }, - { - 'module':'Payroll', - 'doctype':'Income Tax Slab' - }, - { - 'module':'Payroll', - 'doctype':'Payroll Entry' - }, - { - 'module':'Payroll', - 'doctype':'Retention Bonus' - }, - { - 'module':'Payroll', - 'doctype':'Salary Structure' - }, - { - 'module':'Payroll', - 'doctype':'Salary Structure Assignment' - }, - { - 'module':'Payroll', - 'doctype':'Salary Slip' - }, + {"module": "HR", "doctype": "Employee Advance"}, + {"module": "HR", "doctype": "Leave Encashment"}, + {"module": "Payroll", "doctype": "Additional Salary"}, + {"module": "Payroll", "doctype": "Employee Benefit Application"}, + {"module": "Payroll", "doctype": "Employee Benefit Claim"}, + {"module": "Payroll", "doctype": "Employee Incentive"}, + {"module": "Payroll", "doctype": "Employee Tax Exemption Declaration"}, + {"module": "Payroll", "doctype": "Employee Tax Exemption Proof Submission"}, + {"module": "Payroll", "doctype": "Income Tax Slab"}, + {"module": "Payroll", "doctype": "Payroll Entry"}, + {"module": "Payroll", "doctype": "Retention Bonus"}, + {"module": "Payroll", "doctype": "Salary Structure"}, + {"module": "Payroll", "doctype": "Salary Structure Assignment"}, + {"module": "Payroll", "doctype": "Salary Slip"}, ] for item in doctype_list: - frappe.reload_doc(item['module'], 'doctype', item['doctype']) + frappe.reload_doc(item["module"], "doctype", item["doctype"]) # update company in employee advance based on employee company - for dt in ['Employee Incentive', 'Leave Encashment', 'Employee Benefit Application', 'Employee Benefit Claim']: - frappe.db.sql(""" + for dt in [ + "Employee Incentive", + "Leave Encashment", + "Employee Benefit Application", + "Employee Benefit Claim", + ]: + frappe.db.sql( + """ update `tab{doctype}` set company = (select company from tabEmployee where name=`tab{doctype}`.employee) - """.format(doctype=dt)) + """.format( + doctype=dt + ) + ) # update exchange rate for employee advance frappe.db.sql("update `tabEmployee Advance` set exchange_rate=1") # get all companies and it's currency - all_companies = frappe.db.get_all("Company", fields=["name", "default_currency", "default_payroll_payable_account"]) + all_companies = frappe.db.get_all( + "Company", fields=["name", "default_currency", "default_payroll_payable_account"] + ) for d in all_companies: company = d.name company_currency = d.default_currency default_payroll_payable_account = d.default_payroll_payable_account if not default_payroll_payable_account: - default_payroll_payable_account = frappe.db.get_value("Account", - {"account_name": _("Payroll Payable"), "company": company, "account_currency": company_currency, "is_group": 0}) + default_payroll_payable_account = frappe.db.get_value( + "Account", + { + "account_name": _("Payroll Payable"), + "company": company, + "account_currency": company_currency, + "is_group": 0, + }, + ) # update currency in following doctypes based on company currency - doctypes_for_currency = ['Employee Advance', 'Leave Encashment', 'Employee Benefit Application', - 'Employee Benefit Claim', 'Employee Incentive', 'Additional Salary', - 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission', - 'Income Tax Slab', 'Retention Bonus', 'Salary Structure'] + doctypes_for_currency = [ + "Employee Advance", + "Leave Encashment", + "Employee Benefit Application", + "Employee Benefit Claim", + "Employee Incentive", + "Additional Salary", + "Employee Tax Exemption Declaration", + "Employee Tax Exemption Proof Submission", + "Income Tax Slab", + "Retention Bonus", + "Salary Structure", + ] for dt in doctypes_for_currency: - frappe.db.sql("""update `tab{doctype}` set currency = %s where company=%s""" - .format(doctype=dt), (company_currency, company)) + frappe.db.sql( + """update `tab{doctype}` set currency = %s where company=%s""".format(doctype=dt), + (company_currency, company), + ) # update fields in payroll entry - frappe.db.sql(""" + frappe.db.sql( + """ update `tabPayroll Entry` set currency = %s, exchange_rate = 1, payroll_payable_account=%s where company=%s - """, (company_currency, default_payroll_payable_account, company)) + """, + (company_currency, default_payroll_payable_account, company), + ) # update fields in Salary Structure Assignment - frappe.db.sql(""" + frappe.db.sql( + """ update `tabSalary Structure Assignment` set currency = %s, payroll_payable_account=%s where company=%s - """, (company_currency, default_payroll_payable_account, company)) + """, + (company_currency, default_payroll_payable_account, company), + ) # update fields in Salary Slip - frappe.db.sql(""" + frappe.db.sql( + """ update `tabSalary Slip` set currency = %s, exchange_rate = 1, @@ -134,4 +128,6 @@ def execute(): base_rounded_total = rounded_total, base_total_in_words = total_in_words where company=%s - """, (company_currency, company)) + """, + (company_currency, company), + ) diff --git a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py index e43a8bad8ea..693d06dc1a0 100644 --- a/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py +++ b/erpnext/patches/v13_0/wipe_serial_no_field_for_0_qty.py @@ -11,8 +11,6 @@ def execute(): sr_item = frappe.qb.DocType(doctype) - (frappe.qb - .update(sr_item) - .set(sr_item.current_serial_no, None) - .where(sr_item.current_qty == 0) + ( + frappe.qb.update(sr_item).set(sr_item.current_serial_no, None).where(sr_item.current_qty == 0) ).run() diff --git a/erpnext/patches/v4_2/repost_reserved_qty.py b/erpnext/patches/v4_2/repost_reserved_qty.py index ed4b19d07d3..72ac524a5c8 100644 --- a/erpnext/patches/v4_2/repost_reserved_qty.py +++ b/erpnext/patches/v4_2/repost_reserved_qty.py @@ -11,7 +11,8 @@ def execute(): for doctype in ("Sales Order Item", "Bin"): frappe.reload_doctype(doctype) - repost_for = frappe.db.sql(""" + repost_for = frappe.db.sql( + """ select distinct item_code, warehouse from @@ -26,17 +27,18 @@ def execute(): ) so_item where exists(select name from tabItem where name=so_item.item_code and ifnull(is_stock_item, 0)=1) - """) + """ + ) for item_code, warehouse in repost_for: if not (item_code and warehouse): continue - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"reserved_qty": get_reserved_qty(item_code, warehouse)}) - frappe.db.sql("""delete from tabBin + frappe.db.sql( + """delete from tabBin where exists( select name from tabItem where name=tabBin.item_code and ifnull(is_stock_item, 0) = 0 ) - """) + """ + ) diff --git a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py index dd79410ba58..8ebc649aee4 100644 --- a/erpnext/patches/v4_2/update_requested_and_ordered_qty.py +++ b/erpnext/patches/v4_2/update_requested_and_ordered_qty.py @@ -8,20 +8,26 @@ import frappe def execute(): from erpnext.stock.stock_balance import get_indented_qty, get_ordered_qty, update_bin_qty - count=0 - for item_code, warehouse in frappe.db.sql("""select distinct item_code, warehouse from + count = 0 + for item_code, warehouse in frappe.db.sql( + """select distinct item_code, warehouse from (select item_code, warehouse from tabBin union - select item_code, warehouse from `tabStock Ledger Entry`) a"""): - try: - if not (item_code and warehouse): - continue - count += 1 - update_bin_qty(item_code, warehouse, { + select item_code, warehouse from `tabStock Ledger Entry`) a""" + ): + try: + if not (item_code and warehouse): + continue + count += 1 + update_bin_qty( + item_code, + warehouse, + { "indented_qty": get_indented_qty(item_code, warehouse), - "ordered_qty": get_ordered_qty(item_code, warehouse) - }) - if count % 200 == 0: - frappe.db.commit() - except Exception: - frappe.db.rollback() + "ordered_qty": get_ordered_qty(item_code, warehouse), + }, + ) + if count % 200 == 0: + frappe.db.commit() + except Exception: + frappe.db.rollback() diff --git a/erpnext/patches/v5_7/update_item_description_based_on_item_master.py b/erpnext/patches/v5_7/update_item_description_based_on_item_master.py index e7ef5ff0b49..edb0eaa6b9e 100644 --- a/erpnext/patches/v5_7/update_item_description_based_on_item_master.py +++ b/erpnext/patches/v5_7/update_item_description_based_on_item_master.py @@ -1,14 +1,17 @@ - import frappe def execute(): - name = frappe.db.sql(""" select name from `tabPatch Log` \ + name = frappe.db.sql( + """ select name from `tabPatch Log` \ where \ - patch like 'execute:frappe.db.sql("update `tabProduction Order` pro set description%' """) + patch like 'execute:frappe.db.sql("update `tabProduction Order` pro set description%' """ + ) if not name: - frappe.db.sql("update `tabProduction Order` pro \ + frappe.db.sql( + "update `tabProduction Order` pro \ set \ description = (select description from tabItem where name=pro.production_item) \ where \ - ifnull(description, '') = ''") + ifnull(description, '') = ''" + ) diff --git a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py b/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py index ed1dffe75c8..a8108745e98 100644 --- a/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py +++ b/erpnext/patches/v8_1/removed_roles_from_gst_report_non_indian_account.py @@ -6,14 +6,16 @@ import frappe def execute(): - frappe.reload_doc('core', 'doctype', 'has_role') - company = frappe.get_all('Company', filters = {'country': 'India'}) + frappe.reload_doc("core", "doctype", "has_role") + company = frappe.get_all("Company", filters={"country": "India"}) if not company: - frappe.db.sql(""" + frappe.db.sql( + """ delete from `tabHas Role` where parenttype = 'Report' and parent in('GST Sales Register', 'GST Purchase Register', 'GST Itemised Sales Register', - 'GST Itemised Purchase Register', 'Eway Bill')""") + 'GST Itemised Purchase Register', 'Eway Bill')""" + ) diff --git a/erpnext/patches/v8_1/setup_gst_india.py b/erpnext/patches/v8_1/setup_gst_india.py index 98097d00501..b03439842f2 100644 --- a/erpnext/patches/v8_1/setup_gst_india.py +++ b/erpnext/patches/v8_1/setup_gst_india.py @@ -1,37 +1,45 @@ - import frappe from frappe.email import sendmail_to_system_managers def execute(): - frappe.reload_doc('stock', 'doctype', 'item') + frappe.reload_doc("stock", "doctype", "item") frappe.reload_doc("stock", "doctype", "customs_tariff_number") frappe.reload_doc("accounts", "doctype", "payment_terms_template") frappe.reload_doc("accounts", "doctype", "payment_schedule") - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('regional', 'doctype', 'gst_settings') - frappe.reload_doc('regional', 'doctype', 'gst_hsn_code') + frappe.reload_doc("regional", "doctype", "gst_settings") + frappe.reload_doc("regional", "doctype", "gst_hsn_code") - for report_name in ('GST Sales Register', 'GST Purchase Register', - 'GST Itemised Sales Register', 'GST Itemised Purchase Register'): + for report_name in ( + "GST Sales Register", + "GST Purchase Register", + "GST Itemised Sales Register", + "GST Itemised Purchase Register", + ): - frappe.reload_doc('regional', 'report', frappe.scrub(report_name)) + frappe.reload_doc("regional", "report", frappe.scrub(report_name)) from erpnext.regional.india.setup import setup + delete_custom_field_tax_id_if_exists() setup(patch=True) send_gst_update_email() + def delete_custom_field_tax_id_if_exists(): - for field in frappe.db.sql_list("""select name from `tabCustom Field` where fieldname='tax_id' - and dt in ('Sales Order', 'Sales Invoice', 'Delivery Note')"""): + for field in frappe.db.sql_list( + """select name from `tabCustom Field` where fieldname='tax_id' + and dt in ('Sales Order', 'Sales Invoice', 'Delivery Note')""" + ): frappe.delete_doc("Custom Field", field, ignore_permissions=True) frappe.db.commit() + def send_gst_update_email(): message = """Hello, @@ -46,7 +54,9 @@ Templates and update your Customer's and Supplier's GST Numbers.

    Thanks,

    ERPNext Team. - """.format(gst_document_link=" ERPNext GST Document ") + """.format( + gst_document_link=" ERPNext GST Document " + ) try: sendmail_to_system_managers("[Important] ERPNext GST updates", message) diff --git a/erpnext/patches/v8_7/sync_india_custom_fields.py b/erpnext/patches/v8_7/sync_india_custom_fields.py index b5d58dc2eb8..e1b9a732dea 100644 --- a/erpnext/patches/v8_7/sync_india_custom_fields.py +++ b/erpnext/patches/v8_7/sync_india_custom_fields.py @@ -1,36 +1,42 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('Payroll', 'doctype', 'payroll_period') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration_category') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission_detail') + frappe.reload_doc("Payroll", "doctype", "payroll_period") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_declaration") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_proof_submission") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_declaration_category") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_proof_submission_detail") - frappe.reload_doc('accounts', 'doctype', 'tax_category') + frappe.reload_doc("accounts", "doctype", "tax_category") for doctype in ["Sales Invoice", "Delivery Note", "Purchase Invoice"]: - frappe.db.sql("""delete from `tabCustom Field` where dt = %s - and fieldname in ('port_code', 'shipping_bill_number', 'shipping_bill_date')""", doctype) + frappe.db.sql( + """delete from `tabCustom Field` where dt = %s + and fieldname in ('port_code', 'shipping_bill_number', 'shipping_bill_date')""", + doctype, + ) make_custom_fields() - frappe.db.sql(""" + frappe.db.sql( + """ update `tabCustom Field` set reqd = 0, `default` = '' where fieldname = 'reason_for_issuing_document' - """) + """ + ) - frappe.db.sql(""" + frappe.db.sql( + """ update tabAddress set gst_state_number=concat("0", gst_state_number) where ifnull(gst_state_number, '') != '' and gst_state_number<10 - """) + """ + ) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index bf8bd05fcc0..74b780e9e9d 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -30,12 +30,17 @@ class AdditionalSalary(Document): frappe.throw(_("Amount should not be less than zero")) def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def validate_recurring_additional_salary_overlap(self): if self.is_recurring: - additional_salaries = frappe.db.sql(""" + additional_salaries = frappe.db.sql( + """ SELECT name FROM `tabAdditional Salary` @@ -47,22 +52,28 @@ class AdditionalSalary(Document): AND salary_component = %s AND to_date >= %s AND from_date <= %s""", - (self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1) + (self.employee, self.name, self.salary_component, self.from_date, self.to_date), + as_dict=1, + ) additional_salaries = [salary.name for salary in additional_salaries] if additional_salaries and len(additional_salaries): - frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( - bold(comma_and(additional_salaries)), - bold(self.salary_component), - bold(formatdate(self.from_date)), - bold(formatdate(self.to_date) - ))) - + frappe.throw( + _( + "Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}" + ).format( + bold(comma_and(additional_salaries)), + bold(self.salary_component), + bold(formatdate(self.from_date)), + bold(formatdate(self.to_date)), + ) + ) def validate_dates(self): - date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) if getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("From Date can not be greater than To Date.")) @@ -81,19 +92,27 @@ class AdditionalSalary(Document): def validate_employee_referral(self): if self.ref_doctype == "Employee Referral": - referral_details = frappe.db.get_value("Employee Referral", self.ref_docname, - ["is_applicable_for_referral_bonus", "status"], as_dict=1) + referral_details = frappe.db.get_value( + "Employee Referral", + self.ref_docname, + ["is_applicable_for_referral_bonus", "status"], + as_dict=1, + ) if not referral_details.is_applicable_for_referral_bonus: - frappe.throw(_("Employee Referral {0} is not applicable for referral bonus.").format( - self.ref_docname)) + frappe.throw( + _("Employee Referral {0} is not applicable for referral bonus.").format(self.ref_docname) + ) if self.type == "Deduction": frappe.throw(_("Earning Salary Component is required for Employee Referral Bonus.")) if referral_details.status != "Accepted": - frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format( - frappe.bold("Accepted"))) + frappe.throw( + _( + "Additional Salary for referral bonus can only be created against Employee Referral with status {0}" + ).format(frappe.bold("Accepted")) + ) def update_return_amount_in_employee_advance(self): if self.ref_doctype == "Employee Advance" and self.ref_docname: @@ -123,28 +142,37 @@ class AdditionalSalary(Document): no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 return amount_per_day * no_of_days + @frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type): - comp_type = 'Earning' if component_type == 'earnings' else 'Deduction' + comp_type = "Earning" if component_type == "earnings" else "Deduction" - additional_sal = frappe.qb.DocType('Additional Salary') - component_field = additional_sal.salary_component.as_('component') - overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite') + additional_sal = frappe.qb.DocType("Additional Salary") + component_field = additional_sal.salary_component.as_("component") + overwrite_field = additional_sal.overwrite_salary_structure_amount.as_("overwrite") - additional_salary_list = frappe.qb.from_( - additional_sal - ).select( - additional_sal.name, component_field, additional_sal.type, - additional_sal.amount, additional_sal.is_recurring, overwrite_field, - additional_sal.deduct_full_tax_on_selected_payroll_date - ).where( - (additional_sal.employee == employee) - & (additional_sal.docstatus == 1) - & (additional_sal.type == comp_type) - ).where( - additional_sal.payroll_date[start_date: end_date] - | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date)) - ).run(as_dict=True) + additional_salary_list = ( + frappe.qb.from_(additional_sal) + .select( + additional_sal.name, + component_field, + additional_sal.type, + additional_sal.amount, + additional_sal.is_recurring, + overwrite_field, + additional_sal.deduct_full_tax_on_selected_payroll_date, + ) + .where( + (additional_sal.employee == employee) + & (additional_sal.docstatus == 1) + & (additional_sal.type == comp_type) + ) + .where( + additional_sal.payroll_date[start_date:end_date] + | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date)) + ) + .run(as_dict=True) + ) additional_salaries = [] components_to_overwrite = [] @@ -152,8 +180,12 @@ def get_additional_salaries(employee, start_date, end_date, component_type): for d in additional_salary_list: if d.overwrite: if d.component in components_to_overwrite: - frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}.").format( - frappe.bold(d.component), start_date, end_date), title=_("Error")) + frappe.throw( + _( + "Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}." + ).format(frappe.bold(d.component), start_date, end_date), + title=_("Error"), + ) components_to_overwrite.append(d.component) diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index 84de912e431..7d5d9e02f34 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -17,12 +17,16 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_ class TestAdditionalSalary(unittest.TestCase): - def setUp(self): setup_test() def tearDown(self): - for dt in ["Salary Slip", "Additional Salary", "Salary Structure Assignment", "Salary Structure"]: + for dt in [ + "Salary Slip", + "Additional Salary", + "Salary Structure Assignment", + "Salary Structure", + ]: frappe.db.sql("delete from `tab%s`" % dt) def test_recurring_additional_salary(self): @@ -30,10 +34,14 @@ class TestAdditionalSalary(unittest.TestCase): salary_component = None emp_id = make_employee("test_additional@salary.com") frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) - salary_structure = make_salary_structure("Test Salary Structure Additional Salary", "Monthly", employee=emp_id) + salary_structure = make_salary_structure( + "Test Salary Structure Additional Salary", "Monthly", employee=emp_id + ) add_sal = get_additional_salary(emp_id) - ss = make_employee_salary_slip("test_additional@salary.com", "Monthly", salary_structure=salary_structure.name) + ss = make_employee_salary_slip( + "test_additional@salary.com", "Monthly", salary_structure=salary_structure.name + ) for earning in ss.earnings: if earning.salary_component == "Recurring Salary Component": amount = earning.amount @@ -42,6 +50,7 @@ class TestAdditionalSalary(unittest.TestCase): self.assertEqual(amount, add_sal.amount) self.assertEqual(salary_component, add_sal.salary_component) + def get_additional_salary(emp_id): create_salary_component("Recurring Salary Component") add_sal = frappe.new_doc("Additional Salary") diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index eda50150ebd..0acd44711b0 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -34,19 +34,30 @@ class EmployeeBenefitApplication(Document): if self.remaining_benefit > 0: self.validate_remaining_benefit_amount() else: - frappe.throw(_("As per your assigned Salary Structure you cannot apply for benefits").format(self.employee)) + frappe.throw( + _("As per your assigned Salary Structure you cannot apply for benefits").format(self.employee) + ) def validate_prev_benefit_claim(self): if self.employee_benefits: for benefit in self.employee_benefits: if benefit.pay_against_benefit_claim == 1: payroll_period = frappe.get_doc("Payroll Period", self.payroll_period) - benefit_claimed = get_previous_claimed_amount(self.employee, payroll_period, component = benefit.earning_component) - benefit_given = get_sal_slip_total_benefit_given(self.employee, payroll_period, component = benefit.earning_component) + benefit_claimed = get_previous_claimed_amount( + self.employee, payroll_period, component=benefit.earning_component + ) + benefit_given = get_sal_slip_total_benefit_given( + self.employee, payroll_period, component=benefit.earning_component + ) benefit_claim_remining = benefit_claimed - benefit_given if benefit_claimed > 0 and benefit_claim_remining > benefit.amount: - frappe.throw(_("An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}").format( - benefit_claimed, benefit.earning_component, benefit_claim_remining)) + frappe.throw( + _( + "An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}" + ).format( + benefit_claimed, benefit.earning_component, benefit_claim_remining + ) + ) def validate_remaining_benefit_amount(self): # check salary structure earnings have flexi component (sum of max_benefit_amount) @@ -65,20 +76,34 @@ class EmployeeBenefitApplication(Document): if salary_structure.earnings: for earnings in salary_structure.earnings: if earnings.is_flexible_benefit == 1 and earnings.salary_component not in benefit_components: - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", earnings.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + earnings.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) if pay_against_benefit_claim != 1: pro_rata_amount += max_benefit_amount else: non_pro_rata_amount += max_benefit_amount - if pro_rata_amount == 0 and non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to any of the existing component").format(self.remaining_benefit)) + if pro_rata_amount == 0 and non_pro_rata_amount == 0: + frappe.throw( + _("Please add the remaining benefits {0} to any of the existing component").format( + self.remaining_benefit + ) + ) elif non_pro_rata_amount > 0 and non_pro_rata_amount < rounded(self.remaining_benefit): - frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component").format( - non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) + frappe.throw( + _( + "You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component" + ).format(non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount) + ) elif non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to the application as pro-rata component").format( - self.remaining_benefit)) + frappe.throw( + _("Please add the remaining benefits {0} to the application as pro-rata component").format( + self.remaining_benefit + ) + ) def validate_max_benefit_for_component(self): if self.employee_benefits: @@ -87,30 +112,43 @@ class EmployeeBenefitApplication(Document): self.validate_max_benefit(employee_benefit.earning_component) max_benefit_amount += employee_benefit.amount if max_benefit_amount > self.max_benefits: - frappe.throw(_("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, self.max_benefits)) + frappe.throw( + _("Maximum benefit amount of employee {0} exceeds {1}").format( + self.employee, self.max_benefits + ) + ) def validate_max_benefit(self, earning_component_name): - max_benefit_amount = frappe.db.get_value("Salary Component", earning_component_name, "max_benefit_amount") + max_benefit_amount = frappe.db.get_value( + "Salary Component", earning_component_name, "max_benefit_amount" + ) benefit_amount = 0 for employee_benefit in self.employee_benefits: if employee_benefit.earning_component == earning_component_name: benefit_amount += employee_benefit.amount - prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name) + prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given( + self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name + ) benefit_amount += prev_sal_slip_flexi_amount if rounded(benefit_amount, 2) > max_benefit_amount: - frappe.throw(_("Maximum benefit amount of component {0} exceeds {1}").format(earning_component_name, max_benefit_amount)) + frappe.throw( + _("Maximum benefit amount of component {0} exceeds {1}").format( + earning_component_name, max_benefit_amount + ) + ) def validate_duplicate_on_payroll_period(self): application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': self.employee, - 'payroll_period': self.payroll_period, - 'docstatus': 1 - } + {"employee": self.employee, "payroll_period": self.payroll_period, "docstatus": 1}, ) if application: - frappe.throw(_("Employee {0} already submited an apllication {1} for the payroll period {2}").format(self.employee, application, self.payroll_period)) + frappe.throw( + _("Employee {0} already submited an apllication {1} for the payroll period {2}").format( + self.employee, application, self.payroll_period + ) + ) + @frappe.whitelist() def get_max_benefits(employee, on_date): @@ -121,6 +159,7 @@ def get_max_benefits(employee, on_date): return max_benefits return False + @frappe.whitelist() def get_max_benefits_remaining(employee, on_date, payroll_period): max_benefits = get_max_benefits(employee, on_date) @@ -141,9 +180,14 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): sal_struct = frappe.get_doc("Salary Structure", sal_struct_name) for sal_struct_row in sal_struct.get("earnings"): salary_component = frappe.get_doc("Salary Component", sal_struct_row.salary_component) - if salary_component.depends_on_payment_days == 1 and salary_component.pay_against_benefit_claim != 1: + if ( + salary_component.depends_on_payment_days == 1 + and salary_component.pay_against_benefit_claim != 1 + ): have_depends_on_payment_days = True - benefit_amount = get_benefit_amount_based_on_pro_rata(sal_struct, salary_component.max_benefit_amount) + benefit_amount = get_benefit_amount_based_on_pro_rata( + sal_struct, salary_component.max_benefit_amount + ) amount_per_day = benefit_amount / payroll_period_days per_day_amount_total += amount_per_day @@ -159,12 +203,14 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): return max_benefits - prev_sal_slip_flexi_total return max_benefits + def calculate_lwp(employee, start_date, holidays, working_days): lwp = 0 holidays = "','".join(holidays) for d in range(working_days): dt = add_days(cstr(getdate(start_date)), d) - leave = frappe.db.sql(""" + leave = frappe.db.sql( + """ select t1.name, t1.half_day from `tabLeave Application` t1, `tabLeave Type` t2 where t2.name = t1.leave_type @@ -174,56 +220,77 @@ def calculate_lwp(employee, start_date, holidays, working_days): and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date WHEN t2.include_holiday THEN %(dt)s between from_date and to_date END - """.format(holidays), {"employee": employee, "dt": dt}) + """.format( + holidays + ), + {"employee": employee, "dt": dt}, + ) if leave: lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) return lwp -def get_benefit_component_amount(employee, start_date, end_date, salary_component, sal_struct, payroll_frequency, payroll_period): + +def get_benefit_component_amount( + employee, start_date, end_date, salary_component, sal_struct, payroll_frequency, payroll_period +): if not payroll_period: - frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}") - .format(salary_component)) + frappe.msgprint( + _("Start and end dates not in a valid Payroll Period, cannot calculate {0}").format( + salary_component + ) + ) return False # Considering there is only one application for a year - benefit_application = frappe.db.sql(""" + benefit_application = frappe.db.sql( + """ select name from `tabEmployee Benefit Application` where payroll_period=%(payroll_period)s and employee=%(employee)s and docstatus = 1 - """, { - 'employee': employee, - 'payroll_period': payroll_period.name - }) + """, + {"employee": employee, "payroll_period": payroll_period.name}, + ) current_benefit_amount = 0.0 - component_max_benefit, depends_on_payment_days = frappe.db.get_value("Salary Component", - salary_component, ["max_benefit_amount", "depends_on_payment_days"]) + component_max_benefit, depends_on_payment_days = frappe.db.get_value( + "Salary Component", salary_component, ["max_benefit_amount", "depends_on_payment_days"] + ) benefit_amount = 0 if benefit_application: - benefit_amount = frappe.db.get_value("Employee Benefit Application Detail", - {"parent": benefit_application[0][0], "earning_component": salary_component}, "amount") + benefit_amount = frappe.db.get_value( + "Employee Benefit Application Detail", + {"parent": benefit_application[0][0], "earning_component": salary_component}, + "amount", + ) elif component_max_benefit: benefit_amount = get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit) current_benefit_amount = 0 if benefit_amount: - total_sub_periods = get_period_factor(employee, - start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days)[0] + total_sub_periods = get_period_factor( + employee, start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days + )[0] current_benefit_amount = benefit_amount / total_sub_periods return current_benefit_amount + def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): max_benefits_total = 0 benefit_amount = 0 for d in sal_struct.get("earnings"): if d.is_flexible_benefit == 1: - component = frappe.db.get_value("Salary Component", d.salary_component, ["max_benefit_amount", "pay_against_benefit_claim"], as_dict=1) + component = frappe.db.get_value( + "Salary Component", + d.salary_component, + ["max_benefit_amount", "pay_against_benefit_claim"], + as_dict=1, + ) if not component.pay_against_benefit_claim: max_benefits_total += component.max_benefit_amount @@ -234,34 +301,46 @@ def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): return benefit_amount + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_earning_components(doctype, txt, searchfield, start, page_len, filters): if len(filters) < 2: return {} - salary_structure = get_assigned_salary_structure(filters['employee'], filters['date']) + salary_structure = get_assigned_salary_structure(filters["employee"], filters["date"]) if salary_structure: - return frappe.db.sql(""" + return frappe.db.sql( + """ select salary_component from `tabSalary Detail` where parent = %s and is_flexible_benefit = 1 order by name - """, salary_structure) + """, + salary_structure, + ) else: - frappe.throw(_("Salary Structure not found for employee {0} and date {1}") - .format(filters['employee'], filters['date'])) + frappe.throw( + _("Salary Structure not found for employee {0} and date {1}").format( + filters["employee"], filters["date"] + ) + ) + @frappe.whitelist() def get_earning_components_max_benefits(employee, date, earning_component): salary_structure = get_assigned_salary_structure(employee, date) - amount = frappe.db.sql(""" + amount = frappe.db.sql( + """ select amount from `tabSalary Detail` where parent = %s and is_flexible_benefit = 1 and salary_component = %s order by name - """, salary_structure, earning_component) + """, + salary_structure, + earning_component, + ) return amount if amount else 0 diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py index 801ce4ba367..31f26b25e73 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py @@ -23,9 +23,15 @@ class EmployeeBenefitClaim(Document): max_benefits = get_max_benefits(self.employee, self.claim_date) if not max_benefits or max_benefits <= 0: frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) - payroll_period = get_payroll_period(self.claim_date, self.claim_date, frappe.db.get_value("Employee", self.employee, "company")) + payroll_period = get_payroll_period( + self.claim_date, self.claim_date, frappe.db.get_value("Employee", self.employee, "company") + ) if not payroll_period: - frappe.throw(_("{0} is not in a valid Payroll Period").format(frappe.format(self.claim_date, dict(fieldtype='Date')))) + frappe.throw( + _("{0} is not in a valid Payroll Period").format( + frappe.format(self.claim_date, dict(fieldtype="Date")) + ) + ) self.validate_max_benefit_for_component(payroll_period) self.validate_max_benefit_for_sal_struct(max_benefits) self.validate_benefit_claim_amount(max_benefits, payroll_period) @@ -36,21 +42,31 @@ class EmployeeBenefitClaim(Document): claimed_amount = self.claimed_amount claimed_amount += get_previous_claimed_amount(self.employee, payroll_period) if max_benefits < claimed_amount: - frappe.throw(_("Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed\ - amount").format(self.employee, max_benefits, claimed_amount-max_benefits)) + frappe.throw( + _( + "Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed\ + amount" + ).format(self.employee, max_benefits, claimed_amount - max_benefits) + ) def validate_max_benefit_for_sal_struct(self, max_benefits): if self.claimed_amount > max_benefits: - frappe.throw(_("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, max_benefits)) + frappe.throw( + _("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, max_benefits) + ) def validate_max_benefit_for_component(self, payroll_period): if self.max_amount_eligible: claimed_amount = self.claimed_amount - claimed_amount += get_previous_claimed_amount(self.employee, - payroll_period, component = self.earning_component) + claimed_amount += get_previous_claimed_amount( + self.employee, payroll_period, component=self.earning_component + ) if claimed_amount > self.max_amount_eligible: - frappe.throw(_("Maximum amount eligible for the component {0} exceeds {1}") - .format(self.earning_component, self.max_amount_eligible)) + frappe.throw( + _("Maximum amount eligible for the component {0} exceeds {1}").format( + self.earning_component, self.max_amount_eligible + ) + ) def validate_non_pro_rata_benefit_claim(self, max_benefits, payroll_period): claimed_amount = self.claimed_amount @@ -64,30 +80,39 @@ class EmployeeBenefitClaim(Document): sal_struct = frappe.get_doc("Salary Structure", sal_struct_name) pro_rata_amount = get_benefit_pro_rata_ratio_amount(self.employee, self.claim_date, sal_struct) - claimed_amount += get_previous_claimed_amount(self.employee, payroll_period, non_pro_rata = True) + claimed_amount += get_previous_claimed_amount(self.employee, payroll_period, non_pro_rata=True) if max_benefits < pro_rata_amount + claimed_amount: - frappe.throw(_("Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component\ - amount and previous claimed amount").format(self.employee, max_benefits, pro_rata_amount+claimed_amount-max_benefits)) + frappe.throw( + _( + "Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component\ + amount and previous claimed amount" + ).format( + self.employee, max_benefits, pro_rata_amount + claimed_amount - max_benefits + ) + ) def get_pro_rata_amount_in_application(self, payroll_period): application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': self.employee, - 'payroll_period': payroll_period, - 'docstatus': 1 - } + {"employee": self.employee, "payroll_period": payroll_period, "docstatus": 1}, ) if application: - return frappe.db.get_value("Employee Benefit Application", application, "pro_rata_dispensed_amount") + return frappe.db.get_value( + "Employee Benefit Application", application, "pro_rata_dispensed_amount" + ) return False + def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): total_pro_rata_max = 0 benefit_amount_total = 0 for sal_struct_row in sal_struct.get("earnings"): try: - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", sal_struct_row.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + sal_struct_row.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) except TypeError: # show the error in tests? frappe.throw(_("Unable to find Salary Component {0}").format(sal_struct_row.salary_component)) @@ -95,7 +120,11 @@ def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): total_pro_rata_max += max_benefit_amount if total_pro_rata_max > 0: for sal_struct_row in sal_struct.get("earnings"): - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", sal_struct_row.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + sal_struct_row.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) if sal_struct_row.is_flexible_benefit == 1 and pay_against_benefit_claim != 1: component_max = max_benefit_amount @@ -105,6 +134,7 @@ def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): benefit_amount_total += benefit_amount return benefit_amount_total + def get_benefit_claim_amount(employee, start_date, end_date, salary_component=None): query = """ select sum(claimed_amount) @@ -119,41 +149,54 @@ def get_benefit_claim_amount(employee, start_date, end_date, salary_component=No if salary_component: query += " and earning_component = %(earning_component)s" - claimed_amount = flt(frappe.db.sql(query, { - 'employee': employee, - 'start_date': start_date, - 'end_date': end_date, - 'earning_component': salary_component - })[0][0]) + claimed_amount = flt( + frappe.db.sql( + query, + { + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "earning_component": salary_component, + }, + )[0][0] + ) return claimed_amount + def get_total_benefit_dispensed(employee, sal_struct, sal_slip_start_date, payroll_period): pro_rata_amount = 0 claimed_amount = 0 application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': employee, - 'payroll_period': payroll_period.name, - 'docstatus': 1 - } + {"employee": employee, "payroll_period": payroll_period.name, "docstatus": 1}, ) if application: application_obj = frappe.get_doc("Employee Benefit Application", application) - pro_rata_amount = application_obj.pro_rata_dispensed_amount + application_obj.max_benefits - application_obj.remaining_benefit + pro_rata_amount = ( + application_obj.pro_rata_dispensed_amount + + application_obj.max_benefits + - application_obj.remaining_benefit + ) else: pro_rata_amount = get_benefit_pro_rata_ratio_amount(employee, sal_slip_start_date, sal_struct) - claimed_amount += get_benefit_claim_amount(employee, payroll_period.start_date, payroll_period.end_date) + claimed_amount += get_benefit_claim_amount( + employee, payroll_period.start_date, payroll_period.end_date + ) return claimed_amount + pro_rata_amount -def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end_date, payroll_period, sal_struct): + +def get_last_payroll_period_benefits( + employee, sal_slip_start_date, sal_slip_end_date, payroll_period, sal_struct +): max_benefits = get_max_benefits(employee, payroll_period.end_date) if not max_benefits: max_benefits = 0 - remaining_benefit = max_benefits - get_total_benefit_dispensed(employee, sal_struct, sal_slip_start_date, payroll_period) + remaining_benefit = max_benefits - get_total_benefit_dispensed( + employee, sal_struct, sal_slip_start_date, payroll_period + ) if remaining_benefit > 0: have_remaining = True # Set the remaining benefits to flexi non pro-rata component in the salary structure @@ -162,7 +205,9 @@ def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end if d.is_flexible_benefit == 1: salary_component = frappe.get_doc("Salary Component", d.salary_component) if salary_component.pay_against_benefit_claim == 1: - claimed_amount = get_benefit_claim_amount(employee, payroll_period.start_date, sal_slip_end_date, d.salary_component) + claimed_amount = get_benefit_claim_amount( + employee, payroll_period.start_date, sal_slip_end_date, d.salary_component + ) amount_fit_to_component = salary_component.max_benefit_amount - claimed_amount if amount_fit_to_component > 0: if remaining_benefit > amount_fit_to_component: @@ -171,19 +216,23 @@ def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end else: amount = remaining_benefit have_remaining = False - current_claimed_amount = get_benefit_claim_amount(employee, sal_slip_start_date, sal_slip_end_date, d.salary_component) + current_claimed_amount = get_benefit_claim_amount( + employee, sal_slip_start_date, sal_slip_end_date, d.salary_component + ) amount += current_claimed_amount struct_row = {} salary_components_dict = {} - struct_row['depends_on_payment_days'] = salary_component.depends_on_payment_days - struct_row['salary_component'] = salary_component.name - struct_row['abbr'] = salary_component.salary_component_abbr - struct_row['do_not_include_in_total'] = salary_component.do_not_include_in_total - struct_row['is_tax_applicable'] = salary_component.is_tax_applicable, - struct_row['is_flexible_benefit'] = salary_component.is_flexible_benefit, - struct_row['variable_based_on_taxable_salary'] = salary_component.variable_based_on_taxable_salary - salary_components_dict['amount'] = amount - salary_components_dict['struct_row'] = struct_row + struct_row["depends_on_payment_days"] = salary_component.depends_on_payment_days + struct_row["salary_component"] = salary_component.name + struct_row["abbr"] = salary_component.salary_component_abbr + struct_row["do_not_include_in_total"] = salary_component.do_not_include_in_total + struct_row["is_tax_applicable"] = (salary_component.is_tax_applicable,) + struct_row["is_flexible_benefit"] = (salary_component.is_flexible_benefit,) + struct_row[ + "variable_based_on_taxable_salary" + ] = salary_component.variable_based_on_taxable_salary + salary_components_dict["amount"] = amount + salary_components_dict["struct_row"] = struct_row salary_components_array.append(salary_components_dict) if not have_remaining: break diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index a37e22425f7..7686185349f 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -15,13 +15,17 @@ class EmployeeIncentive(Document): self.validate_salary_structure() def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def on_submit(self): - company = frappe.db.get_value('Employee', self.employee, 'company') + company = frappe.db.get_value("Employee", self.employee, "company") - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.currency = self.currency additional_salary.salary_component = self.salary_component diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index 9b5eab636f1..c0ef2eee78c 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -20,7 +20,9 @@ class EmployeeTaxExemptionDeclaration(Document): def validate(self): validate_active_employee(self.employee) validate_tax_declaration(self.declarations) - validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) + validate_duplicate_exemption_for_payroll_period( + self.doctype, self.name, self.payroll_period, self.employee + ) self.set_total_declared_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() @@ -43,17 +45,23 @@ class EmployeeTaxExemptionDeclaration(Document): self.annual_hra_exemption = hra_exemption["annual_exemption"] self.monthly_hra_exemption = hra_exemption["monthly_exemption"] + @frappe.whitelist() def make_proof_submission(source_name, target_doc=None): - doclist = get_mapped_doc("Employee Tax Exemption Declaration", source_name, { - "Employee Tax Exemption Declaration": { - "doctype": "Employee Tax Exemption Proof Submission", - "field_no_map": ["monthly_house_rent", "monthly_hra_exemption"] + doclist = get_mapped_doc( + "Employee Tax Exemption Declaration", + source_name, + { + "Employee Tax Exemption Declaration": { + "doctype": "Employee Tax Exemption Proof Submission", + "field_no_map": ["monthly_house_rent", "monthly_hra_exemption"], + }, + "Employee Tax Exemption Declaration Category": { + "doctype": "Employee Tax Exemption Proof Submission Detail", + "add_if_empty": True, + }, }, - "Employee Tax Exemption Declaration Category": { - "doctype": "Employee Tax Exemption Proof Submission Detail", - "add_if_empty": True - } - }, target_doc) + target_doc, + ) return doclist diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index fc28afdc3e5..1d90e7383fe 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -19,112 +19,147 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""") def test_duplicate_category_in_declaration(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000), - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 50000) - ] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ), + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ) self.assertRaises(frappe.ValidationError, declaration.save) def test_duplicate_entry_for_payroll_period(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000), - dict(exemption_sub_category = "_Test1 Sub Category", - exemption_category = "_Test Category", - amount = 50000), - ] - }).insert() + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ).insert() - duplicate_declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000) - ] - }) + duplicate_declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ) + ], + } + ) self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert) - duplicate_declaration.employee = frappe.get_value("Employee", {"user_id":"employee1@taxexepmtion.com"}, "name") + duplicate_declaration.employee = frappe.get_value( + "Employee", {"user_id": "employee1@taxexepmtion.com"}, "name" + ) self.assertTrue(duplicate_declaration.insert) def test_exemption_amount(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 80000), - dict(exemption_sub_category = "_Test1 Sub Category", - exemption_category = "_Test Category", - amount = 60000), - ] - }).insert() + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() self.assertEqual(declaration.total_exemption_amount, 100000) + def create_payroll_period(**args): args = frappe._dict(args) name = args.name or "_Test Payroll Period" if not frappe.db.exists("Payroll Period", name): from datetime import date - payroll_period = frappe.get_doc(dict( - doctype = 'Payroll Period', - name = name, - company = args.company or erpnext.get_default_company(), - start_date = args.start_date or date(date.today().year, 1, 1), - end_date = args.end_date or date(date.today().year, 12, 31) - )).insert() + + payroll_period = frappe.get_doc( + dict( + doctype="Payroll Period", + name=name, + company=args.company or erpnext.get_default_company(), + start_date=args.start_date or date(date.today().year, 1, 1), + end_date=args.end_date or date(date.today().year, 12, 31), + ) + ).insert() return payroll_period else: return frappe.get_doc("Payroll Period", name) + def create_exemption_category(): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): - category = frappe.get_doc({ - "doctype": "Employee Tax Exemption Category", - "name": "_Test Category", - "deduction_component": "Income Tax", - "max_amount": 100000 - }).insert() + category = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Category", + "name": "_Test Category", + "deduction_component": "Income Tax", + "max_amount": 100000, + } + ).insert() if not frappe.db.exists("Employee Tax Exemption Sub Category", "_Test Sub Category"): - frappe.get_doc({ - "doctype": "Employee Tax Exemption Sub Category", - "name": "_Test Sub Category", - "exemption_category": "_Test Category", - "max_amount": 100000, - "is_active": 1 - }).insert() + frappe.get_doc( + { + "doctype": "Employee Tax Exemption Sub Category", + "name": "_Test Sub Category", + "exemption_category": "_Test Category", + "max_amount": 100000, + "is_active": 1, + } + ).insert() if not frappe.db.exists("Employee Tax Exemption Sub Category", "_Test1 Sub Category"): - frappe.get_doc({ - "doctype": "Employee Tax Exemption Sub Category", - "name": "_Test1 Sub Category", - "exemption_category": "_Test Category", - "max_amount": 50000, - "is_active": 1 - }).insert() + frappe.get_doc( + { + "doctype": "Employee Tax Exemption Sub Category", + "name": "_Test1 Sub Category", + "exemption_category": "_Test Category", + "max_amount": 50000, + "is_active": 1, + } + ).insert() diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 56e73b37dff..c52efaba592 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -21,7 +21,9 @@ class EmployeeTaxExemptionProofSubmission(Document): self.set_total_actual_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() - validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) + validate_duplicate_exemption_for_payroll_period( + self.doctype, self.name, self.payroll_period, self.employee + ) def set_total_actual_amount(self): self.total_actual_amount = flt(self.get("house_rent_payment_amount")) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py index f2aa64c2878..58b2c1af058 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py @@ -19,40 +19,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""") def test_exemption_amount_lesser_than_category_max(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "payroll_period": "Test Payroll Period", - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - type_of_proof = "Test Proof", - exemption_category = "_Test Category", - amount = 150000)] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "payroll_period": "Test Payroll Period", + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + type_of_proof="Test Proof", + exemption_category="_Test Category", + amount=150000, + ) + ], + } + ) self.assertRaises(frappe.ValidationError, declaration.save) - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "payroll_period": "Test Payroll Period", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - type_of_proof = "Test Proof", - exemption_category = "_Test Category", - amount = 100000)] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "payroll_period": "Test Payroll Period", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + type_of_proof="Test Proof", + exemption_category="_Test Category", + amount=100000, + ) + ], + } + ) self.assertTrue(declaration.save) self.assertTrue(declaration.submit) def test_duplicate_category_in_proof_submission(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "payroll_period": "Test Payroll Period", - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - type_of_proof = "Test Proof", - amount = 100000), - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 50000), - ] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "payroll_period": "Test Payroll Period", + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=100000, + ), + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ) self.assertRaises(frappe.ValidationError, declaration.save) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py index 4ac11f7112d..fb75d6706c3 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py @@ -10,7 +10,12 @@ from frappe.utils import flt class EmployeeTaxExemptionSubCategory(Document): def validate(self): - category_max_amount = frappe.db.get_value("Employee Tax Exemption Category", self.exemption_category, "max_amount") + category_max_amount = frappe.db.get_value( + "Employee Tax Exemption Category", self.exemption_category, "max_amount" + ) if flt(self.max_amount) > flt(category_max_amount): - frappe.throw(_("Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1}") - .format(category_max_amount, self.exemption_category)) + frappe.throw( + _( + "Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1}" + ).format(category_max_amount, self.exemption_category) + ) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 939634a9310..91740ae8c6c 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -27,7 +27,7 @@ class Gratuity(AccountsController): self.create_gl_entries() def on_cancel(self): - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] self.create_gl_entries(cancel=True) def create_gl_entries(self, cancel=False): @@ -39,28 +39,34 @@ class Gratuity(AccountsController): # payable entry if self.amount: gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "credit": self.amount, - "credit_in_account_currency": self.amount, - "against": self.expense_account, - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.payable_account, + "credit": self.amount, + "credit_in_account_currency": self.amount, + "against": self.expense_account, + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "cost_center": self.cost_center, + }, + item=self, + ) ) # expense entries gl_entry.append( - self.get_gl_dict({ - "account": self.expense_account, - "debit": self.amount, - "debit_in_account_currency": self.amount, - "against": self.payable_account, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.expense_account, + "debit": self.amount, + "debit_in_account_currency": self.amount, + "against": self.payable_account, + "cost_center": self.cost_center, + }, + item=self, + ) ) else: frappe.throw(_("Total Amount can not be zero")) @@ -69,7 +75,7 @@ class Gratuity(AccountsController): def create_additional_salary(self): if self.pay_via_salary_slip: - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.salary_component = self.salary_component additional_salary.overwrite_salary_structure_amount = 0 @@ -81,19 +87,22 @@ class Gratuity(AccountsController): additional_salary.submit() def set_total_advance_paid(self): - paid_amount = frappe.db.sql(""" + paid_amount = frappe.db.sql( + """ select ifnull(sum(debit_in_account_currency), 0) as paid_amount from `tabGL Entry` where against_voucher_type = 'Gratuity' and against_voucher = %s and party_type = 'Employee' and party = %s - """, (self.name, self.employee), as_dict=1)[0].paid_amount + """, + (self.name, self.employee), + as_dict=1, + )[0].paid_amount if flt(paid_amount) > self.amount: frappe.throw(_("Row {0}# Paid Amount cannot be greater than Total amount")) - self.db_set("paid_amount", paid_amount) if self.amount == self.paid_amount: self.db_set("status", "Paid") @@ -104,69 +113,97 @@ def calculate_work_experience_and_amount(employee, gratuity_rule): current_work_experience = calculate_work_experience(employee, gratuity_rule) or 0 gratuity_amount = calculate_gratuity_amount(employee, gratuity_rule, current_work_experience) or 0 - return {'current_work_experience': current_work_experience, "amount": gratuity_amount} + return {"current_work_experience": current_work_experience, "amount": gratuity_amount} + def calculate_work_experience(employee, gratuity_rule): - total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value("Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"]) + total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"] + ) - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) if not relieving_date: - frappe.throw(_("Please set Relieving Date for employee: {0}").format(bold(get_link_to_form("Employee", employee)))) + frappe.throw( + _("Please set Relieving Date for employee: {0}").format( + bold(get_link_to_form("Employee", employee)) + ) + ) - method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function") - employee_total_workings_days = calculate_employee_total_workings_days(employee, date_of_joining, relieving_date) + method = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, "work_experience_calculation_function" + ) + employee_total_workings_days = calculate_employee_total_workings_days( + employee, date_of_joining, relieving_date + ) - current_work_experience = employee_total_workings_days/total_working_days_per_year or 1 - current_work_experience = get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee) + current_work_experience = employee_total_workings_days / total_working_days_per_year or 1 + current_work_experience = get_work_experience_using_method( + method, current_work_experience, minimum_year_for_gratuity, employee + ) return current_work_experience -def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date ): + +def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date): employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on") or "Leave" if payroll_based_on == "Leave": total_lwp = get_non_working_days(employee, relieving_date, "On Leave") employee_total_workings_days -= total_lwp - elif payroll_based_on == "Attendance": + elif payroll_based_on == "Attendance": total_absents = get_non_working_days(employee, relieving_date, "Absent") employee_total_workings_days -= total_absents return employee_total_workings_days -def get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee): + +def get_work_experience_using_method( + method, current_work_experience, minimum_year_for_gratuity, employee +): if method == "Round off Work Experience": current_work_experience = round(current_work_experience) else: current_work_experience = floor(current_work_experience) if current_work_experience < minimum_year_for_gratuity: - frappe.throw(_("Employee: {0} have to complete minimum {1} years for gratuity").format(bold(employee), minimum_year_for_gratuity)) + frappe.throw( + _("Employee: {0} have to complete minimum {1} years for gratuity").format( + bold(employee), minimum_year_for_gratuity + ) + ) return current_work_experience + def get_non_working_days(employee, relieving_date, status): - filters={ - "docstatus": 1, - "status": status, - "employee": employee, - "attendance_date": ("<=", get_datetime(relieving_date)) - } + filters = { + "docstatus": 1, + "status": status, + "employee": employee, + "attendance_date": ("<=", get_datetime(relieving_date)), + } if status == "On Leave": - lwp_leave_types = frappe.get_list("Leave Type", filters = {"is_lwp":1}) + lwp_leave_types = frappe.get_list("Leave Type", filters={"is_lwp": 1}) lwp_leave_types = [leave_type.name for leave_type in lwp_leave_types] - filters["leave_type"] = ("IN", lwp_leave_types) + filters["leave_type"] = ("IN", lwp_leave_types) - - record = frappe.get_all("Attendance", filters=filters, fields = ["COUNT(name) as total_lwp"]) + record = frappe.get_all("Attendance", filters=filters, fields=["COUNT(name) as total_lwp"]) return record[0].total_lwp if len(record) else 0 + def calculate_gratuity_amount(employee, gratuity_rule, experience): applicable_earnings_component = get_applicable_components(gratuity_rule) - total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule) + total_applicable_components_amount = get_total_applicable_component_amount( + employee, applicable_earnings_component, gratuity_rule + ) - calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on") + calculate_gratuity_amount_based_on = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on" + ) gratuity_amount = 0 slabs = get_gratuity_rule_slabs(gratuity_rule) slab_found = False @@ -174,49 +211,78 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): for slab in slabs: if calculate_gratuity_amount_based_on == "Current Slab": - slab_found, gratuity_amount = calculate_amount_based_on_current_slab(slab.from_year, slab.to_year, - experience, total_applicable_components_amount, slab.fraction_of_applicable_earnings) + slab_found, gratuity_amount = calculate_amount_based_on_current_slab( + slab.from_year, + slab.to_year, + experience, + total_applicable_components_amount, + slab.fraction_of_applicable_earnings, + ) if slab_found: - break + break elif calculate_gratuity_amount_based_on == "Sum of all previous slabs": if slab.to_year == 0 and slab.from_year == 0: - gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + gratuity_amount += ( + year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + ) slab_found = True break - if experience > slab.to_year and experience > slab.from_year and slab.to_year !=0: - gratuity_amount += (slab.to_year - slab.from_year) * total_applicable_components_amount * slab.fraction_of_applicable_earnings - year_left -= (slab.to_year - slab.from_year) + if experience > slab.to_year and experience > slab.from_year and slab.to_year != 0: + gratuity_amount += ( + (slab.to_year - slab.from_year) + * total_applicable_components_amount + * slab.fraction_of_applicable_earnings + ) + year_left -= slab.to_year - slab.from_year slab_found = True elif slab.from_year <= experience and (experience < slab.to_year or slab.to_year == 0): - gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + gratuity_amount += ( + year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + ) slab_found = True if not slab_found: - frappe.throw(_("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format(bold(gratuity_rule))) + frappe.throw( + _("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format( + bold(gratuity_rule) + ) + ) return gratuity_amount + def get_applicable_components(gratuity_rule): - applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"]) + applicable_earnings_component = frappe.get_all( + "Gratuity Applicable Component", filters={"parent": gratuity_rule}, fields=["salary_component"] + ) if len(applicable_earnings_component) == 0: - frappe.throw(_("No Applicable Earnings Component found for Gratuity Rule: {0}").format(bold(get_link_to_form("Gratuity Rule",gratuity_rule)))) - applicable_earnings_component = [component.salary_component for component in applicable_earnings_component] + frappe.throw( + _("No Applicable Earnings Component found for Gratuity Rule: {0}").format( + bold(get_link_to_form("Gratuity Rule", gratuity_rule)) + ) + ) + applicable_earnings_component = [ + component.salary_component for component in applicable_earnings_component + ] return applicable_earnings_component + def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule): - sal_slip = get_last_salary_slip(employee) + sal_slip = get_last_salary_slip(employee) if not sal_slip: frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee))) - component_and_amounts = frappe.get_all("Salary Detail", + component_and_amounts = frappe.get_all( + "Salary Detail", filters={ "docstatus": 1, - 'parent': sal_slip, + "parent": sal_slip, "parentfield": "earnings", - 'salary_component': ('in', applicable_earnings_component) + "salary_component": ("in", applicable_earnings_component), }, - fields=["amount"]) + fields=["amount"], + ) total_applicable_components_amount = 0 if not len(component_and_amounts): frappe.throw(_("No Applicable Component is present in last month salary slip")) @@ -224,30 +290,44 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen total_applicable_components_amount += data.amount return total_applicable_components_amount -def calculate_amount_based_on_current_slab(from_year, to_year, experience, total_applicable_components_amount, fraction_of_applicable_earnings): - slab_found = False; gratuity_amount = 0 + +def calculate_amount_based_on_current_slab( + from_year, + to_year, + experience, + total_applicable_components_amount, + fraction_of_applicable_earnings, +): + slab_found = False + gratuity_amount = 0 if experience >= from_year and (to_year == 0 or experience < to_year): - gratuity_amount = total_applicable_components_amount * experience * fraction_of_applicable_earnings + gratuity_amount = ( + total_applicable_components_amount * experience * fraction_of_applicable_earnings + ) if fraction_of_applicable_earnings: slab_found = True return slab_found, gratuity_amount + def get_gratuity_rule_slabs(gratuity_rule): - return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx") + return frappe.get_all( + "Gratuity Rule Slab", filters={"parent": gratuity_rule}, fields=["*"], order_by="idx" + ) + def get_salary_structure(employee): - return frappe.get_list("Salary Structure Assignment", filters = { - "employee": employee, 'docstatus': 1 - }, + return frappe.get_list( + "Salary Structure Assignment", + filters={"employee": employee, "docstatus": 1}, fields=["from_date", "salary_structure"], - order_by = "from_date desc")[0].salary_structure + order_by="from_date desc", + )[0].salary_structure + def get_last_salary_slip(employee): - salary_slips = frappe.get_list("Salary Slip", filters = { - "employee": employee, 'docstatus': 1 - }, - order_by = "start_date desc" + salary_slips = frappe.get_list( + "Salary Slip", filters={"employee": employee, "docstatus": 1}, order_by="start_date desc" ) if not salary_slips: return diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py index 6c3cdfda512..35ae1f4feaf 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py +++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py @@ -1,21 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'reference_name', - 'non_standard_fieldnames': { - 'Additional Salary': 'ref_docname', + "fieldname": "reference_name", + "non_standard_fieldnames": { + "Additional Salary": "ref_docname", }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry'] - }, - { - 'label': _('Additional Salary'), - 'items': ['Additional Salary'] - } - ] + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry"]}, + {"label": _("Additional Salary"), "items": ["Additional Salary"]}, + ], } diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 098d71c8f80..0e39dd36710 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -17,18 +17,20 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule test_dependencies = ["Salary Component", "Salary Slip", "Account"] + + class TestGratuity(unittest.TestCase): @classmethod def setUpClass(cls): - make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) - make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) + make_earning_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) def setUp(self): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") def test_get_last_salary_slip_should_return_none_for_new_employee(self): - new_employee = make_employee("new_employee@salary.com", company='_Test Company') + new_employee = make_employee("new_employee@salary.com", company="_Test Company") salary_slip = get_last_salary_slip(new_employee) assert salary_slip is None @@ -37,35 +39,42 @@ class TestGratuity(unittest.TestCase): rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") - gratuity = create_gratuity(pay_via_salary_slip = 1, employee=employee, rule=rule.name) + gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name) - #work experience calculation - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) - employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days + # work experience calculation + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) + employee_total_workings_days = ( + get_datetime(relieving_date) - get_datetime(date_of_joining) + ).days - experience = employee_total_workings_days/rule.total_working_days_per_year + experience = employee_total_workings_days / rule.total_working_days_per_year gratuity.reload() from math import floor + self.assertEqual(floor(experience), gratuity.current_work_experience) - #amount Calculation - component_amount = frappe.get_all("Salary Detail", - filters={ - "docstatus": 1, - 'parent': sal_slip, - "parentfield": "earnings", - 'salary_component': "Basic Salary" - }, - fields=["amount"]) + # amount Calculation + component_amount = frappe.get_all( + "Salary Detail", + filters={ + "docstatus": 1, + "parent": sal_slip, + "parentfield": "earnings", + "salary_component": "Basic Salary", + }, + fields=["amount"], + ) - ''' 5 - 0 fraction is 1 ''' + """ 5 - 0 fraction is 1 """ gratuity_amount = component_amount[0].amount * experience gratuity.reload() self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) - #additional salary creation (Pay via salary slip) + # additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) def test_check_gratuity_amount_based_on_all_previous_slabs(self): @@ -73,13 +82,19 @@ class TestGratuity(unittest.TestCase): rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") set_mode_of_payment_account() - gratuity = create_gratuity(expense_account = 'Payment Account - _TC', mode_of_payment='Cash', employee=employee) + gratuity = create_gratuity( + expense_account="Payment Account - _TC", mode_of_payment="Cash", employee=employee + ) - #work experience calculation - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) - employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days + # work experience calculation + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) + employee_total_workings_days = ( + get_datetime(relieving_date) - get_datetime(date_of_joining) + ).days - experience = employee_total_workings_days/rule.total_working_days_per_year + experience = employee_total_workings_days / rule.total_working_days_per_year gratuity.reload() @@ -87,29 +102,32 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) - #amount Calculation - component_amount = frappe.get_all("Salary Detail", - filters={ - "docstatus": 1, - 'parent': sal_slip, - "parentfield": "earnings", - 'salary_component': "Basic Salary" - }, - fields=["amount"]) + # amount Calculation + component_amount = frappe.get_all( + "Salary Detail", + filters={ + "docstatus": 1, + "parent": sal_slip, + "parentfield": "earnings", + "salary_component": "Basic Salary", + }, + fields=["amount"], + ) - ''' range | Fraction + """ range | Fraction 0-1 | 0 1-5 | 0.7 5-0 | 1 - ''' + """ - gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount + gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount gratuity.reload() self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) self.assertEqual(gratuity.status, "Unpaid") from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + pay_entry = get_payment_entry("Gratuity", gratuity.name) pay_entry.reference_no = "123467" pay_entry.reference_date = getdate() @@ -118,26 +136,26 @@ class TestGratuity(unittest.TestCase): gratuity.reload() self.assertEqual(gratuity.status, "Paid") - self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2)) + self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2)) def tearDown(self): frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") + def get_gratuity_rule(name): rule = frappe.db.exists("Gratuity Rule", name) if not rule: create_gratuity_rule() rule = frappe.get_doc("Gratuity Rule", name) rule.applicable_earnings_component = [] - rule.append("applicable_earnings_component", { - "salary_component": "Basic Salary" - }) + rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"}) rule.save() rule.reload() return rule + def create_gratuity(**args): if args: args = frappe._dict(args) @@ -150,15 +168,16 @@ def create_gratuity(**args): gratuity.payroll_date = getdate() gratuity.salary_component = "Performance Bonus" else: - gratuity.expense_account = args.expense_account or 'Payment Account - _TC' + gratuity.expense_account = args.expense_account or "Payment Account - _TC" gratuity.payable_account = args.payable_account or get_payable_account("_Test Company") - gratuity.mode_of_payment = args.mode_of_payment or 'Cash' + gratuity.mode_of_payment = args.mode_of_payment or "Cash" gratuity.save() gratuity.submit() return gratuity + def set_mode_of_payment_account(): if not frappe.db.exists("Account", "Payment Account - _TC"): mode_of_payment = create_account() @@ -166,14 +185,15 @@ def set_mode_of_payment_account(): mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") mode_of_payment.accounts = [] - mode_of_payment.append("accounts", { - "company": "_Test Company", - "default_account": "_Test Bank - _TC" - }) + mode_of_payment.append( + "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} + ) mode_of_payment.save() + def create_account(): - return frappe.get_doc({ + return frappe.get_doc( + { "doctype": "Account", "company": "_Test Company", "account_name": "Payment Account", @@ -182,13 +202,15 @@ def create_account(): "currency": "INR", "parent_account": "Bank Accounts - _TC", "account_type": "Bank", - }).insert(ignore_permissions=True) + } + ).insert(ignore_permissions=True) + def create_employee_and_get_last_salary_slip(): - employee = make_employee("test_employee@salary.com", company='_Test Company') + employee = make_employee("test_employee@salary.com", company="_Test Company") frappe.db.set_value("Employee", employee, "relieving_date", getdate()) - frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), - (6*365))) - if not frappe.db.exists("Salary Slip", {"employee":employee}): + frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365))) + if not frappe.db.exists("Salary Slip", {"employee": employee}): salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly") salary_slip.submit() salary_slip = salary_slip.name @@ -197,7 +219,10 @@ def create_employee_and_get_last_salary_slip(): if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"): from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list + make_holiday_list() - frappe.db.set_value("Company", '_Test Company', "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List" + ) return employee, salary_slip diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py index d30cfc64848..5cde79a1627 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -8,25 +8,34 @@ from frappe.model.document import Document class GratuityRule(Document): - def validate(self): for current_slab in self.gratuity_rule_slabs: if (current_slab.from_year > current_slab.to_year) and current_slab.to_year != 0: - frappe.throw(_("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx)) + frappe.throw( + _("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx) + ) + + if ( + current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1 + ): + frappe.throw( + _("You can not define multiple slabs if you have a slab with no lower and upper limits.") + ) - if current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1: - frappe.throw(_("You can not define multiple slabs if you have a slab with no lower and upper limits.")) def get_gratuity_rule(name, slabs, **args): args = frappe._dict(args) rule = frappe.new_doc("Gratuity Rule") rule.name = name - rule.calculate_gratuity_amount_based_on = args.calculate_gratuity_amount_based_on or "Current Slab" - rule.work_experience_calculation_method = args.work_experience_calculation_method or "Take Exact Completed Years" + rule.calculate_gratuity_amount_based_on = ( + args.calculate_gratuity_amount_based_on or "Current Slab" + ) + rule.work_experience_calculation_method = ( + args.work_experience_calculation_method or "Take Exact Completed Years" + ) rule.minimum_year_for_gratuity = 1 - for slab in slabs: slab = frappe._dict(slab) rule.append("gratuity_rule_slabs", slab) diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py index 15e15d13620..fa5a9dedd35 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'gratuity_rule', - 'transactions': [ - { - 'label': _('Gratuity'), - 'items': ['Gratuity'] - } - ] + "fieldname": "gratuity_rule", + "transactions": [{"label": _("Gratuity"), "items": ["Gratuity"]}], } diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index 040b2c89353..e62d61f4c2f 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -4,7 +4,7 @@ from frappe.model.document import Document -#import frappe +# import frappe import erpnext diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 579c7b2f504..60d38f4ca49 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -28,11 +28,11 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class PayrollEntry(Document): def onload(self): - if not self.docstatus==1 or self.salary_slips_submitted: + if not self.docstatus == 1 or self.salary_slips_submitted: return # check if salary slips were manually submitted - entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) + entries = frappe.db.count("Salary Slip", {"payroll_entry": self.name, "docstatus": 1}, ["name"]) if cint(entries) == len(self.employees): self.set_onload("submitted_ss", True) @@ -51,33 +51,51 @@ class PayrollEntry(Document): def validate_employee_details(self): emp_with_sal_slip = [] for employee_details in self.employees: - if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + if frappe.db.exists( + "Salary Slip", + { + "employee": employee_details.employee, + "start_date": self.start_date, + "end_date": self.end_date, + "docstatus": 1, + }, + ): emp_with_sal_slip.append(employee_details.employee) if len(emp_with_sal_slip): frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) def on_cancel(self): - frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` - where payroll_entry=%s """, (self.name))) + frappe.delete_doc( + "Salary Slip", + frappe.db.sql_list( + """select name from `tabSalary Slip` + where payroll_entry=%s """, + (self.name), + ), + ) self.db_set("salary_slips_created", 0) self.db_set("salary_slips_submitted", 0) def get_emp_list(self): """ - Returns list of active employees based on selected criteria - and for which salary structure exists + Returns list of active employees based on selected criteria + and for which salary structure exists """ self.check_mandatory() filters = self.make_filters() cond = get_filter_condition(filters) cond += get_joining_relieving_condition(self.start_date, self.end_date) - condition = '' + condition = "" if self.payroll_frequency: - condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency} + condition = """and payroll_frequency = '%(payroll_frequency)s'""" % { + "payroll_frequency": self.payroll_frequency + } - sal_struct = get_sal_struct(self.company, self.currency, self.salary_slip_based_on_timesheet, condition) + sal_struct = get_sal_struct( + self.company, self.currency, self.salary_slip_based_on_timesheet, condition + ) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " @@ -88,20 +106,25 @@ class PayrollEntry(Document): def make_filters(self): filters = frappe._dict() - filters['company'] = self.company - filters['branch'] = self.branch - filters['department'] = self.department - filters['designation'] = self.designation + filters["company"] = self.company + filters["branch"] = self.branch + filters["department"] = self.department + filters["designation"] = self.designation return filters @frappe.whitelist() def fill_employee_details(self): - self.set('employees', []) + self.set("employees", []) employees = self.get_emp_list() if not employees: - error_msg = _("No employees found for the mentioned criteria:
    Company: {0}
    Currency: {1}
    Payroll Payable Account: {2}").format( - frappe.bold(self.company), frappe.bold(self.currency), frappe.bold(self.payroll_payable_account)) + error_msg = _( + "No employees found for the mentioned criteria:
    Company: {0}
    Currency: {1}
    Payroll Payable Account: {2}" + ).format( + frappe.bold(self.company), + frappe.bold(self.currency), + frappe.bold(self.payroll_payable_account), + ) if self.branch: error_msg += "
    " + _("Branch: {0}").format(frappe.bold(self.branch)) if self.department: @@ -115,38 +138,40 @@ class PayrollEntry(Document): frappe.throw(error_msg, title=_("No employees found")) for d in employees: - self.append('employees', d) + self.append("employees", d) self.number_of_employees = len(self.employees) if self.validate_attendance: return self.validate_employee_attendance() def check_mandatory(self): - for fieldname in ['company', 'start_date', 'end_date']: + for fieldname in ["company", "start_date", "end_date"]: if not self.get(fieldname): frappe.throw(_("Please set {0}").format(self.meta.get_label(fieldname))) @frappe.whitelist() def create_salary_slips(self): """ - Creates salary slip for selected employees if already not created + Creates salary slip for selected employees if already not created """ - self.check_permission('write') + self.check_permission("write") employees = [emp.employee for emp in self.employees] if employees: - args = frappe._dict({ - "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, - "payroll_frequency": self.payroll_frequency, - "start_date": self.start_date, - "end_date": self.end_date, - "company": self.company, - "posting_date": self.posting_date, - "deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits, - "deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof, - "payroll_entry": self.name, - "exchange_rate": self.exchange_rate, - "currency": self.currency - }) + args = frappe._dict( + { + "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, + "payroll_frequency": self.payroll_frequency, + "start_date": self.start_date, + "end_date": self.end_date, + "company": self.company, + "posting_date": self.posting_date, + "deduct_tax_for_unclaimed_employee_benefits": self.deduct_tax_for_unclaimed_employee_benefits, + "deduct_tax_for_unsubmitted_tax_exemption_proof": self.deduct_tax_for_unsubmitted_tax_exemption_proof, + "payroll_entry": self.name, + "exchange_rate": self.exchange_rate, + "currency": self.currency, + } + ) if len(employees) > 30: frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args) else: @@ -156,22 +181,28 @@ class PayrollEntry(Document): def get_sal_slip_list(self, ss_status, as_dict=False): """ - Returns list of salary slips based on selected criteria + Returns list of salary slips based on selected criteria """ - ss_list = frappe.db.sql(""" + ss_list = frappe.db.sql( + """ select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1 where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s - """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict) + """, + (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), + as_dict=as_dict, + ) return ss_list @frappe.whitelist() def submit_salary_slips(self): - self.check_permission('write') + self.check_permission("write") ss_list = self.get_sal_slip_list(ss_status=0) if len(ss_list) > 30: - frappe.enqueue(submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list) + frappe.enqueue( + submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list + ) else: submit_salary_slips_for_employees(self, ss_list, publish_progress=False) @@ -181,44 +212,51 @@ class PayrollEntry(Document): ss.email_salary_slip() def get_salary_component_account(self, salary_component): - account = frappe.db.get_value("Salary Component Account", - {"parent": salary_component, "company": self.company}, "account") + account = frappe.db.get_value( + "Salary Component Account", {"parent": salary_component, "company": self.company}, "account" + ) if not account: - frappe.throw(_("Please set account in Salary Component {0}") - .format(salary_component)) + frappe.throw(_("Please set account in Salary Component {0}").format(salary_component)) return account def get_salary_components(self, component_type): - salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True) + salary_slips = self.get_sal_slip_list(ss_status=1, as_dict=True) if salary_slips: - salary_components = frappe.db.sql(""" + salary_components = frappe.db.sql( + """ select ssd.salary_component, ssd.amount, ssd.parentfield, ss.payroll_cost_center from `tabSalary Slip` ss, `tabSalary Detail` ssd where ss.name = ssd.parent and ssd.parentfield = '%s' and ss.name in (%s) - """ % (component_type, ', '.join(['%s']*len(salary_slips))), - tuple([d.name for d in salary_slips]), as_dict=True) + """ + % (component_type, ", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=True, + ) return salary_components - def get_salary_component_total(self, component_type = None): + def get_salary_component_total(self, component_type=None): salary_components = self.get_salary_components(component_type) if salary_components: component_dict = {} for item in salary_components: add_component_to_accrual_jv_entry = True if component_type == "earnings": - is_flexible_benefit, only_tax_impact = frappe.db.get_value("Salary Component", item['salary_component'], ['is_flexible_benefit', 'only_tax_impact']) - if is_flexible_benefit == 1 and only_tax_impact ==1: + is_flexible_benefit, only_tax_impact = frappe.db.get_value( + "Salary Component", item["salary_component"], ["is_flexible_benefit", "only_tax_impact"] + ) + if is_flexible_benefit == 1 and only_tax_impact == 1: add_component_to_accrual_jv_entry = False if add_component_to_accrual_jv_entry: - component_dict[(item.salary_component, item.payroll_cost_center)] \ - = component_dict.get((item.salary_component, item.payroll_cost_center), 0) + flt(item.amount) - account_details = self.get_account(component_dict = component_dict) + component_dict[(item.salary_component, item.payroll_cost_center)] = component_dict.get( + (item.salary_component, item.payroll_cost_center), 0 + ) + flt(item.amount) + account_details = self.get_account(component_dict=component_dict) return account_details - def get_account(self, component_dict = None): + def get_account(self, component_dict=None): account_dict = {} for key, amount in component_dict.items(): account = self.get_salary_component_account(key[0]) @@ -227,8 +265,8 @@ class PayrollEntry(Document): def make_accrual_jv_entry(self): self.check_permission("write") - earnings = self.get_salary_component_total(component_type = "earnings") or {} - deductions = self.get_salary_component_total(component_type = "deductions") or {} + earnings = self.get_salary_component_total(component_type="earnings") or {} + deductions = self.get_salary_component_total(component_type="deductions") or {} payroll_payable_account = self.payroll_payable_account jv_name = "" precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") @@ -236,8 +274,9 @@ class PayrollEntry(Document): if earnings or deductions: journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Journal Entry" - journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\ - .format(self.start_date, self.end_date) + journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}").format( + self.start_date, self.end_date + ) journal_entry.company = self.company journal_entry.posting_date = self.posting_date accounting_dimensions = get_accounting_dimensions() or [] @@ -250,36 +289,57 @@ class PayrollEntry(Document): # Earnings for acc_cc, amount in earnings.items(): - exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry( + acc_cc[0], amount, company_currency, currencies + ) payable_amount += flt(amount, precision) - accounts.append(self.update_accounting_dimensions({ - "account": acc_cc[0], - "debit_in_account_currency": flt(amt, precision), - "exchange_rate": flt(exchange_rate), - "cost_center": acc_cc[1] or self.cost_center, - "project": self.project - }, accounting_dimensions)) + accounts.append( + self.update_accounting_dimensions( + { + "account": acc_cc[0], + "debit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), + "cost_center": acc_cc[1] or self.cost_center, + "project": self.project, + }, + accounting_dimensions, + ) + ) # Deductions for acc_cc, amount in deductions.items(): - exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies) + exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry( + acc_cc[0], amount, company_currency, currencies + ) payable_amount -= flt(amount, precision) - accounts.append(self.update_accounting_dimensions({ - "account": acc_cc[0], - "credit_in_account_currency": flt(amt, precision), - "exchange_rate": flt(exchange_rate), - "cost_center": acc_cc[1] or self.cost_center, - "project": self.project - }, accounting_dimensions)) + accounts.append( + self.update_accounting_dimensions( + { + "account": acc_cc[0], + "credit_in_account_currency": flt(amt, precision), + "exchange_rate": flt(exchange_rate), + "cost_center": acc_cc[1] or self.cost_center, + "project": self.project, + }, + accounting_dimensions, + ) + ) # Payable amount - exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies) - accounts.append(self.update_accounting_dimensions({ - "account": payroll_payable_account, - "credit_in_account_currency": flt(payable_amt, precision), - "exchange_rate": flt(exchange_rate), - "cost_center": self.cost_center - }, accounting_dimensions)) + exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry( + payroll_payable_account, payable_amount, company_currency, currencies + ) + accounts.append( + self.update_accounting_dimensions( + { + "account": payroll_payable_account, + "credit_in_account_currency": flt(payable_amt, precision), + "exchange_rate": flt(exchange_rate), + "cost_center": self.cost_center, + }, + accounting_dimensions, + ) + ) journal_entry.set("accounts", accounts) if len(currencies) > 1: @@ -291,7 +351,7 @@ class PayrollEntry(Document): try: journal_entry.submit() jv_name = journal_entry.name - self.update_salary_slip_status(jv_name = jv_name) + self.update_salary_slip_status(jv_name=jv_name) except Exception as e: if type(e) in (str, list, tuple): frappe.msgprint(e) @@ -305,10 +365,12 @@ class PayrollEntry(Document): return row - def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies): + def get_amount_and_exchange_rate_for_journal_entry( + self, account, amount, company_currency, currencies + ): conversion_rate = 1 exchange_rate = self.exchange_rate - account_currency = frappe.db.get_value('Account', account, 'account_currency') + account_currency = frappe.db.get_value("Account", account, "account_currency") if account_currency not in currencies: currencies.append(account_currency) if account_currency == company_currency: @@ -319,26 +381,45 @@ class PayrollEntry(Document): @frappe.whitelist() def make_payment_entry(self): - self.check_permission('write') + self.check_permission("write") - salary_slip_name_list = frappe.db.sql(""" select t1.name from `tabSalary Slip` t1 + salary_slip_name_list = frappe.db.sql( + """ select t1.name from `tabSalary Slip` t1 where t1.docstatus = 1 and start_date >= %s and end_date <= %s and t1.payroll_entry = %s - """, (self.start_date, self.end_date, self.name), as_list = True) + """, + (self.start_date, self.end_date, self.name), + as_list=True, + ) if salary_slip_name_list and len(salary_slip_name_list) > 0: salary_slip_total = 0 for salary_slip_name in salary_slip_name_list: salary_slip = frappe.get_doc("Salary Slip", salary_slip_name[0]) for sal_detail in salary_slip.earnings: - is_flexible_benefit, only_tax_impact, creat_separate_je, statistical_component = frappe.db.get_value("Salary Component", sal_detail.salary_component, - ['is_flexible_benefit', 'only_tax_impact', 'create_separate_payment_entry_against_benefit_claim', 'statistical_component']) + ( + is_flexible_benefit, + only_tax_impact, + creat_separate_je, + statistical_component, + ) = frappe.db.get_value( + "Salary Component", + sal_detail.salary_component, + [ + "is_flexible_benefit", + "only_tax_impact", + "create_separate_payment_entry_against_benefit_claim", + "statistical_component", + ], + ) if only_tax_impact != 1 and statistical_component != 1: if is_flexible_benefit == 1 and creat_separate_je == 1: self.create_journal_entry(sal_detail.amount, sal_detail.salary_component) else: salary_slip_total += sal_detail.amount for sal_detail in salary_slip.deductions: - statistical_component = frappe.db.get_value("Salary Component", sal_detail.salary_component, 'statistical_component') + statistical_component = frappe.db.get_value( + "Salary Component", sal_detail.salary_component, "statistical_component" + ) if statistical_component != 1: salary_slip_total -= sal_detail.amount if salary_slip_total > 0: @@ -354,91 +435,114 @@ class PayrollEntry(Document): company_currency = erpnext.get_company_currency(self.company) accounting_dimensions = get_accounting_dimensions() or [] - exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies) - accounts.append(self.update_accounting_dimensions({ - "account": self.payment_account, - "bank_account": self.bank_account, - "credit_in_account_currency": flt(amount, precision), - "exchange_rate": flt(exchange_rate), - }, accounting_dimensions)) + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry( + self.payment_account, je_payment_amount, company_currency, currencies + ) + accounts.append( + self.update_accounting_dimensions( + { + "account": self.payment_account, + "bank_account": self.bank_account, + "credit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + }, + accounting_dimensions, + ) + ) - exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies) - accounts.append(self.update_accounting_dimensions({ - "account": payroll_payable_account, - "debit_in_account_currency": flt(amount, precision), - "exchange_rate": flt(exchange_rate), - "reference_type": self.doctype, - "reference_name": self.name - }, accounting_dimensions)) + exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry( + payroll_payable_account, je_payment_amount, company_currency, currencies + ) + accounts.append( + self.update_accounting_dimensions( + { + "account": payroll_payable_account, + "debit_in_account_currency": flt(amount, precision), + "exchange_rate": flt(exchange_rate), + "reference_type": self.doctype, + "reference_name": self.name, + }, + accounting_dimensions, + ) + ) if len(currencies) > 1: - multi_currency = 1 + multi_currency = 1 - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Bank Entry' - journal_entry.user_remark = _('Payment of {0} from {1} to {2}')\ - .format(user_remark, self.start_date, self.end_date) + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Bank Entry" + journal_entry.user_remark = _("Payment of {0} from {1} to {2}").format( + user_remark, self.start_date, self.end_date + ) journal_entry.company = self.company journal_entry.posting_date = self.posting_date journal_entry.multi_currency = multi_currency journal_entry.set("accounts", accounts) - journal_entry.save(ignore_permissions = True) + journal_entry.save(ignore_permissions=True) - def update_salary_slip_status(self, jv_name = None): + def update_salary_slip_status(self, jv_name=None): ss_list = self.get_sal_slip_list(ss_status=1) for ss in ss_list: - ss_obj = frappe.get_doc("Salary Slip",ss[0]) + ss_obj = frappe.get_doc("Salary Slip", ss[0]) frappe.db.set_value("Salary Slip", ss_obj.name, "journal_entry", jv_name) def set_start_end_dates(self): - self.update(get_start_end_dates(self.payroll_frequency, - self.start_date or self.posting_date, self.company)) + self.update( + get_start_end_dates(self.payroll_frequency, self.start_date or self.posting_date, self.company) + ) @frappe.whitelist() def validate_employee_attendance(self): employees_to_mark_attendance = [] days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0 for employee_detail in self.employees: - employee_joining_date = frappe.db.get_value("Employee", employee_detail.employee, 'date_of_joining') + employee_joining_date = frappe.db.get_value( + "Employee", employee_detail.employee, "date_of_joining" + ) start_date = self.start_date if employee_joining_date > getdate(self.start_date): start_date = employee_joining_date days_holiday = self.get_count_holidays_of_employee(employee_detail.employee, start_date) - days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee, start_date) + days_attendance_marked = self.get_count_employee_attendance( + employee_detail.employee, start_date + ) days_in_payroll = date_diff(self.end_date, start_date) + 1 if days_in_payroll > days_holiday + days_attendance_marked: - employees_to_mark_attendance.append({ - "employee": employee_detail.employee, - "employee_name": employee_detail.employee_name - }) + employees_to_mark_attendance.append( + {"employee": employee_detail.employee, "employee_name": employee_detail.employee_name} + ) return employees_to_mark_attendance def get_count_holidays_of_employee(self, employee, start_date): holiday_list = get_holiday_list_for_employee(employee) holidays = 0 if holiday_list: - days = frappe.db.sql("""select count(*) from tabHoliday where - parent=%s and holiday_date between %s and %s""", (holiday_list, - start_date, self.end_date)) + days = frappe.db.sql( + """select count(*) from tabHoliday where + parent=%s and holiday_date between %s and %s""", + (holiday_list, start_date, self.end_date), + ) if days and days[0][0]: holidays = days[0][0] return holidays def get_count_employee_attendance(self, employee, start_date): marked_days = 0 - attendances = frappe.get_all("Attendance", - fields = ["count(*)"], - filters = { - "employee": employee, - "attendance_date": ('between', [start_date, self.end_date]) - }, as_list=1) + attendances = frappe.get_all( + "Attendance", + fields=["count(*)"], + filters={"employee": employee, "attendance_date": ("between", [start_date, self.end_date])}, + as_list=1, + ) if attendances and attendances[0][0]: marked_days = attendances[0][0] return marked_days + def get_sal_struct(company, currency, salary_slip_based_on_timesheet, condition): - return frappe.db.sql_list(""" + return frappe.db.sql_list( + """ select name from `tabSalary Structure` where @@ -447,26 +551,40 @@ def get_sal_struct(company, currency, salary_slip_based_on_timesheet, condition) and company = %(company)s and currency = %(currency)s and ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s - {condition}""".format(condition=condition), - {"company": company, "currency": currency, "salary_slip_based_on_timesheet": salary_slip_based_on_timesheet}) + {condition}""".format( + condition=condition + ), + { + "company": company, + "currency": currency, + "salary_slip_based_on_timesheet": salary_slip_based_on_timesheet, + }, + ) + def get_filter_condition(filters): - cond = '' - for f in ['company', 'branch', 'department', 'designation']: + cond = "" + for f in ["company", "branch", "department", "designation"]: if filters.get(f): cond += " and t1." + f + " = " + frappe.db.escape(filters.get(f)) return cond + def get_joining_relieving_condition(start_date, end_date): cond = """ and ifnull(t1.date_of_joining, '0000-00-00') <= '%(end_date)s' and ifnull(t1.relieving_date, '2199-12-31') >= '%(start_date)s' - """ % {"start_date": start_date, "end_date": end_date} + """ % { + "start_date": start_date, + "end_date": end_date, + } return cond + def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): - return frappe.db.sql(""" + return frappe.db.sql( + """ select distinct t1.name as employee, t1.employee_name, t1.department, t1.designation from @@ -476,19 +594,37 @@ def get_emp_list(sal_struct, cond, end_date, payroll_payable_account): and t2.docstatus = 1 and t1.status != 'Inactive' %s order by t2.from_date desc - """ % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True) + """ + % cond, + { + "sal_struct": tuple(sal_struct), + "from_date": end_date, + "payroll_payable_account": payroll_payable_account, + }, + as_dict=True, + ) + def remove_payrolled_employees(emp_list, start_date, end_date): new_emp_list = [] for employee_details in emp_list: - if not frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}): + if not frappe.db.exists( + "Salary Slip", + { + "employee": employee_details.employee, + "start_date": start_date, + "end_date": end_date, + "docstatus": 1, + }, + ): new_emp_list.append(employee_details) return new_emp_list + @frappe.whitelist() def get_start_end_dates(payroll_frequency, start_date=None, company=None): - '''Returns dict of start and end dates for given payroll frequency based on start_date''' + """Returns dict of start and end dates for given payroll frequency based on start_date""" if payroll_frequency == "Monthly" or payroll_frequency == "Bimonthly" or payroll_frequency == "": fiscal_year = get_fiscal_year(start_date, company=company)[0] @@ -496,14 +632,14 @@ def get_start_end_dates(payroll_frequency, start_date=None, company=None): m = get_month_details(fiscal_year, month) if payroll_frequency == "Bimonthly": if getdate(start_date).day <= 15: - start_date = m['month_start_date'] - end_date = m['month_mid_end_date'] + start_date = m["month_start_date"] + end_date = m["month_mid_end_date"] else: - start_date = m['month_mid_start_date'] - end_date = m['month_end_date'] + start_date = m["month_mid_start_date"] + end_date = m["month_end_date"] else: - start_date = m['month_start_date'] - end_date = m['month_end_date'] + start_date = m["month_start_date"] + end_date = m["month_end_date"] if payroll_frequency == "Weekly": end_date = add_days(start_date, 6) @@ -514,16 +650,15 @@ def get_start_end_dates(payroll_frequency, start_date=None, company=None): if payroll_frequency == "Daily": end_date = start_date - return frappe._dict({ - 'start_date': start_date, 'end_date': end_date - }) + return frappe._dict({"start_date": start_date, "end_date": end_date}) + def get_frequency_kwargs(frequency_name): frequency_dict = { - 'monthly': {'months': 1}, - 'fortnightly': {'days': 14}, - 'weekly': {'days': 7}, - 'daily': {'days': 1} + "monthly": {"months": 1}, + "fortnightly": {"days": 14}, + "weekly": {"days": 7}, + "daily": {"days": 1}, } return frequency_dict.get(frequency_name) @@ -531,16 +666,18 @@ def get_frequency_kwargs(frequency_name): @frappe.whitelist() def get_end_date(start_date, frequency): start_date = getdate(start_date) - frequency = frequency.lower() if frequency else 'monthly' - kwargs = get_frequency_kwargs(frequency) if frequency != 'bimonthly' else get_frequency_kwargs('monthly') + frequency = frequency.lower() if frequency else "monthly" + kwargs = ( + get_frequency_kwargs(frequency) if frequency != "bimonthly" else get_frequency_kwargs("monthly") + ) # weekly, fortnightly and daily intervals have fixed days so no problems end_date = add_to_date(start_date, **kwargs) - relativedelta(days=1) - if frequency != 'bimonthly': + if frequency != "bimonthly": return dict(end_date=end_date.strftime(DATE_FORMAT)) else: - return dict(end_date='') + return dict(end_date="") def get_month_details(year, month): @@ -548,32 +685,36 @@ def get_month_details(year, month): if ysd: import calendar import datetime - diff_mnt = cint(month)-cint(ysd.month) - if diff_mnt<0: - diff_mnt = 12-int(ysd.month)+cint(month) - msd = ysd + relativedelta(months=diff_mnt) # month start date - month_days = cint(calendar.monthrange(cint(msd.year) ,cint(month))[1]) # days in month - mid_start = datetime.date(msd.year, cint(month), 16) # month mid start date - mid_end = datetime.date(msd.year, cint(month), 15) # month mid end date - med = datetime.date(msd.year, cint(month), month_days) # month end date - return frappe._dict({ - 'year': msd.year, - 'month_start_date': msd, - 'month_end_date': med, - 'month_mid_start_date': mid_start, - 'month_mid_end_date': mid_end, - 'month_days': month_days - }) + + diff_mnt = cint(month) - cint(ysd.month) + if diff_mnt < 0: + diff_mnt = 12 - int(ysd.month) + cint(month) + msd = ysd + relativedelta(months=diff_mnt) # month start date + month_days = cint(calendar.monthrange(cint(msd.year), cint(month))[1]) # days in month + mid_start = datetime.date(msd.year, cint(month), 16) # month mid start date + mid_end = datetime.date(msd.year, cint(month), 15) # month mid end date + med = datetime.date(msd.year, cint(month), month_days) # month end date + return frappe._dict( + { + "year": msd.year, + "month_start_date": msd, + "month_end_date": med, + "month_mid_start_date": mid_start, + "month_mid_end_date": mid_end, + "month_days": month_days, + } + ) else: frappe.throw(_("Fiscal Year {0} not found").format(year)) + def get_payroll_entry_bank_entries(payroll_entry_name): journal_entries = frappe.db.sql( - 'select name from `tabJournal Entry Account` ' + "select name from `tabJournal Entry Account` " 'where reference_type="Payroll Entry" ' - 'and reference_name=%s and docstatus=1', + "and reference_name=%s and docstatus=1", payroll_entry_name, - as_dict=1 + as_dict=1, ) return journal_entries @@ -583,26 +724,26 @@ def get_payroll_entry_bank_entries(payroll_entry_name): def payroll_entry_has_bank_entries(name): response = {} bank_entries = get_payroll_entry_bank_entries(name) - response['submitted'] = 1 if bank_entries else 0 + response["submitted"] = 1 if bank_entries else 0 return response + def create_salary_slips_for_employees(employees, args, publish_progress=True): salary_slips_exists_for = get_existing_salary_slips(employees, args) - count=0 + count = 0 salary_slips_not_created = [] for emp in employees: if emp not in salary_slips_exists_for: - args.update({ - "doctype": "Salary Slip", - "employee": emp - }) + args.update({"doctype": "Salary Slip", "employee": emp}) ss = frappe.get_doc(args) ss.insert() - count+=1 + count += 1 if publish_progress: - frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), - title = _("Creating Salary Slips...")) + frappe.publish_progress( + count * 100 / len(set(employees) - set(salary_slips_exists_for)), + title=_("Creating Salary Slips..."), + ) else: salary_slips_not_created.append(emp) @@ -612,17 +753,27 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): payroll_entry.notify_update() if salary_slips_not_created: - frappe.msgprint(_("Salary Slips already exists for employees {}, and will not be processed by this payroll.") - .format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))) , title=_("Message"), indicator="orange") + frappe.msgprint( + _( + "Salary Slips already exists for employees {}, and will not be processed by this payroll." + ).format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))), + title=_("Message"), + indicator="orange", + ) + def get_existing_salary_slips(employees, args): - return frappe.db.sql_list(""" + return frappe.db.sql_list( + """ select distinct employee from `tabSalary Slip` where docstatus!= 2 and company = %s and payroll_entry = %s and start_date >= %s and end_date <= %s and employee in (%s) - """ % ('%s', '%s', '%s', '%s', ', '.join(['%s']*len(employees))), - [args.company, args.payroll_entry, args.start_date, args.end_date] + employees) + """ + % ("%s", "%s", "%s", "%s", ", ".join(["%s"] * len(employees))), + [args.company, args.payroll_entry, args.start_date, args.end_date] + employees, + ) + def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True): submitted_ss = [] @@ -631,8 +782,8 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr count = 0 for ss in salary_slips: - ss_obj = frappe.get_doc("Salary Slip",ss[0]) - if ss_obj.net_pay<0: + ss_obj = frappe.get_doc("Salary Slip", ss[0]) + if ss_obj.net_pay < 0: not_submitted_ss.append(ss[0]) else: try: @@ -643,11 +794,12 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr count += 1 if publish_progress: - frappe.publish_progress(count*100/len(salary_slips), title = _("Submitting Salary Slips...")) + frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips...")) if submitted_ss: payroll_entry.make_accrual_jv_entry() - frappe.msgprint(_("Salary Slip submitted for period from {0} to {1}") - .format(ss_obj.start_date, ss_obj.end_date)) + frappe.msgprint( + _("Salary Slip submitted for period from {0} to {1}").format(ss_obj.start_date, ss_obj.end_date) + ) payroll_entry.email_salary_slip(submitted_ss) @@ -655,33 +807,44 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr payroll_entry.notify_update() if not submitted_ss and not not_submitted_ss: - frappe.msgprint(_("No salary slip found to submit for the above selected criteria OR salary slip already submitted")) + frappe.msgprint( + _( + "No salary slip found to submit for the above selected criteria OR salary slip already submitted" + ) + ) if not_submitted_ss: frappe.msgprint(_("Could not submit some Salary Slips")) frappe.flags.via_payroll_entry = False + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ select name from `tabPayroll Entry` where `{key}` LIKE %(txt)s and name not in (select reference_name from `tabJournal Entry Account` where reference_type="Payroll Entry") - order by name limit %(start)s, %(page_len)s""" - .format(key=searchfield), { - 'txt': "%%%s%%" % txt, - 'start': start, 'page_len': page_len - }) + order by name limit %(start)s, %(page_len)s""".format( + key=searchfield + ), + {"txt": "%%%s%%" % txt, "start": start, "page_len": page_len}, + ) + def get_employee_list(filters): cond = get_filter_condition(filters) cond += get_joining_relieving_condition(filters.start_date, filters.end_date) - condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": filters.payroll_frequency} - sal_struct = get_sal_struct(filters.company, filters.currency, filters.salary_slip_based_on_timesheet, condition) + condition = """and payroll_frequency = '%(payroll_frequency)s'""" % { + "payroll_frequency": filters.payroll_frequency + } + sal_struct = get_sal_struct( + filters.company, filters.currency, filters.salary_slip_based_on_timesheet, condition + ) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " @@ -692,34 +855,38 @@ def get_employee_list(filters): return [] + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): filters = frappe._dict(filters) conditions = [] include_employees = [] - emp_cond = '' + emp_cond = "" if not filters.payroll_frequency: - frappe.throw(_('Select Payroll Frequency.')) + frappe.throw(_("Select Payroll Frequency.")) if filters.start_date and filters.end_date: employee_list = get_employee_list(filters) - emp = filters.get('employees') or [] - include_employees = [employee.employee for employee in employee_list if employee.employee not in emp] - filters.pop('start_date') - filters.pop('end_date') - filters.pop('salary_slip_based_on_timesheet') - filters.pop('payroll_frequency') - filters.pop('payroll_payable_account') - filters.pop('currency') + emp = filters.get("employees") or [] + include_employees = [ + employee.employee for employee in employee_list if employee.employee not in emp + ] + filters.pop("start_date") + filters.pop("end_date") + filters.pop("salary_slip_based_on_timesheet") + filters.pop("payroll_frequency") + filters.pop("payroll_payable_account") + filters.pop("currency") if filters.employees is not None: - filters.pop('employees') + filters.pop("employees") if include_employees: - emp_cond += 'and employee in %(include_employees)s' + emp_cond += "and employee in %(include_employees)s" - return frappe.db.sql("""select name, employee_name from `tabEmployee` + return frappe.db.sql( + """select name, employee_name from `tabEmployee` where status = 'Active' and docstatus < 2 and ({key} like %(txt)s @@ -731,14 +898,19 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), idx desc, name, employee_name - limit %(start)s, %(page_len)s""".format(**{ - 'key': searchfield, - 'fcond': get_filters_cond(doctype, filters, conditions), - 'mcond': get_match_cond(doctype), - 'emp_cond': emp_cond - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len, - 'include_employees': include_employees}) + limit %(start)s, %(page_len)s""".format( + **{ + "key": searchfield, + "fcond": get_filters_cond(doctype, filters, conditions), + "mcond": get_match_cond(doctype), + "emp_cond": emp_cond, + } + ), + { + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_len": page_len, + "include_employees": include_employees, + }, + ) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py index 138fed68f4c..eb93d688f92 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry_dashboard.py @@ -1,15 +1,9 @@ - - def get_data(): return { - 'fieldname': 'payroll_entry', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', + "fieldname": "payroll_entry", + "non_standard_fieldnames": { + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", }, - 'transactions': [ - { - 'items': ['Salary Slip', 'Journal Entry'] - } - ] + "transactions": [{"items": ["Salary Slip", "Journal Entry"]}], } diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 5eab1424811..c0932c951bb 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -32,139 +32,224 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( make_salary_structure, ) -test_dependencies = ['Holiday List'] +test_dependencies = ["Holiday List"] + class TestPayrollEntry(unittest.TestCase): @classmethod def setUpClass(cls): - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List') + frappe.db.set_value( + "Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List" + ) def setUp(self): - for dt in ["Salary Slip", "Salary Component", "Salary Component Account", - "Payroll Entry", "Salary Structure", "Salary Structure Assignment", "Payroll Employee Detail", "Additional Salary"]: - frappe.db.sql("delete from `tab%s`" % dt) + for dt in [ + "Salary Slip", + "Salary Component", + "Salary Component Account", + "Payroll Entry", + "Salary Structure", + "Salary Structure Assignment", + "Payroll Employee Detail", + "Additional Salary", + ]: + frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"]) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) - def test_payroll_entry(self): # pylint: disable=no-self-use + def test_payroll_entry(self): # pylint: disable=no-self-use company = erpnext.get_default_company() - for data in frappe.get_all('Salary Component', fields = ["name"]): - if not frappe.db.get_value('Salary Component Account', - {'parent': data.name, 'company': company}, 'name'): + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": company}, "name" + ): get_salary_component_account(data.name) - employee = frappe.db.get_value("Employee", {'company': company}) - company_doc = frappe.get_doc('Company', company) - make_salary_structure("_Test Salary Structure", "Monthly", employee, company=company, currency=company_doc.default_currency) - dates = get_start_end_dates('Monthly', nowdate()) - if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, - currency=company_doc.default_currency) + employee = frappe.db.get_value("Employee", {"company": company}) + company_doc = frappe.get_doc("Company", company) + make_salary_structure( + "_Test Salary Structure", + "Monthly", + employee, + company=company, + currency=company_doc.default_currency, + ) + dates = get_start_end_dates("Monthly", nowdate()) + if not frappe.db.get_value( + "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date} + ): + make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + ) - def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use + def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use company = erpnext.get_default_company() employee = make_employee("test_muti_currency_employee@payroll.com", company=company) - for data in frappe.get_all('Salary Component', fields = ["name"]): - if not frappe.db.get_value('Salary Component Account', - {'parent': data.name, 'company': company}, 'name'): + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": company}, "name" + ): get_salary_component_account(data.name) - company_doc = frappe.get_doc('Company', company) - salary_structure = make_salary_structure("_Test Multi Currency Salary Structure", "Monthly", company=company, currency='USD') - create_salary_structure_assignment(employee, salary_structure.name, company=company, currency='USD') - frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""",(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"}))) - salary_slip = get_salary_slip("test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure") - dates = get_start_end_dates('Monthly', nowdate()) - payroll_entry = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, - payable_account=company_doc.default_payroll_payable_account, currency='USD', exchange_rate=70) + company_doc = frappe.get_doc("Company", company) + salary_structure = make_salary_structure( + "_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD" + ) + create_salary_structure_assignment( + employee, salary_structure.name, company=company, currency="USD" + ) + frappe.db.sql( + """delete from `tabSalary Slip` where employee=%s""", + (frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})), + ) + salary_slip = get_salary_slip( + "test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure" + ) + dates = get_start_end_dates("Monthly", nowdate()) + payroll_entry = make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency="USD", + exchange_rate=70, + ) payroll_entry.make_payment_entry() salary_slip.load_from_db() payroll_je = salary_slip.journal_entry if payroll_je: - payroll_je_doc = frappe.get_doc('Journal Entry', payroll_je) + payroll_je_doc = frappe.get_doc("Journal Entry", payroll_je) self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit) self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit) - payment_entry = frappe.db.sql(''' + payment_entry = frappe.db.sql( + """ Select ifnull(sum(je.total_debit),0) as total_debit, ifnull(sum(je.total_credit),0) as total_credit from `tabJournal Entry` je, `tabJournal Entry Account` jea Where je.name = jea.parent And jea.reference_name = %s - ''', (payroll_entry.name), as_dict=1) + """, + (payroll_entry.name), + as_dict=1, + ) self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit) self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) - def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use - for data in frappe.get_all('Salary Component', fields = ["name"]): - if not frappe.db.get_value('Salary Component Account', - {'parent': data.name, 'company': "_Test Company"}, 'name'): + def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name" + ): get_salary_component_account(data.name) - if not frappe.db.exists('Department', "cc - _TC"): - frappe.get_doc({ - 'doctype': 'Department', - 'department_name': "cc", - "company": "_Test Company" - }).insert() + if not frappe.db.exists("Department", "cc - _TC"): + frappe.get_doc( + {"doctype": "Department", "department_name": "cc", "company": "_Test Company"} + ).insert() frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """) frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """) frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """) frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """) - employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", - department="cc - _TC", company="_Test Company") - employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC", - department="cc - _TC", company="_Test Company") + employee1 = make_employee( + "test_employee1@example.com", + payroll_cost_center="_Test Cost Center - _TC", + department="cc - _TC", + company="_Test Company", + ) + employee2 = make_employee( + "test_employee2@example.com", + payroll_cost_center="_Test Cost Center 2 - _TC", + department="cc - _TC", + company="_Test Company", + ) if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): - create_account(account_name="_Test Payroll Payable", - company="_Test Company", parent_account="Current Liabilities - _TC", account_type="Payable") + create_account( + account_name="_Test Payroll Payable", + company="_Test Company", + parent_account="Current Liabilities - _TC", + account_type="Payable", + ) - if not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") or \ - frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": - frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", - "_Test Payroll Payable - _TC") - currency=frappe.db.get_value("Company", "_Test Company", "default_currency") - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) + if ( + not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") + or frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") + != "_Test Payroll Payable - _TC" + ): + frappe.db.set_value( + "Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC" + ) + currency = frappe.db.get_value("Company", "_Test Company", "default_currency") + make_salary_structure( + "_Test Salary Structure 1", + "Monthly", + employee1, + company="_Test Company", + currency=currency, + test_tax=False, + ) + make_salary_structure( + "_Test Salary Structure 2", + "Monthly", + employee2, + company="_Test Company", + currency=currency, + test_tax=False, + ) - dates = get_start_end_dates('Monthly', nowdate()) - if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): - pe = make_payroll_entry(start_date=dates.start_date, end_date=dates.end_date, payable_account="_Test Payroll Payable - _TC", - currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), department="cc - _TC", company="_Test Company", payment_account="Cash - _TC", cost_center="Main - _TC") + dates = get_start_end_dates("Monthly", nowdate()) + if not frappe.db.get_value( + "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date} + ): + pe = make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account="_Test Payroll Payable - _TC", + currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), + department="cc - _TC", + company="_Test Company", + payment_account="Cash - _TC", + cost_center="Main - _TC", + ) je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") - je_entries = frappe.db.sql(""" + je_entries = frappe.db.sql( + """ select account, cost_center, debit, credit from `tabJournal Entry Account` where parent=%s order by account, cost_center - """, je) + """, + je, + ) expected_je = ( - ('_Test Payroll Payable - _TC', 'Main - _TC', 0.0, 155600.0), - ('Salary - _TC', '_Test Cost Center - _TC', 78000.0, 0.0), - ('Salary - _TC', '_Test Cost Center 2 - _TC', 78000.0, 0.0), - ('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 200.0), - ('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 200.0) + ("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0), + ("Salary - _TC", "_Test Cost Center - _TC", 78000.0, 0.0), + ("Salary - _TC", "_Test Cost Center 2 - _TC", 78000.0, 0.0), + ("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 200.0), + ("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 200.0), ) self.assertEqual(je_entries, expected_je) def test_get_end_date(self): - self.assertEqual(get_end_date('2017-01-01', 'monthly'), {'end_date': '2017-01-31'}) - self.assertEqual(get_end_date('2017-02-01', 'monthly'), {'end_date': '2017-02-28'}) - self.assertEqual(get_end_date('2017-02-01', 'fortnightly'), {'end_date': '2017-02-14'}) - self.assertEqual(get_end_date('2017-02-01', 'bimonthly'), {'end_date': ''}) - self.assertEqual(get_end_date('2017-01-01', 'bimonthly'), {'end_date': ''}) - self.assertEqual(get_end_date('2020-02-15', 'bimonthly'), {'end_date': ''}) - self.assertEqual(get_end_date('2017-02-15', 'monthly'), {'end_date': '2017-03-14'}) - self.assertEqual(get_end_date('2017-02-15', 'daily'), {'end_date': '2017-02-15'}) + self.assertEqual(get_end_date("2017-01-01", "monthly"), {"end_date": "2017-01-31"}) + self.assertEqual(get_end_date("2017-02-01", "monthly"), {"end_date": "2017-02-28"}) + self.assertEqual(get_end_date("2017-02-01", "fortnightly"), {"end_date": "2017-02-14"}) + self.assertEqual(get_end_date("2017-02-01", "bimonthly"), {"end_date": ""}) + self.assertEqual(get_end_date("2017-01-01", "bimonthly"), {"end_date": ""}) + self.assertEqual(get_end_date("2020-02-15", "bimonthly"), {"end_date": ""}) + self.assertEqual(get_end_date("2017-02-15", "monthly"), {"end_date": "2017-03-14"}) + self.assertEqual(get_end_date("2017-02-15", "daily"), {"end_date": "2017-02-15"}) def test_loan(self): branch = "Test Employee Branch" @@ -172,63 +257,88 @@ class TestPayrollEntry(unittest.TestCase): company = "_Test Company" holiday_list = make_holiday("test holiday for loan") - company_doc = frappe.get_doc('Company', company) + company_doc = frappe.get_doc("Company", company) if not company_doc.default_payroll_payable_account: - company_doc.default_payroll_payable_account = frappe.db.get_value('Account', - {'company': company, 'root_type': 'Liability', 'account_type': ''}, 'name') + company_doc.default_payroll_payable_account = frappe.db.get_value( + "Account", {"company": company, "root_type": "Liability", "account_type": ""}, "name" + ) company_doc.save() - if not frappe.db.exists('Branch', branch): - frappe.get_doc({ - 'doctype': 'Branch', - 'branch': branch - }).insert() + if not frappe.db.exists("Branch", branch): + frappe.get_doc({"doctype": "Branch", "branch": branch}).insert() - employee_doc = frappe.get_doc('Employee', applicant) + employee_doc = frappe.get_doc("Employee", applicant) employee_doc.branch = branch employee_doc.holiday_list = holiday_list employee_doc.save() salary_structure = "Test Salary Structure for Loan" - make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency) + make_salary_structure( + salary_structure, + "Monthly", + employee=employee_doc.name, + company="_Test Company", + currency=company_doc.default_currency, + ) if not frappe.db.exists("Loan Type", "Car Loan"): create_loan_accounts() - create_loan_type("Car Loan", 500000, 8.4, + create_loan_type( + "Car Loan", + 500000, + 8.4, is_term_loan=1, - mode_of_payment='Cash', - disbursement_account='Disbursement Account - _TC', - payment_account='Payment Account - _TC', - loan_account='Loan Account - _TC', - interest_income_account='Interest Income Account - _TC', - penalty_income_account='Penalty Income Account - _TC') + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + ) - loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) + loan = create_loan( + applicant, + "Car Loan", + 280000, + "Repay Over Number of Periods", + 20, + posting_date=add_months(nowdate(), -1), + ) loan.repay_from_salary = 1 loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + make_loan_disbursement_entry( + loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1) + ) process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - dates = get_start_end_dates('Monthly', nowdate()) - make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account, - currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") + dates = get_start_end_dates("Monthly", nowdate()) + make_payroll_entry( + company="_Test Company", + start_date=dates.start_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + end_date=dates.end_date, + branch=branch, + cost_center="Main - _TC", + payment_account="Cash - _TC", + ) - name = frappe.db.get_value('Salary Slip', - {'posting_date': nowdate(), 'employee': applicant}, 'name') + name = frappe.db.get_value( + "Salary Slip", {"posting_date": nowdate(), "employee": applicant}, "name" + ) - salary_slip = frappe.get_doc('Salary Slip', name) + salary_slip = frappe.get_doc("Salary Slip", name) for row in salary_slip.loans: if row.loan == loan.name: - interest_amount = (280000 * 8.4)/(12*100) + interest_amount = (280000 * 8.4) / (12 * 100) principal_amount = loan.monthly_repayment_amount - interest_amount self.assertEqual(row.interest_amount, interest_amount) self.assertEqual(row.principal_amount, principal_amount) - self.assertEqual(row.total_payment, - interest_amount + principal_amount) + self.assertEqual(row.total_payment, interest_amount + principal_amount) if salary_slip.docstatus == 0: - frappe.delete_doc('Salary Slip', name) + frappe.delete_doc("Salary Slip", name) def make_payroll_entry(**args): @@ -257,17 +367,22 @@ def make_payroll_entry(**args): payroll_entry.save() payroll_entry.create_salary_slips() payroll_entry.submit_salary_slips() - if payroll_entry.get_sal_slip_list(ss_status = 1): + if payroll_entry.get_sal_slip_list(ss_status=1): payroll_entry.make_payment_entry() return payroll_entry + def get_payment_account(): - return frappe.get_value('Account', - {'account_type': 'Cash', 'company': erpnext.get_default_company(),'is_group':0}, "name") + return frappe.get_value( + "Account", + {"account_type": "Cash", "company": erpnext.get_default_company(), "is_group": 0}, + "name", + ) + def make_holiday(holiday_list_name): - if not frappe.db.exists('Holiday List', holiday_list_name): + if not frappe.db.exists("Holiday List", holiday_list_name): current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True) dt = getdate(nowdate()) @@ -275,25 +390,23 @@ def make_holiday(holiday_list_name): republic_day = dt + relativedelta(month=1, day=26, year=dt.year) test_holiday = dt + relativedelta(month=2, day=2, year=dt.year) - frappe.get_doc({ - 'doctype': 'Holiday List', - 'from_date': current_fiscal_year.year_start_date, - 'to_date': current_fiscal_year.year_end_date, - 'holiday_list_name': holiday_list_name, - 'holidays': [{ - 'holiday_date': new_year, - 'description': 'New Year' - }, { - 'holiday_date': republic_day, - 'description': 'Republic Day' - }, { - 'holiday_date': test_holiday, - 'description': 'Test Holiday' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Holiday List", + "from_date": current_fiscal_year.year_start_date, + "to_date": current_fiscal_year.year_end_date, + "holiday_list_name": holiday_list_name, + "holidays": [ + {"holiday_date": new_year, "description": "New Year"}, + {"holiday_date": republic_day, "description": "Republic Day"}, + {"holiday_date": test_holiday, "description": "Test Holiday"}, + ], + } + ).insert() return holiday_list_name + def get_salary_slip(user, period, salary_structure): salary_slip = make_employee_salary_slip(user, period, salary_structure) salary_slip.exchange_rate = 70 diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py index 659ec6de7b6..e1f1cabbc7e 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py @@ -30,71 +30,92 @@ class PayrollPeriod(Document): """ if not self.name: # hack! if name is null, it could cause problems with != - self.name = "New "+self.doctype + self.name = "New " + self.doctype - overlap_doc = frappe.db.sql(query.format(self.doctype),{ + overlap_doc = frappe.db.sql( + query.format(self.doctype), + { "start_date": self.start_date, "end_date": self.end_date, "name": self.name, - "company": self.company - }, as_dict = 1) + "company": self.company, + }, + as_dict=1, + ) if overlap_doc: - msg = _("A {0} exists between {1} and {2} (").format(self.doctype, - formatdate(self.start_date), formatdate(self.end_date)) \ - + """ {1}""".format(self.doctype, overlap_doc[0].name) \ + msg = ( + _("A {0} exists between {1} and {2} (").format( + self.doctype, formatdate(self.start_date), formatdate(self.end_date) + ) + + """ {1}""".format(self.doctype, overlap_doc[0].name) + _(") for {0}").format(self.company) + ) frappe.throw(msg) + def get_payroll_period_days(start_date, end_date, employee, company=None): if not company: company = frappe.db.get_value("Employee", employee, "company") - payroll_period = frappe.db.sql(""" + payroll_period = frappe.db.sql( + """ select name, start_date, end_date from `tabPayroll Period` where company=%(company)s and %(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date - """, { - 'company': company, - 'start_date': start_date, - 'end_date': end_date - }) + """, + {"company": company, "start_date": start_date, "end_date": end_date}, + ) if len(payroll_period) > 0: actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1 working_days = actual_no_of_days - if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")): - holidays = get_holiday_dates_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2])) + if not cint( + frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days") + ): + holidays = get_holiday_dates_for_employee( + employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2]) + ) working_days -= len(holidays) return payroll_period[0][0], working_days, actual_no_of_days return False, False, False + def get_payroll_period(from_date, to_date, company): - payroll_period = frappe.db.sql(""" + payroll_period = frappe.db.sql( + """ select name, start_date, end_date from `tabPayroll Period` where start_date<=%s and end_date>= %s and company=%s - """, (from_date, to_date, company), as_dict=1) + """, + (from_date, to_date, company), + as_dict=1, + ) return payroll_period[0] if payroll_period else None -def get_period_factor(employee, start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days=0): + +def get_period_factor( + employee, start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days=0 +): # TODO if both deduct checked update the factor to make tax consistent period_start, period_end = payroll_period.start_date, payroll_period.end_date - joining_date, relieving_date = frappe.db.get_value("Employee", employee, ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) if getdate(joining_date) > getdate(period_start): period_start = joining_date if relieving_date and getdate(relieving_date) < getdate(period_end): period_end = relieving_date if month_diff(period_end, start_date) > 1: - start_date = add_months(start_date, - (month_diff(period_end, start_date)+1)) + start_date = add_months(start_date, -(month_diff(period_end, start_date) + 1)) total_sub_periods, remaining_sub_periods = 0.0, 0.0 - if payroll_frequency == "Monthly" and not depends_on_payment_days: + if payroll_frequency == "Monthly" and not depends_on_payment_days: total_sub_periods = month_diff(payroll_period.end_date, payroll_period.start_date) remaining_sub_periods = month_diff(period_end, start_date) else: diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py b/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py index eaa67732af4..96632c50085 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): - return { - 'fieldname': 'payroll_period', - 'transactions': [ - { - 'items': ['Employee Tax Exemption Proof Submission', 'Employee Tax Exemption Declaration'] - }, - ], - } + return { + "fieldname": "payroll_period", + "transactions": [ + {"items": ["Employee Tax Exemption Proof Submission", "Employee Tax Exemption Declaration"]}, + ], + } diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.py b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py index 6fd30946f55..33614e9d30a 100644 --- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.py +++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.py @@ -21,12 +21,25 @@ class PayrollSettings(Document): if not self.password_policy: frappe.throw(_("Password policy for Salary Slips is not set")) - def on_update(self): self.toggle_rounded_total() frappe.clear_cache() def toggle_rounded_total(self): self.disable_rounded_total = cint(self.disable_rounded_total) - make_property_setter("Salary Slip", "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter("Salary Slip", "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter( + "Salary Slip", + "rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Salary Slip", + "rounded_total", + "print_hide", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py index 10e8381007b..cdcd9a90259 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.py @@ -14,14 +14,14 @@ class RetentionBonus(Document): def validate(self): validate_active_employee(self.employee) if getdate(self.bonus_payment_date) < getdate(): - frappe.throw(_('Bonus Payment Date cannot be a past date')) + frappe.throw(_("Bonus Payment Date cannot be a past date")) def on_submit(self): - company = frappe.db.get_value('Employee', self.employee, 'company') + company = frappe.db.get_value("Employee", self.employee, "company") additional_salary = self.get_additional_salary() if not additional_salary: - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.salary_component = self.salary_component additional_salary.amount = self.bonus_amount @@ -34,29 +34,36 @@ class RetentionBonus(Document): # self.db_set('additional_salary', additional_salary.name) else: - bonus_added = frappe.db.get_value('Additional Salary', additional_salary, 'amount') + self.bonus_amount - frappe.db.set_value('Additional Salary', additional_salary, 'amount', bonus_added) - self.db_set('additional_salary', additional_salary) + bonus_added = ( + frappe.db.get_value("Additional Salary", additional_salary, "amount") + self.bonus_amount + ) + frappe.db.set_value("Additional Salary", additional_salary, "amount", bonus_added) + self.db_set("additional_salary", additional_salary) def on_cancel(self): additional_salary = self.get_additional_salary() if self.additional_salary: - bonus_removed = frappe.db.get_value('Additional Salary', self.additional_salary, 'amount') - self.bonus_amount + bonus_removed = ( + frappe.db.get_value("Additional Salary", self.additional_salary, "amount") - self.bonus_amount + ) if bonus_removed == 0: - frappe.get_doc('Additional Salary', self.additional_salary).cancel() + frappe.get_doc("Additional Salary", self.additional_salary).cancel() else: - frappe.db.set_value('Additional Salary', self.additional_salary, 'amount', bonus_removed) + frappe.db.set_value("Additional Salary", self.additional_salary, "amount", bonus_removed) # self.db_set('additional_salary', '') def get_additional_salary(self): - return frappe.db.exists('Additional Salary', { - 'employee': self.employee, - 'salary_component': self.salary_component, - 'payroll_date': self.bonus_payment_date, - 'company': self.company, - 'docstatus': 1, - 'ref_doctype': self.doctype, - 'ref_docname': self.name - }) + return frappe.db.exists( + "Additional Salary", + { + "employee": self.employee, + "salary_component": self.salary_component, + "payroll_date": self.bonus_payment_date, + "company": self.company, + "docstatus": 1, + "ref_doctype": self.doctype, + "ref_docname": self.name, + }, + ) diff --git a/erpnext/payroll/doctype/salary_component/salary_component.py b/erpnext/payroll/doctype/salary_component/salary_component.py index b8def58643a..409c4a1769e 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.py +++ b/erpnext/payroll/doctype/salary_component/salary_component.py @@ -12,9 +12,13 @@ class SalaryComponent(Document): def validate_abbr(self): if not self.salary_component_abbr: - self.salary_component_abbr = ''.join([c[0] for c in - self.salary_component.split()]).upper() + self.salary_component_abbr = "".join([c[0] for c in self.salary_component.split()]).upper() self.salary_component_abbr = self.salary_component_abbr.strip() - self.salary_component_abbr = append_number_if_name_exists('Salary Component', self.salary_component_abbr, - 'salary_component_abbr', separator='_', filters={"name": ["!=", self.name]}) + self.salary_component_abbr = append_number_if_name_exists( + "Salary Component", + self.salary_component_abbr, + "salary_component_abbr", + separator="_", + filters={"name": ["!=", self.name]}, + ) diff --git a/erpnext/payroll/doctype/salary_component/test_salary_component.py b/erpnext/payroll/doctype/salary_component/test_salary_component.py index 6e00971a230..cd729e82400 100644 --- a/erpnext/payroll/doctype/salary_component/test_salary_component.py +++ b/erpnext/payroll/doctype/salary_component/test_salary_component.py @@ -7,15 +7,18 @@ import frappe # test_records = frappe.get_test_records('Salary Component') + class TestSalaryComponent(unittest.TestCase): pass def create_salary_component(component_name, **args): if not frappe.db.exists("Salary Component", component_name): - frappe.get_doc({ + frappe.get_doc( + { "doctype": "Salary Component", "salary_component": component_name, "type": args.get("type") or "Earning", - "is_tax_applicable": args.get("is_tax_applicable") or 1 - }).insert() + "is_tax_applicable": args.get("is_tax_applicable") or 1, + } + ).insert() diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 2691680f578..1962117608b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -49,14 +49,14 @@ from erpnext.utilities.transaction_base import TransactionBase class SalarySlip(TransactionBase): def __init__(self, *args, **kwargs): super(SalarySlip, self).__init__(*args, **kwargs) - self.series = 'Sal Slip/{0}/.#####'.format(self.employee) + self.series = "Sal Slip/{0}/.#####".format(self.employee) self.whitelisted_globals = { "int": int, "float": float, "long": int, "round": round, "date": datetime.date, - "getdate": getdate + "getdate": getdate, } def autoname(self): @@ -74,7 +74,7 @@ class SalarySlip(TransactionBase): # get details from salary structure self.get_emp_and_working_day_details() else: - self.get_working_days_details(lwp = self.leave_without_pay) + self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() self.compute_year_to_date() @@ -83,10 +83,16 @@ class SalarySlip(TransactionBase): self.add_leave_balances() if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): - max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") + max_working_hours = frappe.db.get_single_value( + "Payroll Settings", "max_working_hours_against_timesheet" + ) if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)): - frappe.msgprint(_("Total working hours should not be greater than max working hours {0}"). - format(max_working_hours), alert=True) + frappe.msgprint( + _("Total working hours should not be greater than max working hours {0}").format( + max_working_hours + ), + alert=True, + ) def set_net_total_in_words(self): doc_currency = self.currency @@ -103,19 +109,25 @@ class SalarySlip(TransactionBase): self.set_status() self.update_status(self.name) self.make_loan_repayment_entry() - if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: + if ( + frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee") + ) and not frappe.flags.via_payroll_entry: self.email_salary_slip() self.update_payment_status_for_gratuity() def update_payment_status_for_gratuity(self): - add_salary = frappe.db.get_all("Additional Salary", - filters = { + add_salary = frappe.db.get_all( + "Additional Salary", + filters={ "payroll_date": ("BETWEEN", [self.start_date, self.end_date]), "employee": self.employee, "ref_doctype": "Gratuity", "docstatus": 1, - }, fields = ["ref_docname", "name"], limit=1) + }, + fields=["ref_docname", "name"], + limit=1, + ) if len(add_salary): status = "Paid" if self.docstatus == 1 else "Unpaid" @@ -130,6 +142,7 @@ class SalarySlip(TransactionBase): def on_trash(self): from frappe.model.naming import revert_series_if_last + revert_series_if_last(self.series, self.name) def get_status(self): @@ -147,9 +160,7 @@ class SalarySlip(TransactionBase): if not joining_date: joining_date, relieving_date = frappe.get_cached_value( - "Employee", - self.employee, - ("date_of_joining", "relieving_date") + "Employee", self.employee, ("date_of_joining", "relieving_date") ) if date_diff(self.end_date, joining_date) < 0: @@ -166,16 +177,26 @@ class SalarySlip(TransactionBase): cond = "" if self.payroll_entry: cond += "and payroll_entry = '{0}'".format(self.payroll_entry) - ret_exist = frappe.db.sql("""select name from `tabSalary Slip` + ret_exist = frappe.db.sql( + """select name from `tabSalary Slip` where start_date = %s and end_date = %s and docstatus != 2 - and employee = %s and name != %s {0}""".format(cond), - (self.start_date, self.end_date, self.employee, self.name)) + and employee = %s and name != %s {0}""".format( + cond + ), + (self.start_date, self.end_date, self.employee, self.name), + ) if ret_exist: - frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee)) + frappe.throw( + _("Salary Slip of employee {0} already created for this period").format(self.employee) + ) else: for data in self.timesheets: - if frappe.db.get_value('Timesheet', data.time_sheet, 'status') == 'Payrolled': - frappe.throw(_("Salary Slip of employee {0} already created for time sheet {1}").format(self.employee, data.time_sheet)) + if frappe.db.get_value("Timesheet", data.time_sheet, "status") == "Payrolled": + frappe.throw( + _("Salary Slip of employee {0} already created for time sheet {1}").format( + self.employee, data.time_sheet + ) + ) def get_date_details(self): if not self.end_date: @@ -185,7 +206,7 @@ class SalarySlip(TransactionBase): @frappe.whitelist() def get_emp_and_working_day_details(self): - '''First time, load all the components from salary structure''' + """First time, load all the components from salary structure""" if self.employee: self.set("earnings", []) self.set("deductions", []) @@ -194,52 +215,65 @@ class SalarySlip(TransactionBase): self.get_date_details() joining_date, relieving_date = frappe.get_cached_value( - "Employee", - self.employee, - ("date_of_joining", "relieving_date") + "Employee", self.employee, ("date_of_joining", "relieving_date") ) self.validate_dates(joining_date, relieving_date) - #getin leave details + # getin leave details self.get_working_days_details(joining_date, relieving_date) struct = self.check_sal_struct(joining_date, relieving_date) if struct: - self._salary_structure_doc = frappe.get_doc('Salary Structure', struct) - self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 + self._salary_structure_doc = frappe.get_doc("Salary Structure", struct) + self.salary_slip_based_on_timesheet = ( + self._salary_structure_doc.salary_slip_based_on_timesheet or 0 + ) self.set_time_sheet() self.pull_sal_struct() - ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1) + ps = frappe.db.get_value( + "Payroll Settings", None, ["payroll_based_on", "consider_unmarked_attendance_as"], as_dict=1 + ) return [ps.payroll_based_on, ps.consider_unmarked_attendance_as] def set_time_sheet(self): if self.salary_slip_based_on_timesheet: self.set("timesheets", []) - timesheets = frappe.db.sql(""" select * from `tabTimesheet` where employee = %(employee)s and start_date BETWEEN %(start_date)s AND %(end_date)s and (status = 'Submitted' or - status = 'Billed')""", {'employee': self.employee, 'start_date': self.start_date, 'end_date': self.end_date}, as_dict=1) + timesheets = frappe.db.sql( + """ select * from `tabTimesheet` where employee = %(employee)s and start_date BETWEEN %(start_date)s AND %(end_date)s and (status = 'Submitted' or + status = 'Billed')""", + {"employee": self.employee, "start_date": self.start_date, "end_date": self.end_date}, + as_dict=1, + ) for data in timesheets: - self.append('timesheets', { - 'time_sheet': data.name, - 'working_hours': data.total_hours - }) + self.append("timesheets", {"time_sheet": data.name, "working_hours": data.total_hours}) def check_sal_struct(self, joining_date, relieving_date): cond = """and sa.employee=%(employee)s and (sa.from_date <= %(start_date)s or sa.from_date <= %(end_date)s or sa.from_date <= %(joining_date)s)""" if self.payroll_frequency: - cond += """and ss.payroll_frequency = '%(payroll_frequency)s'""" % {"payroll_frequency": self.payroll_frequency} + cond += """and ss.payroll_frequency = '%(payroll_frequency)s'""" % { + "payroll_frequency": self.payroll_frequency + } - st_name = frappe.db.sql(""" + st_name = frappe.db.sql( + """ select sa.salary_structure from `tabSalary Structure Assignment` sa join `tabSalary Structure` ss where sa.salary_structure=ss.name and sa.docstatus = 1 and ss.docstatus = 1 and ss.is_active ='Yes' %s order by sa.from_date desc limit 1 - """ %cond, {'employee': self.employee, 'start_date': self.start_date, - 'end_date': self.end_date, 'joining_date': joining_date}) + """ + % cond, + { + "employee": self.employee, + "start_date": self.start_date, + "end_date": self.end_date, + "joining_date": joining_date, + }, + ) if st_name: self.salary_structure = st_name[0][0] @@ -247,8 +281,12 @@ class SalarySlip(TransactionBase): else: self.salary_structure = None - frappe.msgprint(_("No active or default Salary Structure found for employee {0} for the given dates") - .format(self.employee), title=_('Salary Structure Missing')) + frappe.msgprint( + _("No active or default Salary Structure found for employee {0} for the given dates").format( + self.employee + ), + title=_("Salary Structure Missing"), + ) def pull_sal_struct(self): from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip @@ -260,13 +298,19 @@ class SalarySlip(TransactionBase): self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0 wages_amount = self.hour_rate * self.total_working_hours - self.add_earning_for_hourly_wages(self, self._salary_structure_doc.salary_component, wages_amount) + self.add_earning_for_hourly_wages( + self, self._salary_structure_doc.salary_component, wages_amount + ) make_salary_slip(self._salary_structure_doc.name, self) - def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): + def get_working_days_details( + self, joining_date=None, relieving_date=None, lwp=None, for_preview=0 + ): payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on") - include_holidays_in_total_working_days = frappe.db.get_single_value("Payroll Settings", "include_holidays_in_total_working_days") + include_holidays_in_total_working_days = frappe.db.get_single_value( + "Payroll Settings", "include_holidays_in_total_working_days" + ) working_days = date_diff(self.end_date, self.start_date) + 1 if for_preview: @@ -293,14 +337,16 @@ class SalarySlip(TransactionBase): if not lwp: lwp = actual_lwp elif lwp != actual_lwp: - frappe.msgprint(_("Leave Without Pay does not match with approved {} records") - .format(payroll_based_on)) + frappe.msgprint( + _("Leave Without Pay does not match with approved {} records").format(payroll_based_on) + ) self.leave_without_pay = lwp self.total_working_days = working_days - payment_days = self.get_payment_days(joining_date, - relieving_date, include_holidays_in_total_working_days) + payment_days = self.get_payment_days( + joining_date, relieving_date, include_holidays_in_total_working_days + ) if flt(payment_days) > flt(lwp): self.payment_days = flt(payment_days) - flt(lwp) @@ -308,43 +354,53 @@ class SalarySlip(TransactionBase): if payroll_based_on == "Attendance": self.payment_days -= flt(absent) - consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" + consider_unmarked_attendance_as = ( + frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present" + ) - if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent": + if payroll_based_on == "Attendance" and consider_unmarked_attendance_as == "Absent": unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days) - self.absent_days += unmarked_days #will be treated as absent + self.absent_days += unmarked_days # will be treated as absent self.payment_days -= unmarked_days else: self.payment_days = 0 def get_unmarked_days(self, include_holidays_in_total_working_days): unmarked_days = self.total_working_days - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.get_cached_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) start_date = self.start_date end_date = self.end_date if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)): start_date = joining_date - unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days, - include_holidays_in_total_working_days, self.start_date, joining_date) + unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( + unmarked_days, include_holidays_in_total_working_days, self.start_date, joining_date + ) if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)): end_date = relieving_date - unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days, - include_holidays_in_total_working_days, relieving_date, self.end_date) + unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( + unmarked_days, include_holidays_in_total_working_days, relieving_date, self.end_date + ) # exclude days for which attendance has been marked - unmarked_days -= frappe.get_all("Attendance", filters = { - "attendance_date": ["between", [start_date, end_date]], - "employee": self.employee, - "docstatus": 1 - }, fields = ["COUNT(*) as marked_days"])[0].marked_days + unmarked_days -= frappe.get_all( + "Attendance", + filters={ + "attendance_date": ["between", [start_date, end_date]], + "employee": self.employee, + "docstatus": 1, + }, + fields=["COUNT(*) as marked_days"], + )[0].marked_days return unmarked_days - def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days, - include_holidays_in_total_working_days, start_date, end_date): + def get_unmarked_days_based_on_doj_or_relieving( + self, unmarked_days, include_holidays_in_total_working_days, start_date, end_date + ): """ Exclude days before DOJ or after Relieving Date from unmarked days @@ -364,8 +420,9 @@ class SalarySlip(TransactionBase): def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days): if not joining_date: - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.get_cached_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) start_date = getdate(self.start_date) if joining_date: @@ -379,8 +436,7 @@ class SalarySlip(TransactionBase): if getdate(self.start_date) <= relieving_date <= getdate(self.end_date): end_date = relieving_date elif relieving_date < getdate(self.start_date): - frappe.throw(_("Employee relieved on {0} must be set as 'Left'") - .format(relieving_date)) + frappe.throw(_("Employee relieved on {0} must be set as 'Left'").format(relieving_date)) payment_days = date_diff(end_date, start_date) + 1 @@ -396,12 +452,14 @@ class SalarySlip(TransactionBase): def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) - daily_wages_fraction_for_half_day = \ + daily_wages_fraction_for_half_day = ( flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + ) for d in range(working_days): dt = add_days(cstr(getdate(self.start_date)), d) - leave = frappe.db.sql(""" + leave = frappe.db.sql( + """ SELECT t1.name, CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) THEN t1.half_day else 0 END, @@ -419,7 +477,11 @@ class SalarySlip(TransactionBase): WHEN t2.include_holiday THEN %(dt)s between from_date and to_date END - """.format(holidays), {"employee": self.employee, "dt": dt}) + """.format( + holidays + ), + {"employee": self.employee, "dt": dt}, + ) if leave: equivalent_lwp_count = 0 @@ -427,10 +489,12 @@ class SalarySlip(TransactionBase): is_partially_paid_leave = cint(leave[0][2]) fraction_of_daily_salary_per_leave = flt(leave[0][3]) - equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 if is_partially_paid_leave: - equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + equivalent_lwp_count *= ( + fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + ) lwp += equivalent_lwp_count @@ -440,18 +504,22 @@ class SalarySlip(TransactionBase): lwp = 0 absent = 0 - daily_wages_fraction_for_half_day = \ + daily_wages_fraction_for_half_day = ( flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + ) - leave_types = frappe.get_all("Leave Type", + leave_types = frappe.get_all( + "Leave Type", or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]], - fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"]) + fields=["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"], + ) leave_type_map = {} for leave_type in leave_types: leave_type_map[leave_type.name] = leave_type - attendances = frappe.db.sql(''' + attendances = frappe.db.sql( + """ SELECT attendance_date, status, leave_type FROM `tabAttendance` WHERE @@ -459,30 +527,46 @@ class SalarySlip(TransactionBase): AND employee = %s AND docstatus = 1 AND attendance_date between %s and %s - ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) + """, + values=(self.employee, self.start_date, self.end_date), + as_dict=1, + ) for d in attendances: - if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys(): + if ( + d.status in ("Half Day", "On Leave") + and d.leave_type + and d.leave_type not in leave_type_map.keys() + ): continue if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays: - if d.status == "Absent" or \ - (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']): - continue + if d.status == "Absent" or ( + d.leave_type + and d.leave_type in leave_type_map.keys() + and not leave_type_map[d.leave_type]["include_holiday"] + ): + continue if d.leave_type: - fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"] + fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type][ + "fraction_of_daily_salary_per_leave" + ] if d.status == "Half Day": - equivalent_lwp = (1 - daily_wages_fraction_for_half_day) + equivalent_lwp = 1 - daily_wages_fraction_for_half_day if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]: - equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + equivalent_lwp *= ( + fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + ) lwp += equivalent_lwp elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys(): equivalent_lwp = 1 if leave_type_map[d.leave_type]["is_ppl"]: - equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + equivalent_lwp *= ( + fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1 + ) lwp += equivalent_lwp elif d.status == "Absent": absent += 1 @@ -502,15 +586,17 @@ class SalarySlip(TransactionBase): "abbr": frappe.db.get_value("Salary Component", salary_component, "salary_component_abbr"), "amount": self.hour_rate * self.total_working_hours, "default_amount": 0.0, - "additional_amount": 0.0 + "additional_amount": 0.0, } - doc.append('earnings', wages_row) + doc.append("earnings", wages_row) def calculate_net_pay(self): if self.salary_structure: self.calculate_component_amounts("earnings") self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1) - self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) + self.base_gross_pay = flt( + flt(self.gross_pay) * flt(self.exchange_rate), self.precision("base_gross_pay") + ) if self.salary_structure: self.calculate_component_amounts("deductions") @@ -521,18 +607,24 @@ class SalarySlip(TransactionBase): def set_net_pay(self): self.total_deduction = self.get_component_totals("deductions") - self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction')) + self.base_total_deduction = flt( + flt(self.total_deduction) * flt(self.exchange_rate), self.precision("base_total_deduction") + ) self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment)) self.rounded_total = rounded(self.net_pay) - self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) - self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision('base_net_pay')) + self.base_net_pay = flt( + flt(self.net_pay) * flt(self.exchange_rate), self.precision("base_net_pay") + ) + self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision("base_net_pay")) if self.hour_rate: - self.base_hour_rate = flt(flt(self.hour_rate) * flt(self.exchange_rate), self.precision('base_hour_rate')) + self.base_hour_rate = flt( + flt(self.hour_rate) * flt(self.exchange_rate), self.precision("base_hour_rate") + ) self.set_net_total_in_words() def calculate_component_amounts(self, component_type): - if not getattr(self, '_salary_structure_doc', None): - self._salary_structure_doc = frappe.get_doc('Salary Structure', self.salary_structure) + if not getattr(self, "_salary_structure_doc", None): + self._salary_structure_doc = frappe.get_doc("Salary Structure", self.salary_structure) payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) @@ -551,15 +643,13 @@ class SalarySlip(TransactionBase): self.update_component_row(struct_row, amount, component_type) def get_data_for_eval(self): - '''Returns data for evaluating formula''' + """Returns data for evaluating formula""" data = frappe._dict() employee = frappe.get_doc("Employee", self.employee).as_dict() start_date = getdate(self.start_date) date_to_validate = ( - employee.date_of_joining - if employee.date_of_joining > start_date - else start_date + employee.date_of_joining if employee.date_of_joining > start_date else start_date ) salary_structure_assignment = frappe.get_value( @@ -577,8 +667,9 @@ class SalarySlip(TransactionBase): if not salary_structure_assignment: frappe.throw( - _("Please assign a Salary Structure for Employee {0} " - "applicable from or before {1} first").format( + _( + "Please assign a Salary Structure for Employee {0} " "applicable from or before {1} first" + ).format( frappe.bold(self.employee_name), frappe.bold(formatdate(date_to_validate)), ) @@ -593,7 +684,7 @@ class SalarySlip(TransactionBase): for sc in salary_components: data.setdefault(sc.salary_component_abbr, 0) - for key in ('earnings', 'deductions'): + for key in ("earnings", "deductions"): for d in self.get(key): data[d.abbr] = d.amount @@ -616,8 +707,10 @@ class SalarySlip(TransactionBase): return amount except NameError as err: - frappe.throw(_("{0}
    This error can be due to missing or deleted field.").format(err), - title=_("Name error")) + frappe.throw( + _("{0}
    This error can be due to missing or deleted field.").format(err), + title=_("Name error"), + ) except SyntaxError as err: frappe.throw(_("Syntax error in formula or condition: {0}").format(err)) except Exception as e: @@ -627,13 +720,27 @@ class SalarySlip(TransactionBase): def add_employee_benefits(self, payroll_period): for struct_row in self._salary_structure_doc.get("earnings"): if struct_row.is_flexible_benefit == 1: - if frappe.db.get_value("Salary Component", struct_row.salary_component, "pay_against_benefit_claim") != 1: - benefit_component_amount = get_benefit_component_amount(self.employee, self.start_date, self.end_date, - struct_row.salary_component, self._salary_structure_doc, self.payroll_frequency, payroll_period) + if ( + frappe.db.get_value( + "Salary Component", struct_row.salary_component, "pay_against_benefit_claim" + ) + != 1 + ): + benefit_component_amount = get_benefit_component_amount( + self.employee, + self.start_date, + self.end_date, + struct_row.salary_component, + self._salary_structure_doc, + self.payroll_frequency, + payroll_period, + ) if benefit_component_amount: self.update_component_row(struct_row, benefit_component_amount, "earnings") else: - benefit_claim_amount = get_benefit_claim_amount(self.employee, self.start_date, self.end_date, struct_row.salary_component) + benefit_claim_amount = get_benefit_claim_amount( + self.employee, self.start_date, self.end_date, struct_row.salary_component + ) if benefit_claim_amount: self.update_component_row(struct_row, benefit_claim_amount, "earnings") @@ -641,9 +748,10 @@ class SalarySlip(TransactionBase): def adjust_benefits_in_last_payroll_period(self, payroll_period): if payroll_period: - if (getdate(payroll_period.end_date) <= getdate(self.end_date)): - last_benefits = get_last_payroll_period_benefits(self.employee, self.start_date, self.end_date, - payroll_period, self._salary_structure_doc) + if getdate(payroll_period.end_date) <= getdate(self.end_date): + last_benefits = get_last_payroll_period_benefits( + self.employee, self.start_date, self.end_date, payroll_period, self._salary_structure_doc + ) if last_benefits: for last_benefit in last_benefits: last_benefit = frappe._dict(last_benefit) @@ -651,8 +759,9 @@ class SalarySlip(TransactionBase): self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings") def add_additional_salary_components(self, component_type): - additional_salaries = get_additional_salaries(self.employee, - self.start_date, self.end_date, component_type) + additional_salaries = get_additional_salaries( + self.employee, self.start_date, self.end_date, component_type + ) for additional_salary in additional_salaries: self.update_component_row( @@ -660,7 +769,7 @@ class SalarySlip(TransactionBase): additional_salary.amount, component_type, additional_salary, - is_recurring = additional_salary.is_recurring + is_recurring=additional_salary.is_recurring, ) def add_tax_components(self, payroll_period): @@ -673,40 +782,43 @@ class SalarySlip(TransactionBase): other_deduction_components.append(d.salary_component) if not tax_components: - tax_components = [d.name for d in frappe.get_all("Salary Component", filters={"variable_based_on_taxable_salary": 1}) - if d.name not in other_deduction_components] + tax_components = [ + d.name + for d in frappe.get_all("Salary Component", filters={"variable_based_on_taxable_salary": 1}) + if d.name not in other_deduction_components + ] for d in tax_components: tax_amount = self.calculate_variable_based_on_taxable_salary(d, payroll_period) tax_row = get_salary_component_data(d) self.update_component_row(tax_row, tax_amount, "deductions") - def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0): + def update_component_row( + self, component_data, amount, component_type, additional_salary=None, is_recurring=0 + ): component_row = None for d in self.get(component_type): if d.salary_component != component_data.salary_component: continue - if ( - ( - not d.additional_salary - and (not additional_salary or additional_salary.overwrite) - ) or ( - additional_salary - and additional_salary.name == d.additional_salary - ) + if (not d.additional_salary and (not additional_salary or additional_salary.overwrite)) or ( + additional_salary and additional_salary.name == d.additional_salary ): component_row = d break if additional_salary and additional_salary.overwrite: # Additional Salary with overwrite checked, remove default rows of same component - self.set(component_type, [ - d for d in self.get(component_type) - if d.salary_component != component_data.salary_component - or (d.additional_salary and additional_salary.name != d.additional_salary) - or d == component_row - ]) + self.set( + component_type, + [ + d + for d in self.get(component_type) + if d.salary_component != component_data.salary_component + or (d.additional_salary and additional_salary.name != d.additional_salary) + or d == component_row + ], + ) if not component_row: if not amount: @@ -714,33 +826,40 @@ class SalarySlip(TransactionBase): component_row = self.append(component_type) for attr in ( - 'depends_on_payment_days', 'salary_component', - 'do_not_include_in_total', 'is_tax_applicable', - 'is_flexible_benefit', 'variable_based_on_taxable_salary', - 'exempted_from_income_tax' + "depends_on_payment_days", + "salary_component", + "do_not_include_in_total", + "is_tax_applicable", + "is_flexible_benefit", + "variable_based_on_taxable_salary", + "exempted_from_income_tax", ): component_row.set(attr, component_data.get(attr)) - abbr = component_data.get('abbr') or component_data.get('salary_component_abbr') - component_row.set('abbr', abbr) + abbr = component_data.get("abbr") or component_data.get("salary_component_abbr") + component_row.set("abbr", abbr) if additional_salary: if additional_salary.overwrite: - component_row.additional_amount = flt(flt(amount) - flt(component_row.get("default_amount", 0)), - component_row.precision("additional_amount")) + component_row.additional_amount = flt( + flt(amount) - flt(component_row.get("default_amount", 0)), + component_row.precision("additional_amount"), + ) else: component_row.default_amount = 0 component_row.additional_amount = amount component_row.is_recurring_additional_salary = is_recurring component_row.additional_salary = additional_salary.name - component_row.deduct_full_tax_on_selected_payroll_date = \ + component_row.deduct_full_tax_on_selected_payroll_date = ( additional_salary.deduct_full_tax_on_selected_payroll_date + ) else: component_row.default_amount = amount component_row.additional_amount = 0 - component_row.deduct_full_tax_on_selected_payroll_date = \ + component_row.deduct_full_tax_on_selected_payroll_date = ( component_data.deduct_full_tax_on_selected_payroll_date + ) component_row.amount = amount @@ -748,7 +867,9 @@ class SalarySlip(TransactionBase): def update_component_amount_based_on_payment_days(self, component_row): joining_date, relieving_date = self.get_joining_and_relieving_dates() - component_row.amount = self.get_amount_based_on_payment_days(component_row, joining_date, relieving_date)[0] + component_row.amount = self.get_amount_based_on_payment_days( + component_row, joining_date, relieving_date + )[0] def set_precision_for_component_amounts(self): for component_type in ("earnings", "deductions"): @@ -757,8 +878,11 @@ class SalarySlip(TransactionBase): def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period): if not payroll_period: - frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}.") - .format(tax_component)) + frappe.msgprint( + _("Start and end dates not in a valid Payroll Period, cannot calculate {0}.").format( + tax_component + ) + ) return # Deduct taxes forcefully for unsubmitted tax exemption proof and unclaimed benefits in the last period @@ -773,23 +897,34 @@ class SalarySlip(TransactionBase): tax_slab = self.get_income_tax_slabs(payroll_period) # get remaining numbers of sub-period (period for which one salary is processed) - remaining_sub_periods = get_period_factor(self.employee, - self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] + remaining_sub_periods = get_period_factor( + self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period + )[1] # get taxable_earnings, paid_taxes for previous period - previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, - self.start_date, tax_slab.allow_tax_exemption) - previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) + previous_taxable_earnings = self.get_taxable_earnings_for_prev_period( + payroll_period.start_date, self.start_date, tax_slab.allow_tax_exemption + ) + previous_total_paid_taxes = self.get_tax_paid_in_period( + payroll_period.start_date, self.start_date, tax_component + ) # get taxable_earnings for current period (all days) - current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption, payroll_period=payroll_period) - future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1) + current_taxable_earnings = self.get_taxable_earnings( + tax_slab.allow_tax_exemption, payroll_period=payroll_period + ) + future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * ( + math.ceil(remaining_sub_periods) - 1 + ) # get taxable_earnings, addition_earnings for current actual payment days - current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, - based_on_payment_days=1, payroll_period=payroll_period) + current_taxable_earnings_for_payment_days = self.get_taxable_earnings( + tax_slab.allow_tax_exemption, based_on_payment_days=1, payroll_period=payroll_period + ) current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income - current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax + current_additional_earnings_with_full_tax = ( + current_taxable_earnings_for_payment_days.additional_income_with_full_tax + ) # Get taxable unclaimed benefits unclaimed_taxable_benefits = 0 @@ -800,20 +935,32 @@ class SalarySlip(TransactionBase): # Total exemption amount based on tax exemption declaration total_exemption_amount = self.get_total_exemption_amount(payroll_period, tax_slab) - #Employee Other Incomes + # Employee Other Incomes other_incomes = self.get_income_form_other_sources(payroll_period) or 0.0 # Total taxable earnings including additional and other incomes - total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \ - + current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount + total_taxable_earnings = ( + previous_taxable_earnings + + current_structured_taxable_earnings + + future_structured_taxable_earnings + + current_additional_earnings + + other_incomes + + unclaimed_taxable_benefits + - total_exemption_amount + ) # Total taxable earnings without additional earnings with full tax - total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax + total_taxable_earnings_without_full_tax_addl_components = ( + total_taxable_earnings - current_additional_earnings_with_full_tax + ) # Structured tax amount total_structured_tax_amount = self.calculate_tax_by_tax_slab( - total_taxable_earnings_without_full_tax_addl_components, tax_slab) - current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods + total_taxable_earnings_without_full_tax_addl_components, tax_slab + ) + current_structured_tax_amount = ( + total_structured_tax_amount - previous_total_paid_taxes + ) / remaining_sub_periods # Total taxable earnings with additional earnings with full tax full_tax_on_additional_earnings = 0.0 @@ -828,25 +975,33 @@ class SalarySlip(TransactionBase): return current_tax_amount def get_income_tax_slabs(self, payroll_period): - income_tax_slab, ss_assignment_name = frappe.db.get_value("Salary Structure Assignment", - {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, ["income_tax_slab", 'name']) + income_tax_slab, ss_assignment_name = frappe.db.get_value( + "Salary Structure Assignment", + {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, + ["income_tax_slab", "name"], + ) if not income_tax_slab: - frappe.throw(_("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name)) + frappe.throw( + _("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name) + ) income_tax_slab_doc = frappe.get_doc("Income Tax Slab", income_tax_slab) if income_tax_slab_doc.disabled: frappe.throw(_("Income Tax Slab: {0} is disabled").format(income_tax_slab)) if getdate(income_tax_slab_doc.effective_from) > getdate(payroll_period.start_date): - frappe.throw(_("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}") - .format(payroll_period.start_date)) + frappe.throw( + _("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}").format( + payroll_period.start_date + ) + ) return income_tax_slab_doc - def get_taxable_earnings_for_prev_period(self, start_date, end_date, allow_tax_exemption=False): - taxable_earnings = frappe.db.sql(""" + taxable_earnings = frappe.db.sql( + """ select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name @@ -858,16 +1013,15 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(from_date)s and %(to_date)s and ss.end_date between %(from_date)s and %(to_date)s - """, { - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - }) + """, + {"employee": self.employee, "from_date": start_date, "to_date": end_date}, + ) taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0 exempted_amount = 0 if allow_tax_exemption: - exempted_amount = frappe.db.sql(""" + exempted_amount = frappe.db.sql( + """ select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name @@ -879,18 +1033,18 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(from_date)s and %(to_date)s and ss.end_date between %(from_date)s and %(to_date)s - """, { - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - }) + """, + {"employee": self.employee, "from_date": start_date, "to_date": end_date}, + ) exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 return taxable_earnings - exempted_amount def get_tax_paid_in_period(self, start_date, end_date, tax_component): # find total_tax_paid, tax paid for benefit, additional_salary - total_tax_paid = flt(frappe.db.sql(""" + total_tax_paid = flt( + frappe.db.sql( + """ select sum(sd.amount) from @@ -903,16 +1057,21 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(from_date)s and %(to_date)s and ss.end_date between %(from_date)s and %(to_date)s - """, { - "salary_component": tax_component, - "employee": self.employee, - "from_date": start_date, - "to_date": end_date - })[0][0]) + """, + { + "salary_component": tax_component, + "employee": self.employee, + "from_date": start_date, + "to_date": end_date, + }, + )[0][0] + ) return total_tax_paid - def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None): + def get_taxable_earnings( + self, allow_tax_exemption=False, based_on_payment_days=0, payroll_period=None + ): joining_date, relieving_date = self.get_joining_and_relieving_dates() taxable_earnings = 0 @@ -922,7 +1081,9 @@ class SalarySlip(TransactionBase): for earning in self.earnings: if based_on_payment_days: - amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date) + amount, additional_amount = self.get_amount_based_on_payment_days( + earning, joining_date, relieving_date + ) else: if earning.additional_amount: amount, additional_amount = earning.amount, earning.additional_amount @@ -933,13 +1094,14 @@ class SalarySlip(TransactionBase): if earning.is_flexible_benefit: flexi_benefits += amount else: - taxable_earnings += (amount - additional_amount) + taxable_earnings += amount - additional_amount additional_income += additional_amount # Get additional amount based on future recurring additional salary if additional_amount and earning.is_recurring_additional_salary: - additional_income += self.get_future_recurring_additional_amount(earning.additional_salary, - earning.additional_amount, payroll_period) # Used earning.additional_amount to consider the amount for the full month + additional_income += self.get_future_recurring_additional_amount( + earning.additional_salary, earning.additional_amount, payroll_period + ) # Used earning.additional_amount to consider the amount for the full month if earning.deduct_full_tax_on_selected_payroll_date: additional_income_with_full_tax += additional_amount @@ -949,25 +1111,32 @@ class SalarySlip(TransactionBase): if ded.exempted_from_income_tax: amount, additional_amount = ded.amount, ded.additional_amount if based_on_payment_days: - amount, additional_amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date) + amount, additional_amount = self.get_amount_based_on_payment_days( + ded, joining_date, relieving_date + ) taxable_earnings -= flt(amount - additional_amount) additional_income -= additional_amount if additional_amount and ded.is_recurring_additional_salary: - additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary, - ded.additional_amount, payroll_period) # Used ded.additional_amount to consider the amount for the full month + additional_income -= self.get_future_recurring_additional_amount( + ded.additional_salary, ded.additional_amount, payroll_period + ) # Used ded.additional_amount to consider the amount for the full month - return frappe._dict({ - "taxable_earnings": taxable_earnings, - "additional_income": additional_income, - "additional_income_with_full_tax": additional_income_with_full_tax, - "flexi_benefits": flexi_benefits - }) + return frappe._dict( + { + "taxable_earnings": taxable_earnings, + "additional_income": additional_income, + "additional_income_with_full_tax": additional_income_with_full_tax, + "flexi_benefits": flexi_benefits, + } + ) - def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount, payroll_period): + def get_future_recurring_additional_amount( + self, additional_salary, monthly_additional_amount, payroll_period + ): future_recurring_additional_amount = 0 - to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date') + to_date = frappe.db.get_value("Additional Salary", additional_salary, "to_date") # future month count excluding current from_date, to_date = getdate(self.start_date), getdate(to_date) @@ -977,42 +1146,69 @@ class SalarySlip(TransactionBase): if getdate(to_date) > getdate(payroll_period.end_date): to_date = getdate(payroll_period.end_date) - future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month) + future_recurring_period = ((to_date.year - from_date.year) * 12) + ( + to_date.month - from_date.month + ) if future_recurring_period > 0: - future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month + future_recurring_additional_amount = ( + monthly_additional_amount * future_recurring_period + ) # Used earning.additional_amount to consider the amount for the full month return future_recurring_additional_amount def get_amount_based_on_payment_days(self, row, joining_date, relieving_date): amount, additional_amount = row.amount, row.additional_amount - timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component") + timesheet_component = frappe.db.get_value( + "Salary Structure", self.salary_structure, "salary_component" + ) - if (self.salary_structure and - cint(row.depends_on_payment_days) and cint(self.total_working_days) - and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary - and (row.salary_component != timesheet_component or - getdate(self.start_date) < joining_date or - (relieving_date and getdate(self.end_date) > relieving_date) - )): - additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days) - / cint(self.total_working_days)), row.precision("additional_amount")) - amount = flt((flt(row.default_amount) * flt(self.payment_days) - / cint(self.total_working_days)), row.precision("amount")) + additional_amount + if ( + self.salary_structure + and cint(row.depends_on_payment_days) + and cint(self.total_working_days) + and not ( + row.additional_salary and row.default_amount + ) # to identify overwritten additional salary + and ( + row.salary_component != timesheet_component + or getdate(self.start_date) < joining_date + or (relieving_date and getdate(self.end_date) > relieving_date) + ) + ): + additional_amount = flt( + (flt(row.additional_amount) * flt(self.payment_days) / cint(self.total_working_days)), + row.precision("additional_amount"), + ) + amount = ( + flt( + (flt(row.default_amount) * flt(self.payment_days) / cint(self.total_working_days)), + row.precision("amount"), + ) + + additional_amount + ) - elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days): + elif ( + not self.payment_days + and row.salary_component != timesheet_component + and cint(row.depends_on_payment_days) + ): amount, additional_amount = 0, 0 elif not row.amount: amount = flt(row.default_amount) + flt(row.additional_amount) # apply rounding - if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"): + if frappe.get_cached_value( + "Salary Component", row.salary_component, "round_to_the_nearest_integer" + ): amount, additional_amount = rounded(amount or 0), rounded(additional_amount or 0) return amount, additional_amount def calculate_unclaimed_taxable_benefits(self, payroll_period): # get total sum of benefits paid - total_benefits_paid = flt(frappe.db.sql(""" + total_benefits_paid = flt( + frappe.db.sql( + """ select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name where @@ -1023,21 +1219,29 @@ class SalarySlip(TransactionBase): and ss.employee=%(employee)s and ss.start_date between %(start_date)s and %(end_date)s and ss.end_date between %(start_date)s and %(end_date)s - """, { - "employee": self.employee, - "start_date": payroll_period.start_date, - "end_date": self.start_date - })[0][0]) + """, + { + "employee": self.employee, + "start_date": payroll_period.start_date, + "end_date": self.start_date, + }, + )[0][0] + ) # get total benefits claimed - total_benefits_claimed = flt(frappe.db.sql(""" + total_benefits_claimed = flt( + frappe.db.sql( + """ select sum(claimed_amount) from `tabEmployee Benefit Claim` where docstatus=1 and employee=%s and claim_date between %s and %s - """, (self.employee, payroll_period.start_date, self.end_date))[0][0]) + """, + (self.employee, payroll_period.start_date, self.end_date), + )[0][0] + ) return total_benefits_paid - total_benefits_claimed @@ -1045,15 +1249,19 @@ class SalarySlip(TransactionBase): total_exemption_amount = 0 if tax_slab.allow_tax_exemption: if self.deduct_tax_for_unsubmitted_tax_exemption_proof: - exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", + exemption_proof = frappe.db.get_value( + "Employee Tax Exemption Proof Submission", {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["exemption_amount"]) + ["exemption_amount"], + ) if exemption_proof: total_exemption_amount = exemption_proof else: - declaration = frappe.db.get_value("Employee Tax Exemption Declaration", + declaration = frappe.db.get_value( + "Employee Tax Exemption Declaration", {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["total_exemption_amount"]) + ["total_exemption_amount"], + ) if declaration: total_exemption_amount = declaration @@ -1062,14 +1270,15 @@ class SalarySlip(TransactionBase): return total_exemption_amount def get_income_form_other_sources(self, payroll_period): - return frappe.get_all("Employee Other Income", + return frappe.get_all( + "Employee Other Income", filters={ "employee": self.employee, "payroll_period": payroll_period.name, "company": self.company, - "docstatus": 1 + "docstatus": 1, }, - fields="SUM(amount) as total_amount" + fields="SUM(amount) as total_amount", )[0].total_amount def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): @@ -1081,12 +1290,12 @@ class SalarySlip(TransactionBase): if cond and not self.eval_tax_slab_condition(cond, data): continue if not slab.to_amount and annual_taxable_earning >= slab.from_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 continue if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount: - tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction * 0.01 elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount: - tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * .01 + tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * 0.01 # other taxes and charges on income tax for d in tax_slab.other_taxes_and_charges: @@ -1106,8 +1315,10 @@ class SalarySlip(TransactionBase): if condition: return frappe.safe_eval(condition, self.whitelisted_globals, data) except NameError as err: - frappe.throw(_("{0}
    This error can be due to missing or deleted field.").format(err), - title=_("Name error")) + frappe.throw( + _("{0}
    This error can be due to missing or deleted field.").format(err), + title=_("Name error"), + ) except SyntaxError as err: frappe.throw(_("Syntax error in condition: {0}").format(err)) except Exception as e: @@ -1115,8 +1326,9 @@ class SalarySlip(TransactionBase): raise def get_component_totals(self, component_type, depends_on_payment_days=0): - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.get_cached_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) total = 0.0 for d in self.get(component_type): @@ -1129,14 +1341,17 @@ class SalarySlip(TransactionBase): return total def get_joining_and_relieving_dates(self): - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.get_cached_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) if not relieving_date: relieving_date = getdate(self.end_date) if not joining_date: - frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) + frappe.throw( + _("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)) + ) return joining_date, relieving_date @@ -1145,28 +1360,38 @@ class SalarySlip(TransactionBase): self.total_interest_amount = 0 self.total_principal_amount = 0 - if not self.get('loans'): + if not self.get("loans"): for loan in self.get_loan_details(): amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment") - if amounts['interest_amount'] or amounts['payable_principal_amount']: - self.append('loans', { - 'loan': loan.name, - 'total_payment': amounts['interest_amount'] + amounts['payable_principal_amount'], - 'interest_amount': amounts['interest_amount'], - 'principal_amount': amounts['payable_principal_amount'], - 'loan_account': loan.loan_account, - 'interest_income_account': loan.interest_income_account - }) + if amounts["interest_amount"] or amounts["payable_principal_amount"]: + self.append( + "loans", + { + "loan": loan.name, + "total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"], + "interest_amount": amounts["interest_amount"], + "principal_amount": amounts["payable_principal_amount"], + "loan_account": loan.loan_account, + "interest_income_account": loan.interest_income_account, + }, + ) - for payment in self.get('loans'): + for payment in self.get("loans"): amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") - total_amount = amounts['interest_amount'] + amounts['payable_principal_amount'] + total_amount = amounts["interest_amount"] + amounts["payable_principal_amount"] if payment.total_payment > total_amount: - frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3}""") - .format(payment.idx, frappe.bold(payment.total_payment), - frappe.bold(total_amount), frappe.bold(payment.loan))) + frappe.throw( + _( + """Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3}""" + ).format( + payment.idx, + frappe.bold(payment.total_payment), + frappe.bold(total_amount), + frappe.bold(payment.loan), + ) + ) self.total_interest_amount += payment.interest_amount self.total_principal_amount += payment.principal_amount @@ -1174,27 +1399,40 @@ class SalarySlip(TransactionBase): self.total_loan_repayment += payment.total_payment def get_loan_details(self): - return frappe.get_all("Loan", + return frappe.get_all( + "Loan", fields=["name", "interest_income_account", "loan_account", "loan_type"], - filters = { + filters={ "applicant": self.employee, "docstatus": 1, "repay_from_salary": 1, - "company": self.company - }) + "company": self.company, + }, + ) def make_loan_repayment_entry(self): payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry) for loan in self.loans: if loan.total_payment: - repayment_entry = create_repayment_entry(loan.loan, self.employee, - self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, - loan.principal_amount, loan.total_payment, payroll_payable_account=payroll_payable_account) + repayment_entry = create_repayment_entry( + loan.loan, + self.employee, + self.company, + self.posting_date, + loan.loan_type, + "Regular Payment", + loan.interest_amount, + loan.principal_amount, + loan.total_payment, + payroll_payable_account=payroll_payable_account, + ) repayment_entry.save() repayment_entry.submit() - frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name) + frappe.db.set_value( + "Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name + ) def cancel_loan_repayment_entry(self): for loan in self.loans: @@ -1210,19 +1448,23 @@ class SalarySlip(TransactionBase): if payroll_settings.encrypt_salary_slips_in_emails: password = generate_password_for_pdf(payroll_settings.password_policy, self.employee) message += """
    Note: Your salary slip is password protected, - the password to unlock the PDF is of the format {0}. """.format(payroll_settings.password_policy) + the password to unlock the PDF is of the format {0}. """.format( + payroll_settings.password_policy + ) if receiver: email_args = { "recipients": [receiver], "message": _(message), - "subject": 'Salary Slip - from {0} to {1}'.format(self.start_date, self.end_date), - "attachments": [frappe.attach_print(self.doctype, self.name, file_name=self.name, password=password)], + "subject": "Salary Slip - from {0} to {1}".format(self.start_date, self.end_date), + "attachments": [ + frappe.attach_print(self.doctype, self.name, file_name=self.name, password=password) + ], "reference_doctype": self.doctype, - "reference_name": self.name - } + "reference_name": self.name, + } if not frappe.flags.in_test: - enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args) else: frappe.sendmail(**email_args) else: @@ -1231,21 +1473,20 @@ class SalarySlip(TransactionBase): def update_status(self, salary_slip=None): for data in self.timesheets: if data.time_sheet: - timesheet = frappe.get_doc('Timesheet', data.time_sheet) + timesheet = frappe.get_doc("Timesheet", data.time_sheet) timesheet.salary_slip = salary_slip timesheet.flags.ignore_validate_update_after_submit = True timesheet.set_status() timesheet.save() def set_status(self, status=None): - '''Get and update status''' + """Get and update status""" if not status: status = self.get_status() self.db_set("status", status) - def process_salary_structure(self, for_preview=0): - '''Calculate salary after salary structure details have been updated''' + """Calculate salary after salary structure details have been updated""" if not self.salary_slip_based_on_timesheet: self.get_date_details() self.pull_emp_details() @@ -1253,7 +1494,9 @@ class SalarySlip(TransactionBase): self.calculate_net_pay() def pull_emp_details(self): - emp = frappe.db.get_value("Employee", self.employee, ["bank_name", "bank_ac_no", "salary_mode"], as_dict=1) + emp = frappe.db.get_value( + "Employee", self.employee, ["bank_name", "bank_ac_no", "salary_mode"], as_dict=1 + ) if emp: self.mode_of_payment = emp.salary_mode self.bank_name = emp.bank_name @@ -1288,7 +1531,7 @@ class SalarySlip(TransactionBase): self.base_rounded_total = rounded(self.base_net_pay or 0) self.set_net_total_in_words() - #calculate total working hours, earnings based on hourly wages and totals + # calculate total working hours, earnings based on hourly wages and totals def calculate_total_for_salary_slip_based_on_timesheet(self): if self.timesheets: self.total_working_hours = 0 @@ -1298,7 +1541,9 @@ class SalarySlip(TransactionBase): wages_amount = self.total_working_hours * self.hour_rate self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate) - salary_component = frappe.db.get_value('Salary Structure', {'name': self.salary_structure}, 'salary_component') + salary_component = frappe.db.get_value( + "Salary Structure", {"name": self.salary_structure}, "salary_component" + ) if self.earnings: for i, earning in enumerate(self.earnings): if earning.salary_component == salary_component: @@ -1310,14 +1555,17 @@ class SalarySlip(TransactionBase): year_to_date = 0 period_start_date, period_end_date = self.get_year_to_date_period() - salary_slip_sum = frappe.get_list('Salary Slip', - fields = ['sum(net_pay) as net_sum', 'sum(gross_pay) as gross_sum'], - filters = {'employee' : self.employee, - 'start_date' : ['>=', period_start_date], - 'end_date' : ['<', period_end_date], - 'name': ['!=', self.name], - 'docstatus': 1 - }) + salary_slip_sum = frappe.get_list( + "Salary Slip", + fields=["sum(net_pay) as net_sum", "sum(gross_pay) as gross_sum"], + filters={ + "employee": self.employee, + "start_date": [">=", period_start_date], + "end_date": ["<", period_end_date], + "name": ["!=", self.name], + "docstatus": 1, + }, + ) year_to_date = flt(salary_slip_sum[0].net_sum) if salary_slip_sum else 0.0 gross_year_to_date = flt(salary_slip_sum[0].gross_sum) if salary_slip_sum else 0.0 @@ -1330,14 +1578,17 @@ class SalarySlip(TransactionBase): def compute_month_to_date(self): month_to_date = 0 first_day_of_the_month = get_first_day(self.start_date) - salary_slip_sum = frappe.get_list('Salary Slip', - fields = ['sum(net_pay) as sum'], - filters = {'employee' : self.employee, - 'start_date' : ['>=', first_day_of_the_month], - 'end_date' : ['<', self.start_date], - 'name': ['!=', self.name], - 'docstatus': 1 - }) + salary_slip_sum = frappe.get_list( + "Salary Slip", + fields=["sum(net_pay) as sum"], + filters={ + "employee": self.employee, + "start_date": [">=", first_day_of_the_month], + "end_date": ["<", self.start_date], + "name": ["!=", self.name], + "docstatus": 1, + }, + ) month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0 @@ -1347,10 +1598,11 @@ class SalarySlip(TransactionBase): def compute_component_wise_year_to_date(self): period_start_date, period_end_date = self.get_year_to_date_period() - for key in ('earnings', 'deductions'): + for key in ("earnings", "deductions"): for component in self.get(key): year_to_date = 0 - component_sum = frappe.db.sql(""" + component_sum = frappe.db.sql( + """ SELECT sum(detail.amount) as sum FROM `tabSalary Detail` as detail INNER JOIN `tabSalary Slip` as salary_slip @@ -1362,8 +1614,13 @@ class SalarySlip(TransactionBase): AND salary_slip.end_date < %(period_end_date)s AND salary_slip.name != %(docname)s AND salary_slip.docstatus = 1""", - {'employee': self.employee, 'component': component.salary_component, 'period_start_date': period_start_date, - 'period_end_date': period_end_date, 'docname': self.name} + { + "employee": self.employee, + "component": component.salary_component, + "period_start_date": period_start_date, + "period_end_date": period_end_date, + "docname": self.name, + }, ) year_to_date = flt(component_sum[0][0]) if component_sum else 0.0 @@ -1385,34 +1642,44 @@ class SalarySlip(TransactionBase): return period_start_date, period_end_date def add_leave_balances(self): - self.set('leave_details', []) + self.set("leave_details", []) - if frappe.db.get_single_value('Payroll Settings', 'show_leave_balances_in_salary_slip'): + if frappe.db.get_single_value("Payroll Settings", "show_leave_balances_in_salary_slip"): from erpnext.hr.doctype.leave_application.leave_application import get_leave_details + leave_details = get_leave_details(self.employee, self.end_date) - for leave_type, leave_values in iteritems(leave_details['leave_allocation']): - self.append('leave_details', { - 'leave_type': leave_type, - 'total_allocated_leaves': flt(leave_values.get('total_leaves')), - 'expired_leaves': flt(leave_values.get('expired_leaves')), - 'used_leaves': flt(leave_values.get('leaves_taken')), - 'pending_leaves': flt(leave_values.get('leaves_pending_approval')), - 'available_leaves': flt(leave_values.get('remaining_leaves')) - }) + for leave_type, leave_values in iteritems(leave_details["leave_allocation"]): + self.append( + "leave_details", + { + "leave_type": leave_type, + "total_allocated_leaves": flt(leave_values.get("total_leaves")), + "expired_leaves": flt(leave_values.get("expired_leaves")), + "used_leaves": flt(leave_values.get("leaves_taken")), + "pending_leaves": flt(leave_values.get("leaves_pending_approval")), + "available_leaves": flt(leave_values.get("remaining_leaves")), + }, + ) + def unlink_ref_doc_from_salary_slip(ref_no): - linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` - where journal_entry=%s and docstatus < 2""", (ref_no)) + linked_ss = frappe.db.sql_list( + """select name from `tabSalary Slip` + where journal_entry=%s and docstatus < 2""", + (ref_no), + ) if linked_ss: for ss in linked_ss: ss_doc = frappe.get_doc("Salary Slip", ss) frappe.db.set_value("Salary Slip", ss_doc.name, "journal_entry", "") + def generate_password_for_pdf(policy_template, employee): employee = frappe.get_doc("Employee", employee) return policy_template.format(**employee.as_dict()) + def get_salary_component_data(component): return frappe.get_value( "Salary Component", @@ -1429,10 +1696,15 @@ def get_salary_component_data(component): as_dict=1, ) + def get_payroll_payable_account(company, payroll_entry): if payroll_entry: - payroll_payable_account = frappe.db.get_value('Payroll Entry', payroll_entry, 'payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Payroll Entry", payroll_entry, "payroll_payable_account" + ) else: - payroll_payable_account = frappe.db.get_value('Company', company, 'default_payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Company", company, "default_payroll_payable_account" + ) - return payroll_payable_account \ No newline at end of file + return payroll_payable_account diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index c989965ac59..2a68d9b979a 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -40,16 +40,14 @@ class TestSalarySlip(unittest.TestCase): setup_test() frappe.flags.pop("via_payroll_entry", None) - def tearDown(self): frappe.db.rollback() frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") - @change_settings("Payroll Settings", { - "payroll_based_on": "Attendance", - "daily_wages_fraction_for_half_day": 0.75 - }) + @change_settings( + "Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75} + ) def test_payment_days_based_on_attendance(self): no_of_days = self.get_no_of_days() @@ -61,21 +59,50 @@ class TestSalarySlip(unittest.TestCase): month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] - mark_attendance(emp_id, first_sunday, 'Absent', ignore_validate=True) # invalid lwp - mark_attendance(emp_id, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent - mark_attendance(emp_id, add_days(first_sunday, 2), 'Half Day', leave_type='Leave Without Pay', ignore_validate=True) # valid 0.75 lwp - mark_attendance(emp_id, add_days(first_sunday, 3), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # valid lwp - mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp - mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, first_sunday, "Absent", ignore_validate=True) # invalid lwp + mark_attendance( + emp_id, add_days(first_sunday, 1), "Absent", ignore_validate=True + ) # counted as absent + mark_attendance( + emp_id, + add_days(first_sunday, 2), + "Half Day", + leave_type="Leave Without Pay", + ignore_validate=True, + ) # valid 0.75 lwp + mark_attendance( + emp_id, + add_days(first_sunday, 3), + "On Leave", + leave_type="Leave Without Pay", + ignore_validate=True, + ) # valid lwp + mark_attendance( + emp_id, add_days(first_sunday, 4), "On Leave", leave_type="Casual Leave", ignore_validate=True + ) # invalid lwp + mark_attendance( + emp_id, + add_days(first_sunday, 7), + "On Leave", + leave_type="Leave Without Pay", + ignore_validate=True, + ) # invalid lwp - ss = make_employee_salary_slip("test_payment_days_based_on_attendance@salary.com", "Monthly", "Test Payment Based On Attendence") + ss = make_employee_salary_slip( + "test_payment_days_based_on_attendance@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) self.assertEqual(ss.leave_without_pay, 1.25) self.assertEqual(ss.absent_days, 1) @@ -85,16 +112,21 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 2.25) - #Gross pay calculation based on attendances - gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay + ss.absent_days)) + # Gross pay calculation based on attendances + gross_pay = 78000 - ( + (78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay + ss.absent_days) + ) self.assertEqual(ss.gross_pay, gross_pay) - @change_settings("Payroll Settings", { - "payroll_based_on": "Attendance", - "consider_unmarked_attendance_as": "Absent", - "include_holidays_in_total_working_days": True - }) + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": True, + }, + ) def test_payment_days_for_mid_joinee_including_holidays(self): from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday @@ -103,31 +135,38 @@ class TestSalarySlip(unittest.TestCase): new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) - frappe.db.set_value("Employee", new_emp_id, { - "date_of_joining": joining_date, - "relieving_date": relieving_date, - "status": "Left" - }) + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, + ) holidays = 0 for days in range(date_diff(relieving_date, joining_date) + 1): date = add_days(joining_date, days) if not is_holiday("Salary Slip Test Holiday List", date): - mark_attendance(new_emp_id, date, 'Present', ignore_validate=True) + mark_attendance(new_emp_id, date, "Present", ignore_validate=True) else: holidays += 1 - new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence") + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) self.assertEqual(new_ss.total_working_days, no_of_days[0]) self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8) - @change_settings("Payroll Settings", { - "payroll_based_on": "Attendance", - "consider_unmarked_attendance_as": "Absent", - "include_holidays_in_total_working_days": False - }) + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": False, + }, + ) def test_payment_days_for_mid_joinee_excluding_holidays(self): from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday @@ -136,29 +175,31 @@ class TestSalarySlip(unittest.TestCase): new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) - frappe.db.set_value("Employee", new_emp_id, { - "date_of_joining": joining_date, - "relieving_date": relieving_date, - "status": "Left" - }) + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, + ) holidays = 0 for days in range(date_diff(relieving_date, joining_date) + 1): date = add_days(joining_date, days) if not is_holiday("Salary Slip Test Holiday List", date): - mark_attendance(new_emp_id, date, 'Present', ignore_validate=True) + mark_attendance(new_emp_id, date, "Present", ignore_validate=True) else: holidays += 1 - new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence") + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1]) self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8) - @change_settings("Payroll Settings", { - "payroll_based_on": "Leave" - }) + @change_settings("Payroll Settings", {"payroll_based_on": "Leave"}) def test_payment_days_based_on_leave_application(self): no_of_days = self.get_no_of_days() @@ -170,30 +211,41 @@ class TestSalarySlip(unittest.TestCase): month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") - leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1) + leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1) leave_type_ppl.save() alloc = create_leave_allocation( - employee = emp_id, from_date = add_days(first_sunday, 4), - to_date = add_days(first_sunday, 10), new_leaves_allocated = 3, - leave_type = "Test Partially Paid Leave") + employee=emp_id, + from_date=add_days(first_sunday, 4), + to_date=add_days(first_sunday, 10), + new_leaves_allocated=3, + leave_type="Test Partially Paid Leave", + ) alloc.save() alloc.submit() - #two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp - make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave") - - ss = make_employee_salary_slip("test_payment_days_based_on_leave_application@salary.com", "Monthly", "Test Payment Based On Leave Application") + # two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp + make_leave_application( + emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave" + ) + ss = make_employee_salary_slip( + "test_payment_days_based_on_leave_application@salary.com", + "Monthly", + "Test Payment Based On Leave Application", + ) self.assertEqual(ss.leave_without_pay, 4) @@ -202,9 +254,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4) - @change_settings("Payroll Settings", { - "payroll_based_on": "Attendance" - }) + @change_settings("Payroll Settings", {"payroll_based_on": "Attendance"}) def test_payment_days_in_salary_slip_based_on_timesheet(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.projects.doctype.timesheet.test_timesheet import ( @@ -215,21 +265,30 @@ class TestSalarySlip(unittest.TestCase): make_salary_slip as make_salary_slip_for_timesheet, ) - emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List") + emp = make_employee( + "test_employee_timesheet@salary.com", + company="_Test Company", + holiday_list="Salary Slip Test Holiday List", + ) frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"}) # mark attendance month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] - mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + mark_attendance( + emp, add_days(first_sunday, 1), "Absent", ignore_validate=True + ) # counted as absent # salary structure based on timesheet make_salary_structure_for_timesheet(emp) @@ -248,13 +307,14 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1) # gross pay calculation based on attendance (payment days) - gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days)) + gross_pay = 78100 - ( + (78000 / (days_in_month - no_of_holidays)) + * flt(salary_slip.leave_without_pay + salary_slip.absent_days) + ) self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2)) - @change_settings("Payroll Settings", { - "payroll_based_on": "Attendance" - }) + @change_settings("Payroll Settings", {"payroll_based_on": "Attendance"}) def test_component_amount_dependent_on_another_payment_days_based_component(self): from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( @@ -266,23 +326,32 @@ class TestSalarySlip(unittest.TestCase): employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company") # base = 50000 - create_salary_structure_assignment(employee, salary_structure.name, company="_Test Company", currency="INR") + create_salary_structure_assignment( + employee, salary_structure.name, company="_Test Company", currency="INR" + ) # mark employee absent for a day since this case works fine if payment days are equal to working days month_start_date = get_first_day(nowdate()) month_end_date = get_last_day(nowdate()) - first_sunday = frappe.db.sql(""" + first_sunday = frappe.db.sql( + """ select holiday_date from `tabHoliday` where parent = 'Salary Slip Test Holiday List' and holiday_date between %s and %s order by holiday_date - """, (month_start_date, month_end_date))[0][0] + """, + (month_start_date, month_end_date), + )[0][0] - mark_attendance(employee, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent + mark_attendance( + employee, add_days(first_sunday, 1), "Absent", ignore_validate=True + ) # counted as absent # make salary slip and assert payment days - ss = make_salary_slip_for_payment_days_dependency_test("test_payment_days_based_component@salary.com", salary_structure.name) + ss = make_salary_slip_for_payment_days_dependency_test( + "test_payment_days_based_component@salary.com", salary_structure.name + ) self.assertEqual(ss.absent_days, 1) days_in_month = no_of_days[0] @@ -308,17 +377,31 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(actual_amount, expected_amount) - @change_settings("Payroll Settings", { - "include_holidays_in_total_working_days": 1 - }) + @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() make_employee("test_salary_slip_with_holidays_included@salary.com") - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_salary_slip_with_holidays_included@salary.com", "Monthly", "Test Salary Slip With Holidays Included") + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_included@salary.com"}, "name" + ), + "relieving_date", + None, + ) + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_included@salary.com"}, "name" + ), + "status", + "Active", + ) + ss = make_employee_salary_slip( + "test_salary_slip_with_holidays_included@salary.com", + "Monthly", + "Test Salary Slip With Holidays Included", + ) self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, no_of_days[0]) @@ -326,17 +409,31 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.gross_pay, 78000) - @change_settings("Payroll Settings", { - "include_holidays_in_total_working_days": 0 - }) + @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0}) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() make_employee("test_salary_slip_with_holidays_excluded@salary.com") - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "status", "Active") - ss = make_employee_salary_slip("test_salary_slip_with_holidays_excluded@salary.com", "Monthly", "Test Salary Slip With Holidays Excluded") + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_excluded@salary.com"}, "name" + ), + "relieving_date", + None, + ) + frappe.db.set_value( + "Employee", + frappe.get_value( + "Employee", {"employee_name": "test_salary_slip_with_holidays_excluded@salary.com"}, "name" + ), + "status", + "Active", + ) + ss = make_employee_salary_slip( + "test_salary_slip_with_holidays_excluded@salary.com", + "Monthly", + "Test Salary Slip With Holidays Excluded", + ) self.assertEqual(ss.total_working_days, no_of_days[0] - no_of_days[1]) self.assertEqual(ss.payment_days, no_of_days[0] - no_of_days[1]) @@ -345,9 +442,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[1].amount, 3000) self.assertEqual(ss.gross_pay, 78000) - @change_settings("Payroll Settings", { - "include_holidays_in_total_working_days": 1 - }) + @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) def test_payment_days(self): from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( create_salary_structure_assignment, @@ -358,23 +453,23 @@ class TestSalarySlip(unittest.TestCase): # set joinng date in the same month employee = make_employee("test_payment_days@salary.com") if getdate(nowdate()).day >= 15: - relieving_date = getdate(add_days(nowdate(),-10)) - date_of_joining = getdate(add_days(nowdate(),-10)) + relieving_date = getdate(add_days(nowdate(), -10)) + date_of_joining = getdate(add_days(nowdate(), -10)) elif getdate(nowdate()).day < 15 and getdate(nowdate()).day >= 5: - date_of_joining = getdate(add_days(nowdate(),-3)) - relieving_date = getdate(add_days(nowdate(),-3)) + date_of_joining = getdate(add_days(nowdate(), -3)) + relieving_date = getdate(add_days(nowdate(), -3)) elif getdate(nowdate()).day < 5 and not getdate(nowdate()).day == 1: - date_of_joining = getdate(add_days(nowdate(),-1)) - relieving_date = getdate(add_days(nowdate(),-1)) + date_of_joining = getdate(add_days(nowdate(), -1)) + relieving_date = getdate(add_days(nowdate(), -1)) elif getdate(nowdate()).day == 1: date_of_joining = getdate(nowdate()) relieving_date = getdate(nowdate()) - frappe.db.set_value("Employee", employee, { - "date_of_joining": date_of_joining, - "relieving_date": None, - "status": "Active" - }) + frappe.db.set_value( + "Employee", + employee, + {"date_of_joining": date_of_joining, "relieving_date": None, "status": "Active"}, + ) salary_structure = "Test Payment Days" ss = make_employee_salary_slip("test_payment_days@salary.com", "Monthly", salary_structure) @@ -383,11 +478,15 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, (no_of_days[0] - getdate(date_of_joining).day + 1)) # set relieving date in the same month - frappe.db.set_value("Employee", employee, { - "date_of_joining": add_days(nowdate(),-60), - "relieving_date": relieving_date, - "status": "Left" - }) + frappe.db.set_value( + "Employee", + employee, + { + "date_of_joining": add_days(nowdate(), -60), + "relieving_date": relieving_date, + "status": "Left", + }, + ) if date_of_joining.day > 1: self.assertRaises(frappe.ValidationError, ss.save) @@ -399,21 +498,31 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.total_working_days, no_of_days[0]) self.assertEqual(ss.payment_days, getdate(relieving_date).day) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "relieving_date", None) - frappe.db.set_value("Employee", frappe.get_value("Employee", - {"employee_name":"test_payment_days@salary.com"}, "name"), "status", "Active") + frappe.db.set_value( + "Employee", + frappe.get_value("Employee", {"employee_name": "test_payment_days@salary.com"}, "name"), + "relieving_date", + None, + ) + frappe.db.set_value( + "Employee", + frappe.get_value("Employee", {"employee_name": "test_payment_days@salary.com"}, "name"), + "status", + "Active", + ) def test_employee_salary_slip_read_permission(self): make_employee("test_employee_salary_slip_read_permission@salary.com") - salary_slip_test_employee = make_employee_salary_slip("test_employee_salary_slip_read_permission@salary.com", "Monthly", "Test Employee Salary Slip Read Permission") + salary_slip_test_employee = make_employee_salary_slip( + "test_employee_salary_slip_read_permission@salary.com", + "Monthly", + "Test Employee Salary Slip Read Permission", + ) frappe.set_user("test_employee_salary_slip_read_permission@salary.com") self.assertTrue(salary_slip_test_employee.has_permission("read")) - @change_settings("Payroll Settings", { - "email_salary_slip_to_employee": 1 - }) + @change_settings("Payroll Settings", {"email_salary_slip_to_employee": 1}) def test_email_salary_slip(self): frappe.db.delete("Email Queue") @@ -444,34 +553,58 @@ class TestSalarySlip(unittest.TestCase): create_loan_accounts() - create_loan_type("Car Loan", 500000, 8.4, + create_loan_type( + "Car Loan", + 500000, + 8.4, is_term_loan=1, - mode_of_payment='Cash', - disbursement_account='Disbursement Account - _TC', - payment_account='Payment Account - _TC', - loan_account='Loan Account - _TC', - interest_income_account='Interest Income Account - _TC', - penalty_income_account='Penalty Income Account - _TC') + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + ) payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") - make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', - payroll_period=payroll_period) + make_salary_structure( + "Test Loan Repayment Salary Structure", + "Monthly", + employee=applicant, + currency="INR", + payroll_period=payroll_period, + ) - frappe.db.sql("delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'") - loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) + frappe.db.sql( + "delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'" + ) + loan = create_loan( + applicant, + "Car Loan", + 11000, + "Repay Over Number of Periods", + 20, + posting_date=add_months(nowdate(), -1), + ) loan.repay_from_salary = 1 loan.submit() - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) + make_loan_disbursement_entry( + loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1) + ) process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - ss = make_employee_salary_slip("test_loan_repayment_salary_slip@salary.com", "Monthly", "Test Loan Repayment Salary Structure") + ss = make_employee_salary_slip( + "test_loan_repayment_salary_slip@salary.com", "Monthly", "Test Loan Repayment Salary Structure" + ) ss.submit() self.assertEqual(ss.total_loan_repayment, 592) - self.assertEqual(ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment)))) + self.assertEqual( + ss.net_pay, (flt(ss.gross_pay) - (flt(ss.total_deduction) + flt(ss.total_loan_repayment))) + ) def test_payroll_frequency(self): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())[0] @@ -480,18 +613,22 @@ class TestSalarySlip(unittest.TestCase): for payroll_frequency in ["Monthly", "Bimonthly", "Fortnightly", "Weekly", "Daily"]: make_employee(payroll_frequency + "_test_employee@salary.com") - ss = make_employee_salary_slip(payroll_frequency + "_test_employee@salary.com", payroll_frequency, payroll_frequency + "_Test Payroll Frequency") + ss = make_employee_salary_slip( + payroll_frequency + "_test_employee@salary.com", + payroll_frequency, + payroll_frequency + "_Test Payroll Frequency", + ) if payroll_frequency == "Monthly": - self.assertEqual(ss.end_date, m['month_end_date']) + self.assertEqual(ss.end_date, m["month_end_date"]) elif payroll_frequency == "Bimonthly": if getdate(ss.start_date).day <= 15: - self.assertEqual(ss.end_date, m['month_mid_end_date']) + self.assertEqual(ss.end_date, m["month_mid_end_date"]) else: - self.assertEqual(ss.end_date, m['month_end_date']) + self.assertEqual(ss.end_date, m["month_end_date"]) elif payroll_frequency == "Fortnightly": - self.assertEqual(ss.end_date, add_days(nowdate(),13)) + self.assertEqual(ss.end_date, add_days(nowdate(), 13)) elif payroll_frequency == "Weekly": - self.assertEqual(ss.end_date, add_days(nowdate(),6)) + self.assertEqual(ss.end_date, add_days(nowdate(), 6)) elif payroll_frequency == "Daily": self.assertEqual(ss.end_date, nowdate()) @@ -499,14 +636,22 @@ class TestSalarySlip(unittest.TestCase): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure applicant = make_employee("test_multi_currency_salary_slip@salary.com", company="_Test Company") - frappe.db.sql("""delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""") - salary_structure = make_salary_structure("Test Multi Currency Salary Slip", "Monthly", employee=applicant, company="_Test Company", currency='USD') - salary_slip = make_salary_slip(salary_structure.name, employee = applicant) + frappe.db.sql( + """delete from `tabSalary Structure` where name='Test Multi Currency Salary Slip'""" + ) + salary_structure = make_salary_structure( + "Test Multi Currency Salary Slip", + "Monthly", + employee=applicant, + company="_Test Company", + currency="USD", + ) + salary_slip = make_salary_slip(salary_structure.name, employee=applicant) salary_slip.exchange_rate = 70 salary_slip.calculate_net_pay() self.assertEqual(salary_slip.gross_pay, 78000) - self.assertEqual(salary_slip.base_gross_pay, 78000*70) + self.assertEqual(salary_slip.base_gross_pay, 78000 * 70) def test_year_to_date_computation(self): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure @@ -515,20 +660,36 @@ class TestSalarySlip(unittest.TestCase): payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") - create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), - company="_Test Company") + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) - salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", - "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Monthly Salary Structure Test for Salary Slip YTD", + "Monthly", + employee=applicant, + company="_Test Company", + currency="INR", + payroll_period=payroll_period, + ) # clear salary slip for this employee frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") - create_salary_slips_for_payroll_period(applicant, salary_structure.name, - payroll_period, deduct_random=False, num=6) + create_salary_slips_for_payroll_period( + applicant, salary_structure.name, payroll_period, deduct_random=False, num=6 + ) - salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': - 'test_ytd@salary.com'}, order_by = 'posting_date') + salary_slips = frappe.get_all( + "Salary Slip", + fields=["year_to_date", "net_pay"], + filters={"employee_name": "test_ytd@salary.com"}, + order_by="posting_date", + ) year_to_date = 0 for slip in salary_slips: @@ -543,20 +704,36 @@ class TestSalarySlip(unittest.TestCase): payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") - create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"), - company="_Test Company") + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) - salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD", - "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Monthly Salary Structure Test for Salary Slip YTD", + "Monthly", + employee=applicant, + company="_Test Company", + currency="INR", + payroll_period=payroll_period, + ) # clear salary slip for this employee frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = '%s'" % employee_name) - create_salary_slips_for_payroll_period(applicant, salary_structure.name, - payroll_period, deduct_random=False, num=3) + create_salary_slips_for_payroll_period( + applicant, salary_structure.name, payroll_period, deduct_random=False, num=3 + ) - salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name": - employee_name}, order_by="posting_date") + salary_slips = frappe.get_all( + "Salary Slip", + fields=["name"], + filters={"employee_name": employee_name}, + order_by="posting_date", + ) year_to_date = dict() for slip in salary_slips: @@ -587,21 +764,27 @@ class TestSalarySlip(unittest.TestCase): "Employee Tax Exemption Declaration", "Employee Tax Exemption Proof Submission", "Employee Benefit Claim", - "Salary Structure Assignment" + "Salary Structure Assignment", ] for doc in delete_docs: frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee)) from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - salary_structure = make_salary_structure("Stucture to test tax", "Monthly", - other_details={"max_benefits": 100000}, test_tax=True, - employee=employee, payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Stucture to test tax", + "Monthly", + other_details={"max_benefits": 100000}, + test_tax=True, + employee=employee, + payroll_period=payroll_period, + ) # create salary slip for whole period deducting tax only on last period # to find the total tax amount paid - create_salary_slips_for_payroll_period(employee, salary_structure.name, - payroll_period, deduct_random=False) + create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period, deduct_random=False + ) tax_paid = get_tax_paid_in_period(employee) annual_tax = 113589.0 @@ -616,8 +799,9 @@ class TestSalarySlip(unittest.TestCase): create_exemption_declaration(employee, payroll_period.name) # create for payroll deducting in random months - data["deducted_dates"] = create_salary_slips_for_payroll_period(employee, - salary_structure.name, payroll_period) + data["deducted_dates"] = create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period + ) tax_paid = get_tax_paid_in_period(employee) # No proof, benefit claim sumitted, total tax paid, should not change @@ -632,12 +816,14 @@ class TestSalarySlip(unittest.TestCase): # Submit benefit claim for total 50000 data["benefit-1"] = create_benefit_claim(employee, payroll_period, 15000, "Medical Allowance") - data["benefit-2"] = create_benefit_claim(employee, payroll_period, 35000, "Leave Travel Allowance") - + data["benefit-2"] = create_benefit_claim( + employee, payroll_period, 35000, "Leave Travel Allowance" + ) frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) - data["deducted_dates"] = create_salary_slips_for_payroll_period(employee, - salary_structure.name, payroll_period) + data["deducted_dates"] = create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period + ) tax_paid = get_tax_paid_in_period(employee) # total taxable income 416000, 166000 @ 5% ie. 8300 @@ -650,8 +836,9 @@ class TestSalarySlip(unittest.TestCase): # create additional salary of 150000 frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) data["additional-1"] = create_additional_salary(employee, payroll_period, 150000) - data["deducted_dates"] = create_salary_slips_for_payroll_period(employee, - salary_structure.name, payroll_period) + data["deducted_dates"] = create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period + ) # total taxable income 566000, 250000 @ 5%, 66000 @ 20%, 12500 + 13200 tax_paid = get_tax_paid_in_period(employee) @@ -680,20 +867,25 @@ class TestSalarySlip(unittest.TestCase): "Employee Tax Exemption Declaration", "Employee Tax Exemption Proof Submission", "Employee Benefit Claim", - "Salary Structure Assignment" + "Salary Structure Assignment", ] for doc in delete_docs: frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee)) from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - salary_structure = make_salary_structure("Stucture to test tax", "Monthly", - other_details={"max_benefits": 100000}, test_tax=True, - employee=employee, payroll_period=payroll_period) + salary_structure = make_salary_structure( + "Stucture to test tax", + "Monthly", + other_details={"max_benefits": 100000}, + test_tax=True, + employee=employee, + payroll_period=payroll_period, + ) - - create_salary_slips_for_payroll_period(employee, salary_structure.name, - payroll_period, deduct_random=False, num=3) + create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period, deduct_random=False, num=3 + ) tax_paid = get_tax_paid_in_period(employee) @@ -702,7 +894,7 @@ class TestSalarySlip(unittest.TestCase): frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) - #------------------------------------ + # ------------------------------------ # Recurring additional salary start_date = add_months(payroll_period.start_date, 3) end_date = add_months(payroll_period.start_date, 5) @@ -710,8 +902,9 @@ class TestSalarySlip(unittest.TestCase): frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) - create_salary_slips_for_payroll_period(employee, salary_structure.name, - payroll_period, deduct_random=False, num=4) + create_salary_slips_for_payroll_period( + employee, salary_structure.name, payroll_period, deduct_random=False, num=4 + ) tax_paid = get_tax_paid_in_period(employee) @@ -728,44 +921,50 @@ class TestSalarySlip(unittest.TestCase): activity_type.save() def get_no_of_days(self): - no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, - getdate(nowdate()).month) - no_of_holidays_in_month = len([1 for i in calendar.monthcalendar(getdate(nowdate()).year, - getdate(nowdate()).month) if i[6] != 0]) + no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month) + no_of_holidays_in_month = len( + [ + 1 + for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month) + if i[6] != 0 + ] + ) return [no_of_days_in_month[1], no_of_holidays_in_month] + def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value( + "Employee", {"user_id": user}, ["name", "company", "employee_name"], as_dict=True + ) - employee = frappe.db.get_value("Employee", - { - "user_id": user - }, - ["name", "company", "employee_name"], - as_dict=True) - - salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company) - salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) + salary_structure_doc = make_salary_structure( + salary_structure, payroll_frequency, employee=employee.name, company=employee.company + ) + salary_slip_name = frappe.db.get_value( + "Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})} + ) if not salary_slip_name: - salary_slip = make_salary_slip(salary_structure_doc.name, employee = employee.name) + salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name) salary_slip.employee_name = employee.employee_name salary_slip.payroll_frequency = payroll_frequency salary_slip.posting_date = nowdate() salary_slip.insert() else: - salary_slip = frappe.get_doc('Salary Slip', salary_slip_name) + salary_slip = frappe.get_doc("Salary Slip", salary_slip_name) return salary_slip + def make_salary_component(salary_components, test_tax, company_list=None): for salary_component in salary_components: - if frappe.db.exists('Salary Component', salary_component["salary_component"]): + if frappe.db.exists("Salary Component", salary_component["salary_component"]): continue if test_tax: @@ -785,6 +984,7 @@ def make_salary_component(salary_components, test_tax, company_list=None): get_salary_component_account(doc, company_list) + def get_salary_component_account(sal_comp, company_list=None): company = erpnext.get_default_company() @@ -796,7 +996,7 @@ def get_salary_component_account(sal_comp, company_list=None): if not sal_comp.get("accounts"): for d in company_list: - company_abbr = frappe.get_cached_value('Company', d, 'abbr') + company_abbr = frappe.get_cached_value("Company", d, "abbr") if sal_comp.type == "Earning": account_name = "Salary" @@ -805,177 +1005,202 @@ def get_salary_component_account(sal_comp, company_list=None): account_name = "Salary Deductions" parent_account = "Current Liabilities - " + company_abbr - sal_comp.append("accounts", { - "company": d, - "account": create_account(account_name, d, parent_account) - }) + sal_comp.append( + "accounts", {"company": d, "account": create_account(account_name, d, parent_account)} + ) sal_comp.save() + def create_account(account_name, company, parent_account, account_type=None): - company_abbr = frappe.get_cached_value('Company', company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") account = frappe.db.get_value("Account", account_name + " - " + company_abbr) if not account: - frappe.get_doc({ - "doctype": "Account", - "account_name": account_name, - "parent_account": parent_account, - "company": company - }).insert() + frappe.get_doc( + { + "doctype": "Account", + "account_name": account_name, + "parent_account": parent_account, + "company": company, + } + ).insert() return account + def make_earning_salary_component(setup=False, test_tax=False, company_list=None): data = [ { - "salary_component": 'Basic Salary', - "abbr":'BS', - "condition": 'base > 10000', - "formula": 'base', + "salary_component": "Basic Salary", + "abbr": "BS", + "condition": "base > 10000", + "formula": "base", "type": "Earning", - "amount_based_on_formula": 1 + "amount_based_on_formula": 1, }, + {"salary_component": "HRA", "abbr": "H", "amount": 3000, "type": "Earning"}, { - "salary_component": 'HRA', - "abbr":'H', - "amount": 3000, - "type": "Earning" - }, - { - "salary_component": 'Special Allowance', - "abbr":'SA', - "condition": 'H < 10000', - "formula": 'BS*.5', + "salary_component": "Special Allowance", + "abbr": "SA", + "condition": "H < 10000", + "formula": "BS*.5", "type": "Earning", - "amount_based_on_formula": 1 + "amount_based_on_formula": 1, }, - { - "salary_component": "Leave Encashment", - "abbr": 'LE', - "type": "Earning" - } + {"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"}, ] if test_tax: - data.extend([ - { - "salary_component": "Leave Travel Allowance", - "abbr": 'B', - "is_flexible_benefit": 1, - "type": "Earning", - "pay_against_benefit_claim": 1, - "max_benefit_amount": 100000, - "depends_on_payment_days": 0 - }, - { - "salary_component": "Medical Allowance", - "abbr": 'B', - "is_flexible_benefit": 1, - "pay_against_benefit_claim": 0, - "type": "Earning", - "max_benefit_amount": 15000 - }, - { - "salary_component": "Performance Bonus", - "abbr": 'B', - "type": "Earning" - } - ]) + data.extend( + [ + { + "salary_component": "Leave Travel Allowance", + "abbr": "B", + "is_flexible_benefit": 1, + "type": "Earning", + "pay_against_benefit_claim": 1, + "max_benefit_amount": 100000, + "depends_on_payment_days": 0, + }, + { + "salary_component": "Medical Allowance", + "abbr": "B", + "is_flexible_benefit": 1, + "pay_against_benefit_claim": 0, + "type": "Earning", + "max_benefit_amount": 15000, + }, + {"salary_component": "Performance Bonus", "abbr": "B", "type": "Earning"}, + ] + ) if setup or test_tax: make_salary_component(data, test_tax, company_list) - data.append({ - "salary_component": 'Basic Salary', - "abbr":'BS', - "condition": 'base < 10000', - "formula": 'base*.2', - "type": "Earning", - "amount_based_on_formula": 1 - }) + data.append( + { + "salary_component": "Basic Salary", + "abbr": "BS", + "condition": "base < 10000", + "formula": "base*.2", + "type": "Earning", + "amount_based_on_formula": 1, + } + ) return data + def make_deduction_salary_component(setup=False, test_tax=False, company_list=None): - data = [ + data = [ { - "salary_component": 'Professional Tax', - "abbr":'PT', + "salary_component": "Professional Tax", + "abbr": "PT", "type": "Deduction", "amount": 200, - "exempted_from_income_tax": 1 - + "exempted_from_income_tax": 1, } ] if not test_tax: - data.append({ - "salary_component": 'TDS', - "abbr":'T', - "condition": 'employment_type=="Intern"', - "type": "Deduction", - "round_to_the_nearest_integer": 1 - }) + data.append( + { + "salary_component": "TDS", + "abbr": "T", + "condition": 'employment_type=="Intern"', + "type": "Deduction", + "round_to_the_nearest_integer": 1, + } + ) else: - data.append({ - "salary_component": 'TDS', - "abbr":'T', - "type": "Deduction", - "depends_on_payment_days": 0, - "variable_based_on_taxable_salary": 1, - "round_to_the_nearest_integer": 1 - }) + data.append( + { + "salary_component": "TDS", + "abbr": "T", + "type": "Deduction", + "depends_on_payment_days": 0, + "variable_based_on_taxable_salary": 1, + "round_to_the_nearest_integer": 1, + } + ) if setup or test_tax: make_salary_component(data, test_tax, company_list) return data + def get_tax_paid_in_period(employee): - tax_paid_amount = frappe.db.sql("""select sum(sd.amount) from `tabSalary Detail` + tax_paid_amount = frappe.db.sql( + """select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss where ss.name=sd.parent and ss.employee=%s - and ss.docstatus=1 and sd.salary_component='TDS'""", (employee)) + and ss.docstatus=1 and sd.salary_component='TDS'""", + (employee), + ) return tax_paid_amount[0][0] + def create_exemption_declaration(employee, payroll_period): create_exemption_category() - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": employee, - "payroll_period": payroll_period, - "company": erpnext.get_default_company(), - "currency": erpnext.get_default_currency() - }) - declaration.append("declarations", { - "exemption_sub_category": "_Test Sub Category", - "exemption_category": "_Test Category", - "amount": 100000 - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "payroll_period": payroll_period, + "company": erpnext.get_default_company(), + "currency": erpnext.get_default_currency(), + } + ) + declaration.append( + "declarations", + { + "exemption_sub_category": "_Test Sub Category", + "exemption_category": "_Test Category", + "amount": 100000, + }, + ) declaration.submit() + def create_proof_submission(employee, payroll_period, amount): submission_date = add_months(payroll_period.start_date, random.randint(0, 11)) - proof_submission = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": employee, - "payroll_period": payroll_period.name, - "submission_date": submission_date, - "currency": erpnext.get_default_currency() - }) - proof_submission.append("tax_exemption_proofs", { - "exemption_sub_category": "_Test Sub Category", - "exemption_category": "_Test Category", - "type_of_proof": "Test", "amount": amount - }) + proof_submission = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": employee, + "payroll_period": payroll_period.name, + "submission_date": submission_date, + "currency": erpnext.get_default_currency(), + } + ) + proof_submission.append( + "tax_exemption_proofs", + { + "exemption_sub_category": "_Test Sub Category", + "exemption_category": "_Test Category", + "type_of_proof": "Test", + "amount": amount, + }, + ) proof_submission.submit() return submission_date + def create_benefit_claim(employee, payroll_period, amount, component): claim_date = add_months(payroll_period.start_date, random.randint(0, 11)) - frappe.get_doc({ - "doctype": "Employee Benefit Claim", - "employee": employee, - "claimed_amount": amount, - "claim_date": claim_date, - "earning_component": component, - "currency": erpnext.get_default_currency() - }).submit() + frappe.get_doc( + { + "doctype": "Employee Benefit Claim", + "employee": employee, + "claimed_amount": amount, + "claim_date": claim_date, + "earning_component": component, + "currency": erpnext.get_default_currency(), + } + ).submit() return claim_date -def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None, - company=None): + +def create_tax_slab( + payroll_period, + effective_date=None, + allow_tax_exemption=False, + dont_submit=False, + currency=None, + company=None, +): if not currency: currency = erpnext.get_default_currency() @@ -987,17 +1212,10 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = "from_amount": 250000, "to_amount": 500000, "percent_deduction": 5, - "condition": "annual_taxable_earning > 500000" + "condition": "annual_taxable_earning > 500000", }, - { - "from_amount": 500001, - "to_amount": 1000000, - "percent_deduction": 20 - }, - { - "from_amount": 1000001, - "percent_deduction": 30 - } + {"from_amount": 500001, "to_amount": 1000000, "percent_deduction": 20}, + {"from_amount": 1000001, "percent_deduction": 30}, ] income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency}) @@ -1005,7 +1223,7 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency) income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) - income_tax_slab.company = company or '' + income_tax_slab.company = company or "" income_tax_slab.currency = currency if allow_tax_exemption: @@ -1015,10 +1233,7 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = for item in slabs: income_tax_slab.append("slabs", item) - income_tax_slab.append("other_taxes_and_charges", { - "description": "cess", - "percent": 4 - }) + income_tax_slab.append("other_taxes_and_charges", {"description": "cess", "percent": 4}) income_tax_slab.save() if not dont_submit: @@ -1028,12 +1243,21 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = else: return income_tax_slab_name -def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12): + +def create_salary_slips_for_payroll_period( + employee, salary_structure, payroll_period, deduct_random=True, num=12 +): deducted_dates = [] i = 0 while i < num: - slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee, - "salary_structure": salary_structure, "frequency": "Monthly"}) + slip = frappe.get_doc( + { + "doctype": "Salary Slip", + "employee": employee, + "salary_structure": salary_structure, + "frequency": "Monthly", + } + ) if i == 0: posting_date = add_days(payroll_period.start_date, 25) else: @@ -1052,50 +1276,66 @@ def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_p i += 1 return deducted_dates + def create_additional_salary(employee, payroll_period, amount): salary_date = add_months(payroll_period.start_date, random.randint(0, 11)) - frappe.get_doc({ - "doctype": "Additional Salary", - "employee": employee, - "company": erpnext.get_default_company(), - "salary_component": "Performance Bonus", - "payroll_date": salary_date, - "amount": amount, - "type": "Earning", - "currency": erpnext.get_default_currency() - }).submit() + frappe.get_doc( + { + "doctype": "Additional Salary", + "employee": employee, + "company": erpnext.get_default_company(), + "salary_component": "Performance Bonus", + "payroll_date": salary_date, + "amount": amount, + "type": "Earning", + "currency": erpnext.get_default_currency(), + } + ).submit() return salary_date + def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True): - leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', - employee = employee, - leave_type = leave_type, - from_date = from_date, - to_date = to_date, - company = company or erpnext.get_default_company() or "_Test Company", - status = "Approved", - leave_approver = 'test@example.com' - )).insert() + leave_application = frappe.get_doc( + dict( + doctype="Leave Application", + employee=employee, + leave_type=leave_type, + from_date=from_date, + to_date=to_date, + company=company or erpnext.get_default_company() or "_Test Company", + status="Approved", + leave_approver="test@example.com", + ) + ).insert() if submit: leave_application.submit() return leave_application + def setup_test(): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance", "Additional Salary"]: + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Attendance", + "Additional Salary", + ]: frappe.db.sql("delete from `tab%s`" % dt) make_holiday_list() - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value( + "Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List" + ) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) - frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) - frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) + frappe.db.set_value("HR Settings", None, "leave_status_notification_template", None) + frappe.db.set_value("HR Settings", None, "leave_approval_notification_template", None) + def make_holiday_list(list_name=None, from_date=None, to_date=None): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) @@ -1103,19 +1343,22 @@ def make_holiday_list(list_name=None, from_date=None, to_date=None): frappe.delete_doc_if_exists("Holiday List", name, force=True) - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": name, - "from_date": from_date or fiscal_year[1], - "to_date": to_date or fiscal_year[2], - "weekly_off": "Sunday" - }).insert() + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": name, + "from_date": from_date or fiscal_year[1], + "to_date": to_date or fiscal_year[2], + "weekly_off": "Sunday", + } + ).insert() holiday_list.get_weekly_off_dates() holiday_list.save() holiday_list = holiday_list.name return holiday_list + def make_salary_structure_for_payment_days_based_component_dependency(): earnings = [ { @@ -1123,7 +1366,7 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "abbr": "P_BS", "type": "Earning", "formula": "base", - "amount_based_on_formula": 1 + "amount_based_on_formula": 1, }, { "salary_component": "HRA - Payment Days", @@ -1131,8 +1374,8 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "type": "Earning", "depends_on_payment_days": 1, "amount_based_on_formula": 1, - "formula": "base * 0.20" - } + "formula": "base * 0.20", + }, ] make_salary_component(earnings, False, company_list=["_Test Company"]) @@ -1143,7 +1386,7 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "abbr": "P_PT", "type": "Deduction", "depends_on_payment_days": 1, - "amount": 200.00 + "amount": 200.00, }, { "salary_component": "P - Employee Provident Fund", @@ -1152,8 +1395,8 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "exempted_from_income_tax": 1, "amount_based_on_formula": 1, "depends_on_payment_days": 0, - "formula": "(gross_pay - P_HRA) * 0.12" - } + "formula": "(gross_pay - P_HRA) * 0.12", + }, ] make_salary_component(deductions, False, company_list=["_Test Company"]) @@ -1168,7 +1411,7 @@ def make_salary_structure_for_payment_days_based_component_dependency(): "company": "_Test Company", "payroll_frequency": "Monthly", "payment_account": get_random("Account", filters={"account_currency": "INR"}), - "currency": "INR" + "currency": "INR", } salary_structure_doc = frappe.get_doc(details) @@ -1184,14 +1427,15 @@ def make_salary_structure_for_payment_days_based_component_dependency(): return salary_structure_doc -def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure): - employee = frappe.db.get_value("Employee", { - "user_id": employee - }, - ["name", "company", "employee_name"], - as_dict=True) - salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})}) +def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure): + employee = frappe.db.get_value( + "Employee", {"user_id": employee}, ["name", "company", "employee_name"], as_dict=True + ) + + salary_slip_name = frappe.db.get_value( + "Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})} + ) if not salary_slip_name: salary_slip = make_salary_slip(salary_structure, employee=employee.name) @@ -1204,16 +1448,21 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure return salary_slip -def create_recurring_additional_salary(employee, salary_component, amount, from_date, to_date, company=None): - frappe.get_doc({ - "doctype": "Additional Salary", - "employee": employee, - "company": company or erpnext.get_default_company(), - "salary_component": salary_component, - "is_recurring": 1, - "from_date": from_date, - "to_date": to_date, - "amount": amount, - "type": "Earning", - "currency": erpnext.get_default_currency() - }).submit() + +def create_recurring_additional_salary( + employee, salary_component, amount, from_date, to_date, company=None +): + frappe.get_doc( + { + "doctype": "Additional Salary", + "employee": employee, + "company": company or erpnext.get_default_company(), + "salary_component": salary_component, + "is_recurring": 1, + "from_date": from_date, + "to_date": to_date, + "amount": amount, + "type": "Earning", + "currency": erpnext.get_default_currency(), + } + ).submit() diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index ae83c046a5e..c72ada630b0 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -20,12 +20,21 @@ class SalaryStructure(Document): self.validate_component_based_on_tax_slab() def set_missing_values(self): - overwritten_fields = ["depends_on_payment_days", "variable_based_on_taxable_salary", "is_tax_applicable", "is_flexible_benefit"] + overwritten_fields = [ + "depends_on_payment_days", + "variable_based_on_taxable_salary", + "is_tax_applicable", + "is_flexible_benefit", + ] overwritten_fields_if_missing = ["amount_based_on_formula", "formula", "amount"] for table in ["earnings", "deductions"]: for d in self.get(table): - component_default_value = frappe.db.get_value("Salary Component", cstr(d.salary_component), - overwritten_fields + overwritten_fields_if_missing, as_dict=1) + component_default_value = frappe.db.get_value( + "Salary Component", + cstr(d.salary_component), + overwritten_fields + overwritten_fields_if_missing, + as_dict=1, + ) if component_default_value: for fieldname in overwritten_fields: value = component_default_value.get(fieldname) @@ -39,8 +48,11 @@ class SalaryStructure(Document): def validate_component_based_on_tax_slab(self): for row in self.deductions: if row.variable_based_on_taxable_salary and (row.amount or row.formula): - frappe.throw(_("Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary") - .format(row.idx, row.salary_component)) + frappe.throw( + _( + "Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary" + ).format(row.idx, row.salary_component) + ) def validate_amount(self): if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet: @@ -63,16 +75,23 @@ class SalaryStructure(Document): for earning_component in self.earnings: if earning_component.is_flexible_benefit == 1: have_a_flexi = True - max_of_component = frappe.db.get_value("Salary Component", earning_component.salary_component, "max_benefit_amount") + max_of_component = frappe.db.get_value( + "Salary Component", earning_component.salary_component, "max_benefit_amount" + ) flexi_amount += max_of_component if have_a_flexi and flt(self.max_benefits) == 0: frappe.throw(_("Max benefits should be greater than zero to dispense benefits")) if have_a_flexi and flexi_amount and flt(self.max_benefits) > flexi_amount: - frappe.throw(_("Total flexible benefit component amount {0} should not be less than max benefits {1}") - .format(flexi_amount, self.max_benefits)) + frappe.throw( + _( + "Total flexible benefit component amount {0} should not be less than max benefits {1}" + ).format(flexi_amount, self.max_benefits) + ) if not have_a_flexi and flt(self.max_benefits) > 0: - frappe.throw(_("Salary Structure should have flexible benefit component(s) to dispense benefit amount")) + frappe.throw( + _("Salary Structure should have flexible benefit component(s) to dispense benefit amount") + ) def get_employees(self, **kwargs): conditions, values = [], [] @@ -83,58 +102,117 @@ class SalaryStructure(Document): condition_str = " and " + " and ".join(conditions) if conditions else "" - employees = frappe.db.sql_list("select name from tabEmployee where status='Active' {condition}" - .format(condition=condition_str), tuple(values)) + employees = frappe.db.sql_list( + "select name from tabEmployee where status='Active' {condition}".format( + condition=condition_str + ), + tuple(values), + ) return employees @frappe.whitelist() - def assign_salary_structure(self, grade=None, department=None, designation=None, employee=None, - payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): - employees = self.get_employees(company= self.company, grade= grade,department= department,designation= designation,name=employee) + def assign_salary_structure( + self, + grade=None, + department=None, + designation=None, + employee=None, + payroll_payable_account=None, + from_date=None, + base=None, + variable=None, + income_tax_slab=None, + ): + employees = self.get_employees( + company=self.company, grade=grade, department=department, designation=designation, name=employee + ) if employees: if len(employees) > 20: - frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self, + frappe.enqueue( + assign_salary_structure_for_employees, + timeout=600, + employees=employees, + salary_structure=self, payroll_payable_account=payroll_payable_account, - from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) + from_date=from_date, + base=base, + variable=variable, + income_tax_slab=income_tax_slab, + ) else: - assign_salary_structure_for_employees(employees, self, + assign_salary_structure_for_employees( + employees, + self, payroll_payable_account=payroll_payable_account, - from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab) + from_date=from_date, + base=base, + variable=variable, + income_tax_slab=income_tax_slab, + ) else: frappe.msgprint(_("No Employee Found")) - -def assign_salary_structure_for_employees(employees, salary_structure, payroll_payable_account=None, from_date=None, base=None, variable=None, income_tax_slab=None): +def assign_salary_structure_for_employees( + employees, + salary_structure, + payroll_payable_account=None, + from_date=None, + base=None, + variable=None, + income_tax_slab=None, +): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) - count=0 + count = 0 for employee in employees: if employee in existing_assignments_for: continue - count +=1 + count += 1 - salary_structures_assignment = create_salary_structures_assignment(employee, - salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab) + salary_structures_assignment = create_salary_structures_assignment( + employee, salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab + ) salary_structures_assignments.append(salary_structures_assignment) - frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) + frappe.publish_progress( + count * 100 / len(set(employees) - set(existing_assignments_for)), + title=_("Assigning Structures..."), + ) if salary_structures_assignments: frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, payroll_payable_account, from_date, base, variable, income_tax_slab=None): +def create_salary_structures_assignment( + employee, + salary_structure, + payroll_payable_account, + from_date, + base, + variable, + income_tax_slab=None, +): if not payroll_payable_account: - payroll_payable_account = frappe.db.get_value('Company', salary_structure.company, 'default_payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Company", salary_structure.company, "default_payroll_payable_account" + ) if not payroll_payable_account: frappe.throw(_('Please set "Default Payroll Payable Account" in Company Defaults')) - payroll_payable_account_currency = frappe.db.get_value('Account', payroll_payable_account, 'account_currency') + payroll_payable_account_currency = frappe.db.get_value( + "Account", payroll_payable_account, "account_currency" + ) company_curency = erpnext.get_company_currency(salary_structure.company) - if payroll_payable_account_currency != salary_structure.currency and payroll_payable_account_currency != company_curency: - frappe.throw(_("Invalid Payroll Payable Account. The account currency must be {0} or {1}").format(salary_structure.currency, company_curency)) + if ( + payroll_payable_account_currency != salary_structure.currency + and payroll_payable_account_currency != company_curency + ): + frappe.throw( + _("Invalid Payroll Payable Account. The account currency must be {0} or {1}").format( + salary_structure.currency, company_curency + ) + ) assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee @@ -146,28 +224,48 @@ def create_salary_structures_assignment(employee, salary_structure, payroll_paya assignment.base = base assignment.variable = variable assignment.income_tax_slab = income_tax_slab - assignment.save(ignore_permissions = True) + assignment.save(ignore_permissions=True) assignment.submit() return assignment.name def get_existing_assignments(employees, salary_structure, from_date): - salary_structures_assignments = frappe.db.sql_list(""" + salary_structures_assignments = frappe.db.sql_list( + """ select distinct employee from `tabSalary Structure Assignment` where salary_structure=%s and employee in (%s) and from_date=%s and company= %s and docstatus=1 - """ % ('%s', ', '.join(['%s']*len(employees)),'%s', '%s'), [salary_structure.name] + employees+[from_date]+[salary_structure.company]) + """ + % ("%s", ", ".join(["%s"] * len(employees)), "%s", "%s"), + [salary_structure.name] + employees + [from_date] + [salary_structure.company], + ) if salary_structures_assignments: - frappe.msgprint(_("Skipping Salary Structure Assignment for the following employees, as Salary Structure Assignment records already exists against them. {0}") - .format("\n".join(salary_structures_assignments))) + frappe.msgprint( + _( + "Skipping Salary Structure Assignment for the following employees, as Salary Structure Assignment records already exists against them. {0}" + ).format("\n".join(salary_structures_assignments)) + ) return salary_structures_assignments + @frappe.whitelist() -def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0, ignore_permissions=False): +def make_salary_slip( + source_name, + target_doc=None, + employee=None, + as_print=False, + print_format=None, + for_preview=0, + ignore_permissions=False, +): def postprocess(source, target): if employee: - employee_details = frappe.db.get_value("Employee", employee, - ["employee_name", "branch", "designation", "department", "payroll_cost_center"], as_dict=1) + employee_details = frappe.db.get_value( + "Employee", + employee, + ["employee_name", "branch", "designation", "department", "payroll_cost_center"], + as_dict=1, + ) target.employee = employee target.employee_name = employee_details.employee_name target.branch = employee_details.branch @@ -175,35 +273,51 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = target.department = employee_details.department target.payroll_cost_center = employee_details.payroll_cost_center if not target.payroll_cost_center and target.department: - target.payroll_cost_center = frappe.db.get_value("Department", target.department, "payroll_cost_center") + target.payroll_cost_center = frappe.db.get_value( + "Department", target.department, "payroll_cost_center" + ) - target.run_method('process_salary_structure', for_preview=for_preview) + target.run_method("process_salary_structure", for_preview=for_preview) - doc = get_mapped_doc("Salary Structure", source_name, { - "Salary Structure": { - "doctype": "Salary Slip", - "field_map": { - "total_earning": "gross_pay", - "name": "salary_structure", - "currency": "currency" + doc = get_mapped_doc( + "Salary Structure", + source_name, + { + "Salary Structure": { + "doctype": "Salary Slip", + "field_map": { + "total_earning": "gross_pay", + "name": "salary_structure", + "currency": "currency", + }, } - } - }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) + }, + target_doc, + postprocess, + ignore_child_tables=True, + ignore_permissions=ignore_permissions, + ) if cint(as_print): - doc.name = 'Preview for {0}'.format(employee) - return frappe.get_print(doc.doctype, doc.name, doc = doc, print_format = print_format) + doc.name = "Preview for {0}".format(employee) + return frappe.get_print(doc.doctype, doc.name, doc=doc, print_format=print_format) else: return doc @frappe.whitelist() def get_employees(salary_structure): - employees = frappe.get_list('Salary Structure Assignment', - filters={'salary_structure': salary_structure, 'docstatus': 1}, fields=['employee']) + employees = frappe.get_list( + "Salary Structure Assignment", + filters={"salary_structure": salary_structure, "docstatus": 1}, + fields=["employee"], + ) if not employees: - frappe.throw(_("There's no Employee with Salary Structure: {0}. Assign {1} to an Employee to preview Salary Slip").format( - salary_structure, salary_structure)) + frappe.throw( + _( + "There's no Employee with Salary Structure: {0}. Assign {1} to an Employee to preview Salary Slip" + ).format(salary_structure, salary_structure) + ) return list(set([d.employee for d in employees])) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py b/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py index 27eb5ed8b11..cf363b410df 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure_dashboard.py @@ -1,17 +1,9 @@ - - def get_data(): return { - 'fieldname': 'salary_structure', - 'non_standard_fieldnames': { - 'Employee Grade': 'default_salary_structure' - }, - 'transactions': [ - { - 'items': ['Salary Structure Assignment', 'Salary Slip'] - }, - { - 'items': ['Employee Grade'] - }, - ] + "fieldname": "salary_structure", + "non_standard_fieldnames": {"Employee Grade": "default_salary_structure"}, + "transactions": [ + {"items": ["Salary Structure Assignment", "Salary Slip"]}, + {"items": ["Employee Grade"]}, + ], } diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e2d0d1c864c..def622bf80e 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -22,25 +22,33 @@ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salar test_dependencies = ["Fiscal Year"] + class TestSalaryStructure(unittest.TestCase): def setUp(self): for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment"]: frappe.db.sql("delete from `tab%s`" % dt) self.make_holiday_list() - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Structure Test Holiday List") + frappe.db.set_value( + "Company", + erpnext.get_default_company(), + "default_holiday_list", + "Salary Structure Test Holiday List", + ) make_employee("test_employee@salary.com") make_employee("test_employee_2@salary.com") def make_holiday_list(self): if not frappe.db.get_value("Holiday List", "Salary Structure Test Holiday List"): - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "Salary Structure Test Holiday List", - "from_date": nowdate(), - "to_date": add_years(nowdate(), 1), - "weekly_off": "Sunday" - }).insert() + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": "Salary Structure Test Holiday List", + "from_date": nowdate(), + "to_date": add_years(nowdate(), 1), + "weekly_off": "Sunday", + } + ).insert() holiday_list.get_weekly_off_dates() holiday_list.save() @@ -48,31 +56,33 @@ class TestSalaryStructure(unittest.TestCase): emp = make_employee("test_employee_3@salary.com") - sal_struct = make_salary_structure("Salary Structure 2", "Monthly", dont_submit = True) + sal_struct = make_salary_structure("Salary Structure 2", "Monthly", dont_submit=True) sal_struct.earnings = [sal_struct.earnings[0]] sal_struct.earnings[0].amount_based_on_formula = 1 - sal_struct.earnings[0].formula = "base" + sal_struct.earnings[0].formula = "base" sal_struct.deductions = [sal_struct.deductions[0]] sal_struct.deductions[0].amount_based_on_formula = 1 sal_struct.deductions[0].condition = "gross_pay > 100" - sal_struct.deductions[0].formula = "gross_pay * 0.2" + sal_struct.deductions[0].formula = "gross_pay * 0.2" sal_struct.submit() assignment = create_salary_structure_assignment(emp, "Salary Structure 2") - ss = make_salary_slip(sal_struct.name, employee = emp) + ss = make_salary_slip(sal_struct.name, employee=emp) self.assertEqual(assignment.base * 0.2, ss.deductions[0].amount) def test_amount_totals(self): frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) - sal_slip = frappe.get_value("Salary Slip", {"employee_name":"test_employee_2@salary.com"}) + sal_slip = frappe.get_value("Salary Slip", {"employee_name": "test_employee_2@salary.com"}) if not sal_slip: - sal_slip = make_employee_salary_slip("test_employee_2@salary.com", "Monthly", "Salary Structure Sample") - self.assertEqual(sal_slip.get("salary_structure"), 'Salary Structure Sample') + sal_slip = make_employee_salary_slip( + "test_employee_2@salary.com", "Monthly", "Salary Structure Sample" + ) + self.assertEqual(sal_slip.get("salary_structure"), "Salary Structure Sample") self.assertEqual(sal_slip.get("earnings")[0].amount, 50000) self.assertEqual(sal_slip.get("earnings")[1].amount, 3000) self.assertEqual(sal_slip.get("earnings")[2].amount, 25000) @@ -84,12 +94,12 @@ class TestSalaryStructure(unittest.TestCase): salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", dont_submit=True) for row in salary_structure.earnings: - row.formula = "\n%s\n\n"%row.formula - row.condition = "\n%s\n\n"%row.condition + row.formula = "\n%s\n\n" % row.formula + row.condition = "\n%s\n\n" % row.condition for row in salary_structure.deductions: - row.formula = "\n%s\n\n"%row.formula - row.condition = "\n%s\n\n"%row.condition + row.formula = "\n%s\n\n" % row.formula + row.condition = "\n%s\n\n" % row.condition salary_structure.save() @@ -101,29 +111,47 @@ class TestSalaryStructure(unittest.TestCase): def test_salary_structures_assignment(self): company_currency = erpnext.get_default_currency() - salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", currency=company_currency) + salary_structure = make_salary_structure( + "Salary Structure Sample", "Monthly", currency=company_currency + ) employee = "test_assign_stucture@salary.com" employee_doc_name = make_employee(employee) # clear the already assigned stuctures - frappe.db.sql('''delete from `tabSalary Structure Assignment` where employee=%s and salary_structure=%s ''', - ("test_assign_stucture@salary.com",salary_structure.name)) - #test structure_assignment - salary_structure.assign_salary_structure(employee=employee_doc_name,from_date='2013-01-01',base=5000,variable=200) - salary_structure_assignment = frappe.get_doc("Salary Structure Assignment",{'employee':employee_doc_name, 'from_date':'2013-01-01'}) + frappe.db.sql( + """delete from `tabSalary Structure Assignment` where employee=%s and salary_structure=%s """, + ("test_assign_stucture@salary.com", salary_structure.name), + ) + # test structure_assignment + salary_structure.assign_salary_structure( + employee=employee_doc_name, from_date="2013-01-01", base=5000, variable=200 + ) + salary_structure_assignment = frappe.get_doc( + "Salary Structure Assignment", {"employee": employee_doc_name, "from_date": "2013-01-01"} + ) self.assertEqual(salary_structure_assignment.docstatus, 1) self.assertEqual(salary_structure_assignment.base, 5000) self.assertEqual(salary_structure_assignment.variable, 200) def test_multi_currency_salary_structure(self): make_employee("test_muti_currency_employee@salary.com") - sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency='USD') - self.assertEqual(sal_struct.currency, 'USD') + sal_struct = make_salary_structure("Salary Structure Multi Currency", "Monthly", currency="USD") + self.assertEqual(sal_struct.currency, "USD") -def make_salary_structure(salary_structure, payroll_frequency, employee=None, - from_date=None, dont_submit=False, other_details=None,test_tax=False, - company=None, currency=erpnext.get_default_currency(), payroll_period=None): + +def make_salary_structure( + salary_structure, + payroll_frequency, + employee=None, + from_date=None, + dont_submit=False, + other_details=None, + test_tax=False, + company=None, + currency=erpnext.get_default_currency(), + payroll_period=None, +): if test_tax: - frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) + frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure)) if frappe.db.exists("Salary Structure", salary_structure): frappe.db.delete("Salary Structure", salary_structure) @@ -132,11 +160,15 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, "doctype": "Salary Structure", "name": salary_structure, "company": company or erpnext.get_default_company(), - "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), - "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]), + "earnings": make_earning_salary_component( + setup=True, test_tax=test_tax, company_list=["_Test Company"] + ), + "deductions": make_deduction_salary_component( + setup=True, test_tax=test_tax, company_list=["_Test Company"] + ), "payroll_frequency": payroll_frequency, - "payment_account": get_random("Account", filters={'account_currency': currency}), - "currency": currency + "payment_account": get_random("Account", filters={"account_currency": currency}), + "currency": currency, } if other_details and isinstance(other_details, dict): details.update(other_details) @@ -145,31 +177,41 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, if not dont_submit: salary_structure_doc.submit() - filters = {'employee':employee, 'docstatus': 1} + filters = {"employee": employee, "docstatus": 1} if not from_date and payroll_period: from_date = payroll_period.start_date if from_date: - filters['from_date'] = from_date + filters["from_date"] = from_date - if employee and not frappe.db.get_value("Salary Structure Assignment", - filters) and salary_structure_doc.docstatus==1: + if ( + employee + and not frappe.db.get_value("Salary Structure Assignment", filters) + and salary_structure_doc.docstatus == 1 + ): create_salary_structure_assignment( employee, salary_structure, from_date=from_date, company=company, currency=currency, - payroll_period=payroll_period + payroll_period=payroll_period, ) return salary_structure_doc -def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(), - payroll_period=None): + +def create_salary_structure_assignment( + employee, + salary_structure, + from_date=None, + company=None, + currency=erpnext.get_default_currency(), + payroll_period=None, +): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): - frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) + frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee)) if not payroll_period: payroll_period = create_payroll_period() @@ -200,6 +242,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non salary_structure_assignment.submit() return salary_structure_assignment + def get_payable_account(company=None): if not company: company = erpnext.get_default_company() diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index e1ff9ca9f04..e34e48e6c05 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -8,7 +8,9 @@ from frappe.model.document import Document from frappe.utils import getdate -class DuplicateAssignment(frappe.ValidationError): pass +class DuplicateAssignment(frappe.ValidationError): + pass + class SalaryStructureAssignment(Document): def validate(self): @@ -17,56 +19,90 @@ class SalaryStructureAssignment(Document): self.set_payroll_payable_account() def validate_dates(self): - joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + joining_date, relieving_date = frappe.db.get_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) if self.from_date: - if frappe.db.exists("Salary Structure Assignment", {"employee": self.employee, "from_date": self.from_date, "docstatus": 1}): + if frappe.db.exists( + "Salary Structure Assignment", + {"employee": self.employee, "from_date": self.from_date, "docstatus": 1}, + ): frappe.throw(_("Salary Structure Assignment for Employee already exists"), DuplicateAssignment) if joining_date and getdate(self.from_date) < joining_date: - frappe.throw(_("From Date {0} cannot be before employee's joining Date {1}") - .format(self.from_date, joining_date)) + frappe.throw( + _("From Date {0} cannot be before employee's joining Date {1}").format( + self.from_date, joining_date + ) + ) # flag - old_employee is for migrating the old employees data via patch if relieving_date and getdate(self.from_date) > relieving_date and not self.flags.old_employee: - frappe.throw(_("From Date {0} cannot be after employee's relieving Date {1}") - .format(self.from_date, relieving_date)) + frappe.throw( + _("From Date {0} cannot be after employee's relieving Date {1}").format( + self.from_date, relieving_date + ) + ) def validate_income_tax_slab(self): if not self.income_tax_slab: return - income_tax_slab_currency = frappe.db.get_value('Income Tax Slab', self.income_tax_slab, 'currency') + income_tax_slab_currency = frappe.db.get_value( + "Income Tax Slab", self.income_tax_slab, "currency" + ) if self.currency != income_tax_slab_currency: - frappe.throw(_("Currency of selected Income Tax Slab should be {0} instead of {1}").format(self.currency, income_tax_slab_currency)) + frappe.throw( + _("Currency of selected Income Tax Slab should be {0} instead of {1}").format( + self.currency, income_tax_slab_currency + ) + ) def set_payroll_payable_account(self): if not self.payroll_payable_account: - payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account') + payroll_payable_account = frappe.db.get_value( + "Company", self.company, "default_payroll_payable_account" + ) if not payroll_payable_account: payroll_payable_account = frappe.db.get_value( - "Account", { - "account_name": _("Payroll Payable"), "company": self.company, "account_currency": frappe.db.get_value( - "Company", self.company, "default_currency"), "is_group": 0}) + "Account", + { + "account_name": _("Payroll Payable"), + "company": self.company, + "account_currency": frappe.db.get_value("Company", self.company, "default_currency"), + "is_group": 0, + }, + ) self.payroll_payable_account = payroll_payable_account + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: return None - salary_structure = frappe.db.sql(""" + salary_structure = frappe.db.sql( + """ select salary_structure from `tabSalary Structure Assignment` where employee=%(employee)s and docstatus = 1 - and %(on_date)s >= from_date order by from_date desc limit 1""", { - 'employee': employee, - 'on_date': on_date, - }) + and %(on_date)s >= from_date order by from_date desc limit 1""", + { + "employee": employee, + "on_date": on_date, + }, + ) return salary_structure[0][0] if salary_structure else None + @frappe.whitelist() def get_employee_currency(employee): - employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency') + employee_currency = frappe.db.get_value( + "Salary Structure Assignment", {"employee": employee}, "currency" + ) if not employee_currency: - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(employee)) + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + employee + ) + ) return employee_currency diff --git a/erpnext/payroll/notification/retention_bonus/retention_bonus.py b/erpnext/payroll/notification/retention_bonus/retention_bonus.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/payroll/notification/retention_bonus/retention_bonus.py +++ b/erpnext/payroll/notification/retention_bonus/retention_bonus.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py index 6c3bd37b043..9d8efff8218 100644 --- a/erpnext/payroll/report/bank_remittance/bank_remittance.py +++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py @@ -13,63 +13,47 @@ def execute(filters=None): "fieldtype": "Link", "fieldname": "payroll_no", "options": "Payroll Entry", - "width": 150 + "width": 150, }, { "label": _("Debit A/C Number"), "fieldtype": "Int", "fieldname": "debit_account", "hidden": 1, - "width": 200 - }, - { - "label": _("Payment Date"), - "fieldtype": "Data", - "fieldname": "payment_date", - "width": 100 + "width": 200, }, + {"label": _("Payment Date"), "fieldtype": "Data", "fieldname": "payment_date", "width": 100}, { "label": _("Employee Name"), "fieldtype": "Link", "fieldname": "employee_name", "options": "Employee", - "width": 200 - }, - { - "label": _("Bank Name"), - "fieldtype": "Data", - "fieldname": "bank_name", - "width": 50 + "width": 200, }, + {"label": _("Bank Name"), "fieldtype": "Data", "fieldname": "bank_name", "width": 50}, { "label": _("Employee A/C Number"), "fieldtype": "Int", "fieldname": "employee_account_no", - "width": 50 - } + "width": 50, + }, ] - if frappe.db.has_column('Employee', 'ifsc_code'): - columns.append({ - "label": _("IFSC Code"), - "fieldtype": "Data", - "fieldname": "bank_code", - "width": 100 - }) + if frappe.db.has_column("Employee", "ifsc_code"): + columns.append( + {"label": _("IFSC Code"), "fieldtype": "Data", "fieldname": "bank_code", "width": 100} + ) - columns += [{ - "label": _("Currency"), - "fieldtype": "Data", - "fieldname": "currency", - "width": 50 - }, - { - "label": _("Net Salary Amount"), - "fieldtype": "Currency", - "options": "currency", - "fieldname": "amount", - "width": 100 - }] + columns += [ + {"label": _("Currency"), "fieldtype": "Data", "fieldname": "currency", "width": 50}, + { + "label": _("Net Salary Amount"), + "fieldtype": "Currency", + "options": "currency", + "fieldname": "amount", + "width": 100, + }, + ] data = [] @@ -77,41 +61,48 @@ def execute(filters=None): payroll_entries = get_payroll_entries(accounts, filters) salary_slips = get_salary_slips(payroll_entries) - if frappe.db.has_column('Employee', 'ifsc_code'): + if frappe.db.has_column("Employee", "ifsc_code"): get_emp_bank_ifsc_code(salary_slips) for salary in salary_slips: - if salary.bank_name and salary.bank_account_no and salary.debit_acc_no and salary.status in ["Submitted", "Paid"]: + if ( + salary.bank_name + and salary.bank_account_no + and salary.debit_acc_no + and salary.status in ["Submitted", "Paid"] + ): row = { "payroll_no": salary.payroll_entry, "debit_account": salary.debit_acc_no, - "payment_date": frappe.utils.formatdate(salary.modified.strftime('%Y-%m-%d')), + "payment_date": frappe.utils.formatdate(salary.modified.strftime("%Y-%m-%d")), "bank_name": salary.bank_name, "employee_account_no": salary.bank_account_no, "bank_code": salary.ifsc_code, - "employee_name": salary.employee+": " + salary.employee_name, - "currency": frappe.get_cached_value('Company', filters.company, 'default_currency'), + "employee_name": salary.employee + ": " + salary.employee_name, + "currency": frappe.get_cached_value("Company", filters.company, "default_currency"), "amount": salary.net_pay, } data.append(row) return columns, data + def get_bank_accounts(): accounts = [d.name for d in get_all("Account", filters={"account_type": "Bank"})] return accounts + def get_payroll_entries(accounts, filters): payroll_filter = [ - ('payment_account', 'IN', accounts), - ('number_of_employees', '>', 0), - ('Company', '=', filters.company) + ("payment_account", "IN", accounts), + ("number_of_employees", ">", 0), + ("Company", "=", filters.company), ] if filters.to_date: - payroll_filter.append(('posting_date', '<', filters.to_date)) + payroll_filter.append(("posting_date", "<", filters.to_date)) if filters.from_date: - payroll_filter.append(('posting_date', '>', filters.from_date)) + payroll_filter.append(("posting_date", ">", filters.from_date)) entries = get_all("Payroll Entry", payroll_filter, ["name", "payment_account"]) @@ -119,10 +110,22 @@ def get_payroll_entries(accounts, filters): entries = set_company_account(payment_accounts, entries) return entries + def get_salary_slips(payroll_entries): - payroll = [d.name for d in payroll_entries] - salary_slips = get_all("Salary Slip", filters = [("payroll_entry", "IN", payroll)], - fields = ["modified", "net_pay", "bank_name", "bank_account_no", "payroll_entry", "employee", "employee_name", "status"] + payroll = [d.name for d in payroll_entries] + salary_slips = get_all( + "Salary Slip", + filters=[("payroll_entry", "IN", payroll)], + fields=[ + "modified", + "net_pay", + "bank_name", + "bank_account_no", + "payroll_entry", + "employee", + "employee_name", + "status", + ], ) payroll_entry_map = {} @@ -132,12 +135,13 @@ def get_salary_slips(payroll_entries): # appending company debit accounts for slip in salary_slips: if slip.payroll_entry: - slip["debit_acc_no"] = payroll_entry_map[slip.payroll_entry]['company_account'] + slip["debit_acc_no"] = payroll_entry_map[slip.payroll_entry]["company_account"] else: slip["debit_acc_no"] = None return salary_slips + def get_emp_bank_ifsc_code(salary_slips): emp_names = [d.employee for d in salary_slips] ifsc_codes = get_all("Employee", [("name", "IN", emp_names)], ["ifsc_code", "name"]) @@ -147,20 +151,23 @@ def get_emp_bank_ifsc_code(salary_slips): ifsc_codes_map[code.name] = code for slip in salary_slips: - slip["ifsc_code"] = ifsc_codes_map[code.name]['ifsc_code'] + slip["ifsc_code"] = ifsc_codes_map[code.name]["ifsc_code"] return salary_slips + def set_company_account(payment_accounts, payroll_entries): - company_accounts = get_all("Bank Account", [("account", "in", payment_accounts)], ["account", "bank_account_no"]) + company_accounts = get_all( + "Bank Account", [("account", "in", payment_accounts)], ["account", "bank_account_no"] + ) company_accounts_map = {} for acc in company_accounts: company_accounts_map[acc.account] = acc for entry in payroll_entries: - company_account = '' + company_account = "" if entry.payment_account in company_accounts_map: - company_account = company_accounts_map[entry.payment_account]['bank_account_no'] + company_account = company_accounts_map[entry.payment_account]["bank_account_no"] entry["company_account"] = company_account return payroll_entries diff --git a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py index 75a9f97ea58..ccf16565c1f 100644 --- a/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py +++ b/erpnext/payroll/report/income_tax_deductions/income_tax_deductions.py @@ -14,6 +14,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -21,65 +22,55 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 - }] + "width": 160, + }, + ] if erpnext.get_region() == "India": - columns.append({ - "label": _("PAN Number"), - "fieldname": "pan_number", - "fieldtype": "Data", - "width": 140 - }) + columns.append( + {"label": _("PAN Number"), "fieldname": "pan_number", "fieldtype": "Data", "width": 140} + ) - columns += [{ - "label": _("Income Tax Component"), - "fieldname": "it_comp", - "fieldtype": "Data", - "width": 170 - }, + columns += [ + {"label": _("Income Tax Component"), "fieldname": "it_comp", "fieldtype": "Data", "width": 170}, { "label": _("Income Tax Amount"), "fieldname": "it_amount", "fieldtype": "Currency", "options": "currency", - "width": 140 + "width": 140, }, { "label": _("Gross Pay"), "fieldname": "gross_pay", "fieldtype": "Currency", "options": "currency", - "width": 140 + "width": 140, }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 140 - } + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140}, ] return columns + def get_conditions(filters): conditions = [""] if filters.get("department"): - conditions.append("sal.department = '%s' " % (filters["department"]) ) + conditions.append("sal.department = '%s' " % (filters["department"])) if filters.get("branch"): - conditions.append("sal.branch = '%s' " % (filters["branch"]) ) + conditions.append("sal.branch = '%s' " % (filters["branch"])) if filters.get("company"): - conditions.append("sal.company = '%s' " % (filters["company"]) ) + conditions.append("sal.company = '%s' " % (filters["company"])) if filters.get("month"): conditions.append("month(sal.start_date) = '%s' " % (filters["month"])) @@ -95,10 +86,14 @@ def get_data(filters): data = [] if erpnext.get_region() == "India": - employee_pan_dict = frappe._dict(frappe.db.sql(""" select employee, pan_number from `tabEmployee`""")) + employee_pan_dict = frappe._dict( + frappe.db.sql(""" select employee, pan_number from `tabEmployee`""") + ) - component_types = frappe.db.sql(""" select name from `tabSalary Component` - where is_income_tax_component = 1 """) + component_types = frappe.db.sql( + """ select name from `tabSalary Component` + where is_income_tax_component = 1 """ + ) component_types = [comp_type[0] for comp_type in component_types] @@ -107,14 +102,19 @@ def get_data(filters): conditions = get_conditions(filters) - entry = frappe.db.sql(""" select sal.employee, sal.employee_name, sal.posting_date, ded.salary_component, ded.amount,sal.gross_pay + entry = frappe.db.sql( + """ select sal.employee, sal.employee_name, sal.posting_date, ded.salary_component, ded.amount,sal.gross_pay from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions , ", ".join(['%s']*len(component_types))), tuple(component_types), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_types))), + tuple(component_types), + as_dict=1, + ) for d in entry: @@ -125,7 +125,7 @@ def get_data(filters): "posting_date": d.posting_date, # "pan_number": employee_pan_dict.get(d.employee), "it_amount": d.amount, - "gross_pay": d.gross_pay + "gross_pay": d.gross_pay, } if erpnext.get_region() == "India": diff --git a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py index fa68575e688..e5348df8864 100644 --- a/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py +++ b/erpnext/payroll/report/salary_payments_based_on_payment_mode/salary_payments_based_on_payment_mode.py @@ -19,42 +19,39 @@ def execute(filters=None): columns = get_columns(filters, mode_of_payments) data, total_rows, report_summary = get_data(filters, mode_of_payments) - chart = get_chart(mode_of_payments, total_rows) + chart = get_chart(mode_of_payments, total_rows) return columns, data, None, chart, report_summary + def get_columns(filters, mode_of_payments): - columns = [{ - "label": _("Branch"), - "options": "Branch", - "fieldname": "branch", - "fieldtype": "Link", - "width": 200 - }] + columns = [ + { + "label": _("Branch"), + "options": "Branch", + "fieldname": "branch", + "fieldtype": "Link", + "width": 200, + } + ] for mode in mode_of_payments: - columns.append({ - "label": _(mode), - "fieldname": mode, - "fieldtype": "Currency", - "width": 160 - }) + columns.append({"label": _(mode), "fieldname": mode, "fieldtype": "Currency", "width": 160}) - columns.append({ - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Currency", - "width": 140 - }) + columns.append({"label": _("Total"), "fieldname": "total", "fieldtype": "Currency", "width": 140}) return columns + def get_payment_modes(): - mode_of_payments = frappe.db.sql_list(""" + mode_of_payments = frappe.db.sql_list( + """ select distinct mode_of_payment from `tabSalary Slip` where docstatus = 1 - """) + """ + ) return mode_of_payments + def prepare_data(entry): branch_wise_entries = {} gross_pay = 0 @@ -68,36 +65,42 @@ def prepare_data(entry): return branch_wise_entries, gross_pay + def get_data(filters, mode_of_payments): data = [] conditions = get_conditions(filters) - entry = frappe.db.sql(""" + entry = frappe.db.sql( + """ select branch, mode_of_payment, sum(net_pay) as net_pay, sum(gross_pay) as gross_pay from `tabSalary Slip` sal where docstatus = 1 %s group by branch, mode_of_payment - """ % (conditions), as_dict=1) + """ + % (conditions), + as_dict=1, + ) branch_wise_entries, gross_pay = prepare_data(entry) - branches = frappe.db.sql_list(""" + branches = frappe.db.sql_list( + """ select distinct branch from `tabSalary Slip` sal where docstatus = 1 %s - """ % (conditions)) + """ + % (conditions) + ) total_row = {"total": 0, "branch": "Total"} for branch in branches: total = 0 - row = { - "branch": branch - } + row = {"branch": branch} for mode in mode_of_payments: if branch_wise_entries.get(branch).get(mode): row[mode] = branch_wise_entries.get(branch).get(mode) - total += branch_wise_entries.get(branch).get(mode) + total += branch_wise_entries.get(branch).get(mode) row["total"] = total data.append(row) @@ -110,24 +113,18 @@ def get_data(filters, mode_of_payments): if data: data.append(total_row) data.append({}) - data.append({ - "branch": "Total Gross Pay", - mode_of_payments[0]:gross_pay - }) - data.append({ - "branch": "Total Deductions", - mode_of_payments[0]:total_deductions - }) - data.append({ - "branch": "Total Net Pay", - mode_of_payments[0]:total_row.get("total") - }) + data.append({"branch": "Total Gross Pay", mode_of_payments[0]: gross_pay}) + data.append({"branch": "Total Deductions", mode_of_payments[0]: total_deductions}) + data.append({"branch": "Total Net Pay", mode_of_payments[0]: total_row.get("total")}) currency = erpnext.get_company_currency(filters.company) - report_summary = get_report_summary(gross_pay, total_deductions, total_row.get("total"), currency) + report_summary = get_report_summary( + gross_pay, total_deductions, total_row.get("total"), currency + ) return data, total_row, report_summary + def get_total_based_on_mode_of_payment(data, mode_of_payments): total = 0 @@ -140,6 +137,7 @@ def get_total_based_on_mode_of_payment(data, mode_of_payments): total_row["total"] = total return total_row + def get_report_summary(gross_pay, total_deductions, net_pay, currency): return [ { @@ -147,24 +145,25 @@ def get_report_summary(gross_pay, total_deductions, net_pay, currency): "label": "Total Gross Pay", "indicator": "Green", "datatype": "Currency", - "currency": currency + "currency": currency, }, { "value": total_deductions, "label": "Total Deduction", "datatype": "Currency", "indicator": "Red", - "currency": currency + "currency": currency, }, { "value": net_pay, "label": "Total Net Pay", "datatype": "Currency", "indicator": "Blue", - "currency": currency - } + "currency": currency, + }, ] + def get_chart(mode_of_payments, data): if data: values = [] @@ -175,10 +174,7 @@ def get_chart(mode_of_payments, data): labels.append([mode]) chart = { - "data": { - "labels": labels, - "datasets": [{'name': 'Mode Of Payments', "values": values}] - } + "data": {"labels": labels, "datasets": [{"name": "Mode Of Payments", "values": values}]} } - chart['type'] = "bar" + chart["type"] = "bar" return chart diff --git a/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py index 578c8164009..4f9370b742b 100644 --- a/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py +++ b/erpnext/payroll/report/salary_payments_via_ecs/salary_payments_via_ecs.py @@ -14,6 +14,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -21,71 +22,52 @@ def get_columns(filters): "options": "Branch", "fieldname": "branch", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 + "width": 160, }, { "label": _("Employee"), - "options":"Employee", + "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Gross Pay"), "fieldname": "gross_pay", "fieldtype": "Currency", "options": "currency", - "width": 140 - }, - { - "label": _("Bank"), - "fieldname": "bank", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("Account No"), - "fieldname": "account_no", - "fieldtype": "Data", - "width": 140 + "width": 140, }, + {"label": _("Bank"), "fieldname": "bank", "fieldtype": "Data", "width": 140}, + {"label": _("Account No"), "fieldname": "account_no", "fieldtype": "Data", "width": 140}, ] if erpnext.get_region() == "India": columns += [ - { - "label": _("IFSC"), - "fieldname": "ifsc", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("MICR"), - "fieldname": "micr", - "fieldtype": "Data", - "width": 140 - } + {"label": _("IFSC"), "fieldname": "ifsc", "fieldtype": "Data", "width": 140}, + {"label": _("MICR"), "fieldname": "micr", "fieldtype": "Data", "width": 140}, ] return columns + def get_conditions(filters): conditions = [""] if filters.get("department"): - conditions.append("department = '%s' " % (filters["department"]) ) + conditions.append("department = '%s' " % (filters["department"])) if filters.get("branch"): - conditions.append("branch = '%s' " % (filters["branch"]) ) + conditions.append("branch = '%s' " % (filters["branch"])) if filters.get("company"): - conditions.append("company = '%s' " % (filters["company"]) ) + conditions.append("company = '%s' " % (filters["company"])) if filters.get("month"): conditions.append("month(start_date) = '%s' " % (filters["month"])) @@ -95,6 +77,7 @@ def get_conditions(filters): return " and ".join(conditions) + def get_data(filters): data = [] @@ -103,36 +86,39 @@ def get_data(filters): if erpnext.get_region() == "India": fields += ["ifsc_code", "micr_code"] - - employee_details = frappe.get_list("Employee", fields = fields) + employee_details = frappe.get_list("Employee", fields=fields) employee_data_dict = {} for d in employee_details: employee_data_dict.setdefault( - d.employee,{ - "bank_ac_no" : d.bank_ac_no, - "ifsc_code" : d.ifsc_code or None, - "micr_code" : d.micr_code or None, - "branch" : d.branch, - "salary_mode" : d.salary_mode, - "bank_name": d.bank_name - } + d.employee, + { + "bank_ac_no": d.bank_ac_no, + "ifsc_code": d.ifsc_code or None, + "micr_code": d.micr_code or None, + "branch": d.branch, + "salary_mode": d.salary_mode, + "bank_name": d.bank_name, + }, ) conditions = get_conditions(filters) - entry = frappe.db.sql(""" select employee, employee_name, gross_pay + entry = frappe.db.sql( + """ select employee, employee_name, gross_pay from `tabSalary Slip` where docstatus = 1 %s """ - %(conditions), as_dict =1) + % (conditions), + as_dict=1, + ) for d in entry: employee = { - "branch" : employee_data_dict.get(d.employee).get("branch"), - "employee_name" : d.employee_name, - "employee" : d.employee, - "gross_pay" : d.gross_pay, + "branch": employee_data_dict.get(d.employee).get("branch"), + "employee_name": d.employee_name, + "employee": d.employee, + "gross_pay": d.gross_pay, } if employee_data_dict.get(d.employee).get("salary_mode") == "Bank": @@ -144,7 +130,9 @@ def get_data(filters): else: employee["account_no"] = employee_data_dict.get(d.employee).get("salary_mode") - if filters.get("type") and employee_data_dict.get(d.employee).get("salary_mode") == filters.get("type"): + if filters.get("type") and employee_data_dict.get(d.employee).get("salary_mode") == filters.get( + "type" + ): data.append(employee) elif not filters.get("type"): data.append(employee) diff --git a/erpnext/payroll/report/salary_register/salary_register.py b/erpnext/payroll/report/salary_register/salary_register.py index 78deb227783..0a62b43a8ea 100644 --- a/erpnext/payroll/report/salary_register/salary_register.py +++ b/erpnext/payroll/report/salary_register/salary_register.py @@ -10,29 +10,46 @@ import erpnext def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} currency = None - if filters.get('currency'): - currency = filters.get('currency') + if filters.get("currency"): + currency = filters.get("currency") company_currency = erpnext.get_company_currency(filters.get("company")) salary_slips = get_salary_slips(filters, company_currency) - if not salary_slips: return [], [] + if not salary_slips: + return [], [] columns, earning_types, ded_types = get_columns(salary_slips) ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) - ss_ded_map = get_ss_ded_map(salary_slips,currency, company_currency) + ss_ded_map = get_ss_ded_map(salary_slips, currency, company_currency) doj_map = get_employee_doj_map() data = [] for ss in salary_slips: - row = [ss.name, ss.employee, ss.employee_name, doj_map.get(ss.employee), ss.branch, ss.department, ss.designation, - ss.company, ss.start_date, ss.end_date, ss.leave_without_pay, ss.payment_days] - - if ss.branch is not None: columns[3] = columns[3].replace('-1','120') - if ss.department is not None: columns[4] = columns[4].replace('-1','120') - if ss.designation is not None: columns[5] = columns[5].replace('-1','120') - if ss.leave_without_pay is not None: columns[9] = columns[9].replace('-1','130') + row = [ + ss.name, + ss.employee, + ss.employee_name, + doj_map.get(ss.employee), + ss.branch, + ss.department, + ss.designation, + ss.company, + ss.start_date, + ss.end_date, + ss.leave_without_pay, + ss.payment_days, + ] + if ss.branch is not None: + columns[3] = columns[3].replace("-1", "120") + if ss.department is not None: + columns[4] = columns[4].replace("-1", "120") + if ss.designation is not None: + columns[5] = columns[5].replace("-1", "120") + if ss.leave_without_pay is not None: + columns[9] = columns[9].replace("-1", "130") for e in earning_types: row.append(ss_earning_map.get(ss.name, {}).get(e)) @@ -48,7 +65,10 @@ def execute(filters=None): row.append(ss.total_loan_repayment) if currency == company_currency: - row += [flt(ss.total_deduction) * flt(ss.exchange_rate), flt(ss.net_pay) * flt(ss.exchange_rate)] + row += [ + flt(ss.total_deduction) * flt(ss.exchange_rate), + flt(ss.net_pay) * flt(ss.exchange_rate), + ] else: row += [ss.total_deduction, ss.net_pay] row.append(currency or company_currency) @@ -56,53 +76,81 @@ def execute(filters=None): return columns, data + def get_columns(salary_slips): + """ + columns = [ + _("Salary Slip ID") + ":Link/Salary Slip:150", + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::140", + _("Date of Joining") + "::80", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Company") + ":Link/Company:120", + _("Start Date") + "::80", + _("End Date") + "::80", + _("Leave Without Pay") + ":Float:130", + _("Payment Days") + ":Float:120", + _("Currency") + ":Link/Currency:80" + ] """ columns = [ _("Salary Slip ID") + ":Link/Salary Slip:150", _("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", _("Date of Joining") + "::80", - _("Branch") + ":Link/Branch:120", - _("Department") + ":Link/Department:120", + _("Branch") + ":Link/Branch:-1", + _("Department") + ":Link/Department:-1", _("Designation") + ":Link/Designation:120", _("Company") + ":Link/Company:120", _("Start Date") + "::80", _("End Date") + "::80", - _("Leave Without Pay") + ":Float:130", + _("Leave Without Pay") + ":Float:50", _("Payment Days") + ":Float:120", - _("Currency") + ":Link/Currency:80" - ] - """ - columns = [ - _("Salary Slip ID") + ":Link/Salary Slip:150",_("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", - _("Date of Joining") + "::80", _("Branch") + ":Link/Branch:-1", _("Department") + ":Link/Department:-1", - _("Designation") + ":Link/Designation:120", _("Company") + ":Link/Company:120", _("Start Date") + "::80", - _("End Date") + "::80", _("Leave Without Pay") + ":Float:50", _("Payment Days") + ":Float:120" ] salary_components = {_("Earning"): [], _("Deduction"): []} - for component in frappe.db.sql("""select distinct sd.salary_component, sc.type + for component in frappe.db.sql( + """select distinct sd.salary_component, sc.type from `tabSalary Detail` sd, `tabSalary Component` sc - where sc.name=sd.salary_component and sd.amount != 0 and sd.parent in (%s)""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1): + where sc.name=sd.salary_component and sd.amount != 0 and sd.parent in (%s)""" + % (", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ): salary_components[_(component.type)].append(component.salary_component) - columns = columns + [(e + ":Currency:120") for e in salary_components[_("Earning")]] + \ - [_("Gross Pay") + ":Currency:120"] + [(d + ":Currency:120") for d in salary_components[_("Deduction")]] + \ - [_("Loan Repayment") + ":Currency:120", _("Total Deduction") + ":Currency:120", _("Net Pay") + ":Currency:120"] + columns = ( + columns + + [(e + ":Currency:120") for e in salary_components[_("Earning")]] + + [_("Gross Pay") + ":Currency:120"] + + [(d + ":Currency:120") for d in salary_components[_("Deduction")]] + + [ + _("Loan Repayment") + ":Currency:120", + _("Total Deduction") + ":Currency:120", + _("Net Pay") + ":Currency:120", + ] + ) return columns, salary_components[_("Earning")], salary_components[_("Deduction")] + def get_salary_slips(filters, company_currency): - filters.update({"from_date": filters.get("from_date"), "to_date":filters.get("to_date")}) + filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")}) conditions, filters = get_conditions(filters, company_currency) - salary_slips = frappe.db.sql("""select * from `tabSalary Slip` where %s - order by employee""" % conditions, filters, as_dict=1) + salary_slips = frappe.db.sql( + """select * from `tabSalary Slip` where %s + order by employee""" + % conditions, + filters, + as_dict=1, + ) return salary_slips or [] + def get_conditions(filters, company_currency): conditions = "" doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} @@ -110,48 +158,71 @@ def get_conditions(filters, company_currency): if filters.get("docstatus"): conditions += "docstatus = {0}".format(doc_status[filters.get("docstatus")]) - if filters.get("from_date"): conditions += " and start_date >= %(from_date)s" - if filters.get("to_date"): conditions += " and end_date <= %(to_date)s" - if filters.get("company"): conditions += " and company = %(company)s" - if filters.get("employee"): conditions += " and employee = %(employee)s" + if filters.get("from_date"): + conditions += " and start_date >= %(from_date)s" + if filters.get("to_date"): + conditions += " and end_date <= %(to_date)s" + if filters.get("company"): + conditions += " and company = %(company)s" + if filters.get("employee"): + conditions += " and employee = %(employee)s" if filters.get("currency") and filters.get("currency") != company_currency: conditions += " and currency = %(currency)s" return conditions, filters + def get_employee_doj_map(): - return frappe._dict(frappe.db.sql(""" + return frappe._dict( + frappe.db.sql( + """ SELECT employee, date_of_joining FROM `tabEmployee` - """)) + """ + ) + ) + def get_ss_earning_map(salary_slips, currency, company_currency): - ss_earnings = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name - from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) + ss_earnings = frappe.db.sql( + """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" + % (", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) ss_earning_map = {} for d in ss_earnings: ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0) if currency == company_currency: - ss_earning_map[d.parent][d.salary_component] += flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + ss_earning_map[d.parent][d.salary_component] += flt(d.amount) * flt( + d.exchange_rate if d.exchange_rate else 1 + ) else: ss_earning_map[d.parent][d.salary_component] += flt(d.amount) return ss_earning_map + def get_ss_ded_map(salary_slips, currency, company_currency): - ss_deductions = frappe.db.sql("""select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name - from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) + ss_deductions = frappe.db.sql( + """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" + % (", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) ss_ded_map = {} for d in ss_deductions: ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0) if currency == company_currency: - ss_ded_map[d.parent][d.salary_component] += flt(d.amount) * flt(d.exchange_rate if d.exchange_rate else 1) + ss_ded_map[d.parent][d.salary_component] += flt(d.amount) * flt( + d.exchange_rate if d.exchange_rate else 1 + ) else: ss_ded_map[d.parent][d.salary_component] += flt(d.amount) diff --git a/erpnext/portal/doctype/homepage/homepage.py b/erpnext/portal/doctype/homepage/homepage.py index 8092ba208a4..5bb05f0842d 100644 --- a/erpnext/portal/doctype/homepage/homepage.py +++ b/erpnext/portal/doctype/homepage/homepage.py @@ -11,17 +11,27 @@ class Homepage(Document): def validate(self): if not self.description: self.description = frappe._("This is an example website auto-generated from ERPNext") - delete_page_cache('home') + delete_page_cache("home") def setup_items(self): - for d in frappe.get_all('Website Item', fields=['name', 'item_name', 'description', 'image', 'route'], - filters={'published': 1}, limit=3): + for d in frappe.get_all( + "Website Item", + fields=["name", "item_name", "description", "image", "route"], + filters={"published": 1}, + limit=3, + ): - doc = frappe.get_doc('Website Item', d.name) + doc = frappe.get_doc("Website Item", d.name) if not doc.route: # set missing route doc.save() - self.append('products', dict(item_code=d.name, - item_name=d.item_name, description=d.description, - image=d.image, route=d.route)) - + self.append( + "products", + dict( + item_code=d.name, + item_name=d.item_name, + description=d.description, + image=d.image, + route=d.route, + ), + ) diff --git a/erpnext/portal/doctype/homepage/test_homepage.py b/erpnext/portal/doctype/homepage/test_homepage.py index 9eb1f015af6..c8a1a1deec2 100644 --- a/erpnext/portal/doctype/homepage/test_homepage.py +++ b/erpnext/portal/doctype/homepage/test_homepage.py @@ -10,7 +10,7 @@ from frappe.website.render import render class TestHomepage(unittest.TestCase): def test_homepage_load(self): - set_request(method='GET', path='home') + set_request(method="GET", path="home") response = render() self.assertEqual(response.status_code, 200) diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py index 4b8ba3002f0..ae1183a5a51 100644 --- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py +++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py @@ -12,65 +12,79 @@ from frappe.website.render import render class TestHomepageSection(unittest.TestCase): def test_homepage_section_card(self): try: - frappe.get_doc({ - 'doctype': 'Homepage Section', - 'name': 'Card Section', - 'section_based_on': 'Cards', - 'section_cards': [ - {'title': 'Card 1', 'subtitle': 'Subtitle 1', 'content': 'This is test card 1', 'route': '/card-1'}, - {'title': 'Card 2', 'subtitle': 'Subtitle 2', 'content': 'This is test card 2', 'image': 'test.jpg'}, - ], - 'no_of_columns': 3 - }).insert() + frappe.get_doc( + { + "doctype": "Homepage Section", + "name": "Card Section", + "section_based_on": "Cards", + "section_cards": [ + { + "title": "Card 1", + "subtitle": "Subtitle 1", + "content": "This is test card 1", + "route": "/card-1", + }, + { + "title": "Card 2", + "subtitle": "Subtitle 2", + "content": "This is test card 2", + "image": "test.jpg", + }, + ], + "no_of_columns": 3, + } + ).insert() except frappe.DuplicateEntryError: pass - set_request(method='GET', path='home') + set_request(method="GET", path="home") response = render() self.assertEqual(response.status_code, 200) html = frappe.safe_decode(response.get_data()) - soup = BeautifulSoup(html, 'html.parser') - sections = soup.find('main').find_all('section') + soup = BeautifulSoup(html, "html.parser") + sections = soup.find("main").find_all("section") self.assertEqual(len(sections), 3) homepage_section = sections[2] - self.assertEqual(homepage_section.h3.text, 'Card Section') + self.assertEqual(homepage_section.h3.text, "Card Section") cards = homepage_section.find_all(class_="card") self.assertEqual(len(cards), 2) - self.assertEqual(cards[0].h5.text, 'Card 1') - self.assertEqual(cards[0].a['href'], '/card-1') - self.assertEqual(cards[1].p.text, 'Subtitle 2') - self.assertEqual(cards[1].find(class_='website-image-lazy')['data-src'], 'test.jpg') + self.assertEqual(cards[0].h5.text, "Card 1") + self.assertEqual(cards[0].a["href"], "/card-1") + self.assertEqual(cards[1].p.text, "Subtitle 2") + self.assertEqual(cards[1].find(class_="website-image-lazy")["data-src"], "test.jpg") # cleanup frappe.db.rollback() def test_homepage_section_custom_html(self): - frappe.get_doc({ - 'doctype': 'Homepage Section', - 'name': 'Custom HTML Section', - 'section_based_on': 'Custom HTML', - 'section_html': '
    My custom html
    ', - }).insert() + frappe.get_doc( + { + "doctype": "Homepage Section", + "name": "Custom HTML Section", + "section_based_on": "Custom HTML", + "section_html": '
    My custom html
    ', + } + ).insert() - set_request(method='GET', path='home') + set_request(method="GET", path="home") response = render() self.assertEqual(response.status_code, 200) html = frappe.safe_decode(response.get_data()) - soup = BeautifulSoup(html, 'html.parser') - sections = soup.find('main').find_all(class_='custom-section') + soup = BeautifulSoup(html, "html.parser") + sections = soup.find("main").find_all(class_="custom-section") self.assertEqual(len(sections), 1) homepage_section = sections[0] - self.assertEqual(homepage_section.text, 'My custom html') + self.assertEqual(homepage_section.text, "My custom html") # cleanup frappe.db.rollback() diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py index 4552e1257d0..09d100708e3 100644 --- a/erpnext/portal/utils.py +++ b/erpnext/portal/utils.py @@ -1,4 +1,3 @@ - import frappe from frappe.utils.nestedset import get_root_of @@ -9,40 +8,41 @@ from erpnext.e_commerce.shopping_cart.cart import get_debtors_account def set_default_role(doc, method): - '''Set customer, supplier, student, guardian based on email''' + """Set customer, supplier, student, guardian based on email""" if frappe.flags.setting_role or frappe.flags.in_migrate: return roles = frappe.get_roles(doc.name) - contact_name = frappe.get_value('Contact', dict(email_id=doc.email)) + contact_name = frappe.get_value("Contact", dict(email_id=doc.email)) if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) for link in contact.links: frappe.flags.setting_role = True - if link.link_doctype=='Customer' and 'Customer' not in roles: - doc.add_roles('Customer') - elif link.link_doctype=='Supplier' and 'Supplier' not in roles: - doc.add_roles('Supplier') - elif frappe.get_value('Student', dict(student_email_id=doc.email)) and 'Student' not in roles: - doc.add_roles('Student') - elif frappe.get_value('Guardian', dict(email_address=doc.email)) and 'Guardian' not in roles: - doc.add_roles('Guardian') + if link.link_doctype == "Customer" and "Customer" not in roles: + doc.add_roles("Customer") + elif link.link_doctype == "Supplier" and "Supplier" not in roles: + doc.add_roles("Supplier") + elif frappe.get_value("Student", dict(student_email_id=doc.email)) and "Student" not in roles: + doc.add_roles("Student") + elif frappe.get_value("Guardian", dict(email_address=doc.email)) and "Guardian" not in roles: + doc.add_roles("Guardian") + def create_customer_or_supplier(): - '''Based on the default Role (Customer, Supplier), create a Customer / Supplier. + """Based on the default Role (Customer, Supplier), create a Customer / Supplier. Called on_session_creation hook. - ''' + """ user = frappe.session.user - if frappe.db.get_value('User', user, 'user_type') != 'Website User': + if frappe.db.get_value("User", user, "user_type") != "Website User": return user_roles = frappe.get_roles() - portal_settings = frappe.get_single('Portal Settings') + portal_settings = frappe.get_single("Portal Settings") default_role = portal_settings.default_role - if default_role not in ['Customer', 'Supplier']: + if default_role not in ["Customer", "Supplier"]: return # create customer / supplier if the user has that role @@ -60,34 +60,33 @@ def create_customer_or_supplier(): party = frappe.new_doc(doctype) fullname = frappe.utils.get_fullname(user) - if doctype == 'Customer': + if doctype == "Customer": cart_settings = get_shopping_cart_settings() if cart_settings.enable_checkout: debtors_account = get_debtors_account(cart_settings) else: - debtors_account = '' + debtors_account = "" - party.update({ - "customer_name": fullname, - "customer_type": "Individual", - "customer_group": cart_settings.default_customer_group, - "territory": get_root_of("Territory") - }) + party.update( + { + "customer_name": fullname, + "customer_type": "Individual", + "customer_group": cart_settings.default_customer_group, + "territory": get_root_of("Territory"), + } + ) if debtors_account: - party.update({ - "accounts": [{ - "company": cart_settings.company, - "account": debtors_account - }] - }) + party.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]}) else: - party.update({ - "supplier_name": fullname, - "supplier_group": "All Supplier Groups", - "supplier_type": "Individual" - }) + party.update( + { + "supplier_name": fullname, + "supplier_group": "All Supplier Groups", + "supplier_type": "Individual", + } + ) party.flags.ignore_mandatory = True party.insert(ignore_permissions=True) @@ -96,28 +95,27 @@ def create_customer_or_supplier(): if party_exists(alternate_doctype, user): # if user is both customer and supplier, alter fullname to avoid contact name duplication - fullname += "-" + doctype + fullname += "-" + doctype create_party_contact(doctype, fullname, user, party.name) return party + def create_party_contact(doctype, fullname, user, party_name): contact = frappe.new_doc("Contact") - contact.update({ - "first_name": fullname, - "email_id": user - }) - contact.append('links', dict(link_doctype=doctype, link_name=party_name)) - contact.append('email_ids', dict(email_id=user)) + contact.update({"first_name": fullname, "email_id": user}) + contact.append("links", dict(link_doctype=doctype, link_name=party_name)) + contact.append("email_ids", dict(email_id=user)) contact.flags.ignore_mandatory = True contact.insert(ignore_permissions=True) + def party_exists(doctype, user): # check if contact exists against party and if it is linked to the doctype contact_name = frappe.db.get_value("Contact", {"email_id": user}) if contact_name: - contact = frappe.get_doc('Contact', contact_name) + contact = frappe.get_doc("Contact", contact_name) doctypes = [d.link_doctype for d in contact.links] return doctype in doctypes diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.py b/erpnext/projects/doctype/activity_cost/activity_cost.py index bc4bb9dcba4..b99aa1e37d5 100644 --- a/erpnext/projects/doctype/activity_cost/activity_cost.py +++ b/erpnext/projects/doctype/activity_cost/activity_cost.py @@ -7,7 +7,9 @@ from frappe import _ from frappe.model.document import Document -class DuplicationError(frappe.ValidationError): pass +class DuplicationError(frappe.ValidationError): + pass + class ActivityCost(Document): def validate(self): @@ -24,12 +26,22 @@ class ActivityCost(Document): def check_unique(self): if self.employee: - if frappe.db.sql("""select name from `tabActivity Cost` where employee_name= %s and activity_type= %s and name != %s""", - (self.employee_name, self.activity_type, self.name)): - frappe.throw(_("Activity Cost exists for Employee {0} against Activity Type - {1}") - .format(self.employee, self.activity_type), DuplicationError) + if frappe.db.sql( + """select name from `tabActivity Cost` where employee_name= %s and activity_type= %s and name != %s""", + (self.employee_name, self.activity_type, self.name), + ): + frappe.throw( + _("Activity Cost exists for Employee {0} against Activity Type - {1}").format( + self.employee, self.activity_type + ), + DuplicationError, + ) else: - if frappe.db.sql("""select name from `tabActivity Cost` where ifnull(employee, '')='' and activity_type= %s and name != %s""", - (self.activity_type, self.name)): - frappe.throw(_("Default Activity Cost exists for Activity Type - {0}") - .format(self.activity_type), DuplicationError) + if frappe.db.sql( + """select name from `tabActivity Cost` where ifnull(employee, '')='' and activity_type= %s and name != %s""", + (self.activity_type, self.name), + ): + frappe.throw( + _("Default Activity Cost exists for Activity Type - {0}").format(self.activity_type), + DuplicationError, + ) diff --git a/erpnext/projects/doctype/activity_cost/test_activity_cost.py b/erpnext/projects/doctype/activity_cost/test_activity_cost.py index d53e582adc6..8da797ed4f2 100644 --- a/erpnext/projects/doctype/activity_cost/test_activity_cost.py +++ b/erpnext/projects/doctype/activity_cost/test_activity_cost.py @@ -11,15 +11,17 @@ from erpnext.projects.doctype.activity_cost.activity_cost import DuplicationErro class TestActivityCost(unittest.TestCase): def test_duplication(self): frappe.db.sql("delete from `tabActivity Cost`") - activity_cost1 = frappe.new_doc('Activity Cost') - activity_cost1.update({ - "employee": "_T-Employee-00001", - "employee_name": "_Test Employee", - "activity_type": "_Test Activity Type 1", - "billing_rate": 100, - "costing_rate": 50 - }) + activity_cost1 = frappe.new_doc("Activity Cost") + activity_cost1.update( + { + "employee": "_T-Employee-00001", + "employee_name": "_Test Employee", + "activity_type": "_Test Activity Type 1", + "billing_rate": 100, + "costing_rate": 50, + } + ) activity_cost1.insert() activity_cost2 = frappe.copy_doc(activity_cost1) - self.assertRaises(DuplicationError, activity_cost2.insert ) + self.assertRaises(DuplicationError, activity_cost2.insert) frappe.db.sql("delete from `tabActivity Cost`") diff --git a/erpnext/projects/doctype/activity_type/test_activity_type.py b/erpnext/projects/doctype/activity_type/test_activity_type.py index bb74b881f4c..d51439eb5b1 100644 --- a/erpnext/projects/doctype/activity_type/test_activity_type.py +++ b/erpnext/projects/doctype/activity_type/test_activity_type.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Activity Type') +test_records = frappe.get_test_records("Activity Type") diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5ffae2d0fb9..2a3e31f2891 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -17,20 +17,26 @@ from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday class Project(Document): def get_feed(self): - return '{0}: {1}'.format(_(self.status), frappe.safe_decode(self.project_name)) + return "{0}: {1}".format(_(self.status), frappe.safe_decode(self.project_name)) def onload(self): - self.set_onload('activity_summary', frappe.db.sql('''select activity_type, + self.set_onload( + "activity_summary", + frappe.db.sql( + """select activity_type, sum(hours) as total_hours from `tabTimesheet Detail` where project=%s and docstatus < 2 group by activity_type - order by total_hours desc''', self.name, as_dict=True)) + order by total_hours desc""", + self.name, + as_dict=True, + ), + ) self.update_costing() def before_print(self, settings=None): self.onload() - def validate(self): if not self.is_new(): self.copy_from_template() @@ -39,17 +45,17 @@ class Project(Document): self.update_percent_complete() def copy_from_template(self): - ''' + """ Copy tasks from template - ''' - if self.project_template and not frappe.db.get_all('Task', dict(project = self.name), limit=1): + """ + if self.project_template and not frappe.db.get_all("Task", dict(project=self.name), limit=1): # has a template, and no loaded tasks, so lets create if not self.expected_start_date: # project starts today self.expected_start_date = today() - template = frappe.get_doc('Project Template', self.project_template) + template = frappe.get_doc("Project Template", self.project_template) if not self.project_type: self.project_type = template.project_type @@ -65,19 +71,21 @@ class Project(Document): self.dependency_mapping(tmp_task_details, project_tasks) def create_task_from_template(self, task_details): - return frappe.get_doc(dict( - doctype = 'Task', - subject = task_details.subject, - project = self.name, - status = 'Open', - exp_start_date = self.calculate_start_date(task_details), - exp_end_date = self.calculate_end_date(task_details), - description = task_details.description, - task_weight = task_details.task_weight, - type = task_details.type, - issue = task_details.issue, - is_group = task_details.is_group - )).insert() + return frappe.get_doc( + dict( + doctype="Task", + subject=task_details.subject, + project=self.name, + status="Open", + exp_start_date=self.calculate_start_date(task_details), + exp_end_date=self.calculate_end_date(task_details), + description=task_details.description, + task_weight=task_details.task_weight, + type=task_details.type, + issue=task_details.issue, + is_group=task_details.is_group, + ) + ).insert() def calculate_start_date(self, task_details): self.start_date = add_days(self.expected_start_date, task_details.start) @@ -105,23 +113,26 @@ class Project(Document): if template_task.get("depends_on") and not project_task.get("depends_on"): for child_task in template_task.get("depends_on"): child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") - corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks)) + corresponding_project_task = list( + filter(lambda x: x.subject == child_task_subject, project_tasks) + ) if len(corresponding_project_task): - project_task.append("depends_on",{ - "task": corresponding_project_task[0].name - }) + project_task.append("depends_on", {"task": corresponding_project_task[0].name}) project_task.save() def check_for_parent_tasks(self, template_task, project_task, project_tasks): if template_task.get("parent_task") and not project_task.get("parent_task"): parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") - corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks)) + corresponding_project_task = list( + filter(lambda x: x.subject == parent_task_subject, project_tasks) + ) if len(corresponding_project_task): project_task.parent_task = corresponding_project_task[0].name project_task.save() def is_row_updated(self, row, existing_task_data, fields): - if self.get("__islocal") or not existing_task_data: return True + if self.get("__islocal") or not existing_task_data: + return True d = existing_task_data.get(row.task_id, {}) @@ -130,7 +141,7 @@ class Project(Document): return True def update_project(self): - '''Called externally by Task''' + """Called externally by Task""" self.update_percent_complete() self.update_costing() self.db_update() @@ -149,52 +160,74 @@ class Project(Document): self.percent_complete = 100 return - total = frappe.db.count('Task', dict(project=self.name)) + total = frappe.db.count("Task", dict(project=self.name)) if not total: self.percent_complete = 0 else: if (self.percent_complete_method == "Task Completion" and total > 0) or ( - not self.percent_complete_method and total > 0): - completed = frappe.db.sql("""select count(name) from tabTask where - project=%s and status in ('Cancelled', 'Completed')""", self.name)[0][0] + not self.percent_complete_method and total > 0 + ): + completed = frappe.db.sql( + """select count(name) from tabTask where + project=%s and status in ('Cancelled', 'Completed')""", + self.name, + )[0][0] self.percent_complete = flt(flt(completed) / total * 100, 2) - if (self.percent_complete_method == "Task Progress" and total > 0): - progress = frappe.db.sql("""select sum(progress) from tabTask where - project=%s""", self.name)[0][0] + if self.percent_complete_method == "Task Progress" and total > 0: + progress = frappe.db.sql( + """select sum(progress) from tabTask where + project=%s""", + self.name, + )[0][0] self.percent_complete = flt(flt(progress) / total, 2) - if (self.percent_complete_method == "Task Weight" and total > 0): - weight_sum = frappe.db.sql("""select sum(task_weight) from tabTask where - project=%s""", self.name)[0][0] - weighted_progress = frappe.db.sql("""select progress, task_weight from tabTask where - project=%s""", self.name, as_dict=1) + if self.percent_complete_method == "Task Weight" and total > 0: + weight_sum = frappe.db.sql( + """select sum(task_weight) from tabTask where + project=%s""", + self.name, + )[0][0] + weighted_progress = frappe.db.sql( + """select progress, task_weight from tabTask where + project=%s""", + self.name, + as_dict=1, + ) pct_complete = 0 for row in weighted_progress: pct_complete += row["progress"] * frappe.utils.safe_div(row["task_weight"], weight_sum) self.percent_complete = flt(flt(pct_complete), 2) # don't update status if it is cancelled - if self.status == 'Cancelled': + if self.status == "Cancelled": return if self.percent_complete == 100: self.status = "Completed" def update_costing(self): - from_time_sheet = frappe.db.sql("""select + from_time_sheet = frappe.db.sql( + """select sum(costing_amount) as costing_amount, sum(billing_amount) as billing_amount, min(from_time) as start_date, max(to_time) as end_date, sum(hours) as time - from `tabTimesheet Detail` where project = %s and docstatus = 1""", self.name, as_dict=1)[0] + from `tabTimesheet Detail` where project = %s and docstatus = 1""", + self.name, + as_dict=1, + )[0] - from_expense_claim = frappe.db.sql("""select + from_expense_claim = frappe.db.sql( + """select sum(total_sanctioned_amount) as total_sanctioned_amount from `tabExpense Claim` where project = %s - and docstatus = 1""", self.name, as_dict=1)[0] + and docstatus = 1""", + self.name, + as_dict=1, + )[0] self.actual_start_date = from_time_sheet.start_date self.actual_end_date = from_time_sheet.end_date @@ -210,41 +243,54 @@ class Project(Document): self.calculate_gross_margin() def calculate_gross_margin(self): - expense_amount = (flt(self.total_costing_amount) + flt(self.total_expense_claim) - + flt(self.total_purchase_cost) + flt(self.get('total_consumed_material_cost', 0))) + expense_amount = ( + flt(self.total_costing_amount) + + flt(self.total_expense_claim) + + flt(self.total_purchase_cost) + + flt(self.get("total_consumed_material_cost", 0)) + ) self.gross_margin = flt(self.total_billed_amount) - expense_amount if self.total_billed_amount: self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100 def update_purchase_costing(self): - total_purchase_cost = frappe.db.sql("""select sum(base_net_amount) - from `tabPurchase Invoice Item` where project = %s and docstatus=1""", self.name) + total_purchase_cost = frappe.db.sql( + """select sum(base_net_amount) + from `tabPurchase Invoice Item` where project = %s and docstatus=1""", + self.name, + ) self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0 def update_sales_amount(self): - total_sales_amount = frappe.db.sql("""select sum(base_net_total) - from `tabSales Order` where project = %s and docstatus=1""", self.name) + total_sales_amount = frappe.db.sql( + """select sum(base_net_total) + from `tabSales Order` where project = %s and docstatus=1""", + self.name, + ) self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0 def update_billed_amount(self): - total_billed_amount = frappe.db.sql("""select sum(base_net_total) - from `tabSales Invoice` where project = %s and docstatus=1""", self.name) + total_billed_amount = frappe.db.sql( + """select sum(base_net_total) + from `tabSales Invoice` where project = %s and docstatus=1""", + self.name, + ) self.total_billed_amount = total_billed_amount and total_billed_amount[0][0] or 0 def after_rename(self, old_name, new_name, merge=False): if old_name == self.copied_from: - frappe.db.set_value('Project', new_name, 'copied_from', new_name) + frappe.db.set_value("Project", new_name, "copied_from", new_name) def send_welcome_email(self): url = get_url("/project/?name={0}".format(self.name)) messages = ( _("You have been invited to collaborate on the project: {0}").format(self.name), url, - _("Join") + _("Join"), ) content = """ @@ -254,21 +300,31 @@ class Project(Document): for user in self.users: if user.welcome_email_sent == 0: - frappe.sendmail(user.user, subject=_("Project Collaboration Invitation"), - content=content.format(*messages)) + frappe.sendmail( + user.user, subject=_("Project Collaboration Invitation"), content=content.format(*messages) + ) user.welcome_email_sent = 1 + def get_timeline_data(doctype, name): - '''Return timeline for attendance''' - return dict(frappe.db.sql('''select unix_timestamp(from_time), count(*) + """Return timeline for attendance""" + return dict( + frappe.db.sql( + """select unix_timestamp(from_time), count(*) from `tabTimesheet Detail` where project=%s and from_time > date_sub(curdate(), interval 1 year) and docstatus < 2 - group by date(from_time)''', name)) + group by date(from_time)""", + name, + ) + ) -def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): - return frappe.db.sql('''select distinct project.* +def get_project_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" +): + return frappe.db.sql( + """select distinct project.* from tabProject project, `tabProject User` project_user where (project_user.user = %(user)s @@ -276,27 +332,32 @@ def get_project_list(doctype, txt, filters, limit_start, limit_page_length=20, o or project.owner = %(user)s order by project.modified desc limit {0}, {1} - '''.format(limit_start, limit_page_length), - {'user': frappe.session.user}, - as_dict=True, - update={'doctype': 'Project'}) + """.format( + limit_start, limit_page_length + ), + {"user": frappe.session.user}, + as_dict=True, + update={"doctype": "Project"}, + ) def get_list_context(context=None): return { "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True, + "no_breadcrumbs": True, "title": _("Projects"), "get_list": get_project_list, - "row_template": "templates/includes/projects/project_row.html" + "row_template": "templates/includes/projects/project_row.html", } + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): conditions = [] - return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name) + return frappe.db.sql( + """select name, concat_ws(' ', first_name, middle_name, last_name) from `tabUser` where enabled=1 and name not in ("Guest", "Administrator") @@ -308,47 +369,51 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999), idx desc, name, full_name - limit %(start)s, %(page_len)s""".format(**{ - 'key': searchfield, - 'fcond': get_filters_cond(doctype, filters, conditions), - 'mcond': get_match_cond(doctype) - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s""".format( + **{ + "key": searchfield, + "fcond": get_filters_cond(doctype, filters, conditions), + "mcond": get_match_cond(doctype), + } + ), + {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + ) @frappe.whitelist() def get_cost_center_name(project): return frappe.db.get_value("Project", project, "cost_center") + def hourly_reminder(): fields = ["from_time", "to_time"] projects = get_projects_for_collect_progress("Hourly", fields) for project in projects: - if (get_time(nowtime()) >= get_time(project.from_time) or - get_time(nowtime()) <= get_time(project.to_time)): + if get_time(nowtime()) >= get_time(project.from_time) or get_time(nowtime()) <= get_time( + project.to_time + ): send_project_update_email_to_users(project.name) + def project_status_update_reminder(): daily_reminder() twice_daily_reminder() weekly_reminder() + def daily_reminder(): fields = ["daily_time_to_send"] - projects = get_projects_for_collect_progress("Daily", fields) + projects = get_projects_for_collect_progress("Daily", fields) for project in projects: if allow_to_make_project_update(project.name, project.get("daily_time_to_send"), "Daily"): send_project_update_email_to_users(project.name) + def twice_daily_reminder(): fields = ["first_email", "second_email"] - projects = get_projects_for_collect_progress("Twice Daily", fields) + projects = get_projects_for_collect_progress("Twice Daily", fields) fields.remove("name") for project in projects: @@ -356,9 +421,10 @@ def twice_daily_reminder(): if allow_to_make_project_update(project.name, project.get(d), "Twicely"): send_project_update_email_to_users(project.name) + def weekly_reminder(): fields = ["day_to_send", "weekly_time_to_send"] - projects = get_projects_for_collect_progress("Weekly", fields) + projects = get_projects_for_collect_progress("Weekly", fields) current_day = get_datetime().strftime("%A") for project in projects: @@ -368,12 +434,16 @@ def weekly_reminder(): if allow_to_make_project_update(project.name, project.get("weekly_time_to_send"), "Weekly"): send_project_update_email_to_users(project.name) + def allow_to_make_project_update(project, time, frequency): - data = frappe.db.sql(""" SELECT name from `tabProject Update` - WHERE project = %s and date = %s """, (project, today())) + data = frappe.db.sql( + """ SELECT name from `tabProject Update` + WHERE project = %s and date = %s """, + (project, today()), + ) # len(data) > 1 condition is checked for twicely frequency - if data and (frequency in ['Daily', 'Weekly'] or len(data) > 1): + if data and (frequency in ["Daily", "Weekly"] or len(data) > 1): return False if get_time(nowtime()) >= get_time(time): @@ -382,138 +452,162 @@ def allow_to_make_project_update(project, time, frequency): @frappe.whitelist() def create_duplicate_project(prev_doc, project_name): - ''' Create duplicate project based on the old project ''' + """Create duplicate project based on the old project""" import json + prev_doc = json.loads(prev_doc) - if project_name == prev_doc.get('name'): + if project_name == prev_doc.get("name"): frappe.throw(_("Use a name that is different from previous project name")) # change the copied doc name to new project name project = frappe.copy_doc(prev_doc) project.name = project_name - project.project_template = '' + project.project_template = "" project.project_name = project_name project.insert() # fetch all the task linked with the old project - task_list = frappe.get_all("Task", filters={ - 'project': prev_doc.get('name') - }, fields=['name']) + task_list = frappe.get_all("Task", filters={"project": prev_doc.get("name")}, fields=["name"]) # Create duplicate task for all the task for task in task_list: - task = frappe.get_doc('Task', task) + task = frappe.get_doc("Task", task) new_task = frappe.copy_doc(task) new_task.project = project.name new_task.insert() - project.db_set('project_template', prev_doc.get('project_template')) + project.db_set("project_template", prev_doc.get("project_template")) + def get_projects_for_collect_progress(frequency, fields): fields.extend(["name"]) - return frappe.get_all("Project", fields = fields, - filters = {'collect_progress': 1, 'frequency': frequency, 'status': 'Open'}) + return frappe.get_all( + "Project", + fields=fields, + filters={"collect_progress": 1, "frequency": frequency, "status": "Open"}, + ) + def send_project_update_email_to_users(project): - doc = frappe.get_doc('Project', project) + doc = frappe.get_doc("Project", project) - if is_holiday(doc.holiday_list) or not doc.users: return + if is_holiday(doc.holiday_list) or not doc.users: + return - project_update = frappe.get_doc({ - "doctype" : "Project Update", - "project" : project, - "sent": 0, - "date": today(), - "time": nowtime(), - "naming_series": "UPDATE-.project.-.YY.MM.DD.-", - }).insert() + project_update = frappe.get_doc( + { + "doctype": "Project Update", + "project": project, + "sent": 0, + "date": today(), + "time": nowtime(), + "naming_series": "UPDATE-.project.-.YY.MM.DD.-", + } + ).insert() subject = "For project %s, update your status" % (project) - incoming_email_account = frappe.db.get_value('Email Account', - dict(enable_incoming=1, default_incoming=1), 'email_id') + incoming_email_account = frappe.db.get_value( + "Email Account", dict(enable_incoming=1, default_incoming=1), "email_id" + ) - frappe.sendmail(recipients=get_users_email(doc), + frappe.sendmail( + recipients=get_users_email(doc), message=doc.message, subject=_(subject), reference_doctype=project_update.doctype, reference_name=project_update.name, - reply_to=incoming_email_account + reply_to=incoming_email_account, ) + def collect_project_status(): - for data in frappe.get_all("Project Update", - {'date': today(), 'sent': 0}): - replies = frappe.get_all('Communication', - fields=['content', 'text_content', 'sender'], - filters=dict(reference_doctype="Project Update", + for data in frappe.get_all("Project Update", {"date": today(), "sent": 0}): + replies = frappe.get_all( + "Communication", + fields=["content", "text_content", "sender"], + filters=dict( + reference_doctype="Project Update", reference_name=data.name, - communication_type='Communication', - sent_or_received='Received'), - order_by='creation asc') + communication_type="Communication", + sent_or_received="Received", + ), + order_by="creation asc", + ) for d in replies: doc = frappe.get_doc("Project Update", data.name) - user_data = frappe.db.get_values("User", {"email": d.sender}, - ["full_name", "user_image", "name"], as_dict=True)[0] + user_data = frappe.db.get_values( + "User", {"email": d.sender}, ["full_name", "user_image", "name"], as_dict=True + )[0] - doc.append("users", { - 'user': user_data.name, - 'full_name': user_data.full_name, - 'image': user_data.user_image, - 'project_status': frappe.utils.md_to_html( - EmailReplyParser.parse_reply(d.text_content) or d.content - ) - }) + doc.append( + "users", + { + "user": user_data.name, + "full_name": user_data.full_name, + "image": user_data.user_image, + "project_status": frappe.utils.md_to_html( + EmailReplyParser.parse_reply(d.text_content) or d.content + ), + }, + ) doc.save(ignore_permissions=True) + def send_project_status_email_to_users(): yesterday = add_days(today(), -1) - for d in frappe.get_all("Project Update", - {'date': yesterday, 'sent': 0}): + for d in frappe.get_all("Project Update", {"date": yesterday, "sent": 0}): doc = frappe.get_doc("Project Update", d.name) - project_doc = frappe.get_doc('Project', doc.project) + project_doc = frappe.get_doc("Project", doc.project) - args = { - "users": doc.users, - "title": _("Project Summary for {0}").format(yesterday) - } + args = {"users": doc.users, "title": _("Project Summary for {0}").format(yesterday)} - frappe.sendmail(recipients=get_users_email(project_doc), - template='daily_project_summary', + frappe.sendmail( + recipients=get_users_email(project_doc), + template="daily_project_summary", args=args, subject=_("Daily Project Summary for {0}").format(d.name), reference_doctype="Project Update", - reference_name=d.name) + reference_name=d.name, + ) + + doc.db_set("sent", 1) - doc.db_set('sent', 1) def update_project_sales_billing(): sales_update_frequency = frappe.db.get_single_value("Selling Settings", "sales_update_frequency") if sales_update_frequency == "Each Transaction": return - elif (sales_update_frequency == "Monthly" and frappe.utils.now_datetime().day != 1): + elif sales_update_frequency == "Monthly" and frappe.utils.now_datetime().day != 1: return - #Else simply fallback to Daily - exists_query = '(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)' + # Else simply fallback to Daily + exists_query = ( + "(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)" + ) project_map = {} - for project_details in frappe.db.sql(''' + for project_details in frappe.db.sql( + """ SELECT name, 1 as order_exists, null as invoice_exists from `tabProject` where exists {order_exists} union SELECT name, null as order_exists, 1 as invoice_exists from `tabProject` where exists {invoice_exists} - '''.format( + """.format( order_exists=exists_query.format(doctype="Sales Order"), invoice_exists=exists_query.format(doctype="Sales Invoice"), - ), as_dict=True): - project = project_map.setdefault(project_details.name, frappe.get_doc('Project', project_details.name)) + ), + as_dict=True, + ): + project = project_map.setdefault( + project_details.name, frappe.get_doc("Project", project_details.name) + ) if project_details.order_exists: project.update_sales_amount() if project_details.invoice_exists: @@ -522,29 +616,31 @@ def update_project_sales_billing(): for project in project_map.values(): project.save() + @frappe.whitelist() def create_kanban_board_if_not_exists(project): from frappe.desk.doctype.kanban_board.kanban_board import quick_kanban_board - project = frappe.get_doc('Project', project) - if not frappe.db.exists('Kanban Board', project.project_name): - quick_kanban_board('Task', project.project_name, 'status', project.name) + project = frappe.get_doc("Project", project) + if not frappe.db.exists("Kanban Board", project.project_name): + quick_kanban_board("Task", project.project_name, "status", project.name) return True + @frappe.whitelist() def set_project_status(project, status): - ''' + """ set status for project and all related tasks - ''' - if not status in ('Completed', 'Cancelled'): - frappe.throw(_('Status must be Cancelled or Completed')) + """ + if not status in ("Completed", "Cancelled"): + frappe.throw(_("Status must be Cancelled or Completed")) - project = frappe.get_doc('Project', project) - frappe.has_permission(doc = project, throw = True) + project = frappe.get_doc("Project", project) + frappe.has_permission(doc=project, throw=True) - for task in frappe.get_all('Task', dict(project = project.name)): - frappe.db.set_value('Task', task.name, 'status', status) + for task in frappe.get_all("Task", dict(project=project.name)): + frappe.db.set_value("Task", task.name, "status", status) project.status = status project.save() diff --git a/erpnext/projects/doctype/project/project_dashboard.py b/erpnext/projects/doctype/project/project_dashboard.py index df274ed9a94..b6c74f93870 100644 --- a/erpnext/projects/doctype/project/project_dashboard.py +++ b/erpnext/projects/doctype/project/project_dashboard.py @@ -1,28 +1,18 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on the Time Sheets created against this project'), - 'fieldname': 'project', - 'transactions': [ + "heatmap": True, + "heatmap_message": _("This is based on the Time Sheets created against this project"), + "fieldname": "project", + "transactions": [ { - 'label': _('Project'), - 'items': ['Task', 'Timesheet', 'Expense Claim', 'Issue' , 'Project Update'] + "label": _("Project"), + "items": ["Task", "Timesheet", "Expense Claim", "Issue", "Project Update"], }, - { - 'label': _('Material'), - 'items': ['Material Request', 'BOM', 'Stock Entry'] - }, - { - 'label': _('Sales'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - }, - ] + {"label": _("Material"), "items": ["Material Request", "BOM", "Stock Entry"]}, + {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Purchase"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, + ], } diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index df42e82ad47..8a599cef753 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -11,7 +11,7 @@ from erpnext.projects.doctype.task.test_task import create_task from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -test_records = frappe.get_test_records('Project') +test_records = frappe.get_test_records("Project") test_ignore = ["Sales Order"] @@ -19,53 +19,83 @@ class TestProject(unittest.TestCase): def test_project_with_template_having_no_parent_and_depend_tasks(self): project_name = "Test Project with Template - No Parent and Dependend Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) - frappe.delete_doc('Project', project_name) + frappe.delete_doc("Project", project_name) task1 = task_exists("Test Template Task with No Parent and Dependency") if not task1: - task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3) + task1 = create_task( + subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3 + ) - template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1]) + template = make_project_template( + "Test Project Template - No Parent and Dependend Tasks", [task1] + ) project = get_project(project_name, template) - tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc') + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks"], + dict(project=project.name), + order_by="creation asc", + ) - self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency') + self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency") self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3)) self.assertEqual(len(tasks), 1) def test_project_template_having_parent_child_tasks(self): project_name = "Test Project with Template - Tasks with Parent-Child Relation" - if frappe.db.get_value('Project', {'project_name': project_name}, 'name'): - project_name = frappe.db.get_value('Project', {'project_name': project_name}, 'name') + if frappe.db.get_value("Project", {"project_name": project_name}, "name"): + project_name = frappe.db.get_value("Project", {"project_name": project_name}, "name") frappe.db.sql(""" delete from tabTask where project = %s """, project_name) - frappe.delete_doc('Project', project_name) + frappe.delete_doc("Project", project_name) task1 = task_exists("Test Template Task Parent") if not task1: - task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=10) + task1 = create_task( + subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=10 + ) task2 = task_exists("Test Template Task Child 1") if not task2: - task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3) + task2 = create_task( + subject="Test Template Task Child 1", + parent_task=task1.name, + is_template=1, + begin=1, + duration=3, + ) task3 = task_exists("Test Template Task Child 2") if not task3: - task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3) + task3 = create_task( + subject="Test Template Task Child 2", + parent_task=task1.name, + is_template=1, + begin=2, + duration=3, + ) - template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3]) + template = make_project_template( + "Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3] + ) project = get_project(project_name, template) - tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc') + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"], + dict(project=project.name), + order_by="creation asc", + ) - self.assertEqual(tasks[0].subject, 'Test Template Task Parent') + self.assertEqual(tasks[0].subject, "Test Template Task Parent") self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 10)) - self.assertEqual(tasks[1].subject, 'Test Template Task Child 1') + self.assertEqual(tasks[1].subject, "Test Template Task Child 1") self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3)) self.assertEqual(tasks[1].parent_task, tasks[0].name) - self.assertEqual(tasks[2].subject, 'Test Template Task Child 2') + self.assertEqual(tasks[2].subject, "Test Template Task Child 2") self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3)) self.assertEqual(tasks[2].parent_task, tasks[0].name) @@ -74,26 +104,39 @@ class TestProject(unittest.TestCase): def test_project_template_having_dependent_tasks(self): project_name = "Test Project with Template - Dependent Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) - frappe.delete_doc('Project', project_name) + frappe.delete_doc("Project", project_name) task1 = task_exists("Test Template Task for Dependency") if not task1: - task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1) + task1 = create_task( + subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1 + ) task2 = task_exists("Test Template Task with Dependency") if not task2: - task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2) + task2 = create_task( + subject="Test Template Task with Dependency", + depends_on=task1.name, + is_template=1, + begin=2, + duration=2, + ) template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2]) project = get_project(project_name, template) - tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc') + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks", "name"], + dict(project=project.name), + order_by="creation asc", + ) - self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency') + self.assertEqual(tasks[1].subject, "Test Template Task with Dependency") self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2)) - self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 ) + self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0) - self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) ) + self.assertEqual(tasks[0].subject, "Test Template Task for Dependency") + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1)) self.assertEqual(len(tasks), 2) @@ -112,32 +155,38 @@ class TestProject(unittest.TestCase): so.reload() self.assertFalse(so.project) + def get_project(name, template): - project = frappe.get_doc(dict( - doctype = 'Project', - project_name = name, - status = 'Open', - project_template = template.name, - expected_start_date = nowdate(), - company="_Test Company" - )).insert() + project = frappe.get_doc( + dict( + doctype="Project", + project_name=name, + status="Open", + project_template=template.name, + expected_start_date=nowdate(), + company="_Test Company", + ) + ).insert() return project + def make_project(args): args = frappe._dict(args) if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}): return frappe.get_doc("Project", {"project_name": args.project_name}) - project = frappe.get_doc(dict( - doctype = 'Project', - project_name = args.project_name, - status = 'Open', - expected_start_date = args.start_date, - company= args.company or '_Test Company' - )) + project = frappe.get_doc( + dict( + doctype="Project", + project_name=args.project_name, + status="Open", + expected_start_date=args.start_date, + company=args.company or "_Test Company", + ) + ) if args.project_template_name: template = make_project_template(args.project_template_name) @@ -147,12 +196,14 @@ def make_project(args): return project + def task_exists(subject): - result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"]) + result = frappe.db.get_list("Task", filters={"subject": subject}, fields=["name"]) if not len(result): return False return frappe.get_doc("Task", result[0].name) + def calculate_end_date(project, start, duration): start = add_days(project.expected_start_date, start) start = project.update_if_holiday(start) diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py index 3cc8d6855f2..89afb1bd770 100644 --- a/erpnext/projects/doctype/project_template/project_template.py +++ b/erpnext/projects/doctype/project_template/project_template.py @@ -9,7 +9,6 @@ from frappe.utils import get_link_to_form class ProjectTemplate(Document): - def validate(self): self.validate_dependencies() @@ -19,9 +18,13 @@ class ProjectTemplate(Document): if task_details.depends_on: for dependency_task in task_details.depends_on: if not self.check_dependent_task_presence(dependency_task.task): - task_details_format = get_link_to_form("Task",task_details.name) + task_details_format = get_link_to_form("Task", task_details.name) dependency_task_format = get_link_to_form("Task", dependency_task.task) - frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format))) + frappe.throw( + _("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format( + frappe.bold(task_details_format), frappe.bold(dependency_task_format) + ) + ) def check_dependent_task_presence(self, task): for task_details in self.tasks: diff --git a/erpnext/projects/doctype/project_template/project_template_dashboard.py b/erpnext/projects/doctype/project_template/project_template_dashboard.py index 65cd8d4b55a..0c567c1e599 100644 --- a/erpnext/projects/doctype/project_template/project_template_dashboard.py +++ b/erpnext/projects/doctype/project_template/project_template_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'project_template', - 'transactions': [ - { - 'items': ['Project'] - } - ] - } + return {"fieldname": "project_template", "transactions": [{"items": ["Project"]}]} diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py index 842483343a2..4fd24bf78a2 100644 --- a/erpnext/projects/doctype/project_template/test_project_template.py +++ b/erpnext/projects/doctype/project_template/test_project_template.py @@ -11,20 +11,16 @@ from erpnext.projects.doctype.task.test_task import create_task class TestProjectTemplate(unittest.TestCase): pass + def make_project_template(project_template_name, project_tasks=[]): - if not frappe.db.exists('Project Template', project_template_name): + if not frappe.db.exists("Project Template", project_template_name): project_tasks = project_tasks or [ - create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), - create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), - ] - doc = frappe.get_doc(dict( - doctype = 'Project Template', - name = project_template_name - )) + create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3), + create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2), + ] + doc = frappe.get_doc(dict(doctype="Project Template", name=project_template_name)) for task in project_tasks: - doc.append("tasks",{ - "task": task.name - }) + doc.append("tasks", {"task": task.name}) doc.insert() - return frappe.get_doc('Project Template', project_template_name) + return frappe.get_doc("Project Template", project_template_name) diff --git a/erpnext/projects/doctype/project_update/project_update.py b/erpnext/projects/doctype/project_update/project_update.py index 42ba5f6075c..5a29fb6c33e 100644 --- a/erpnext/projects/doctype/project_update/project_update.py +++ b/erpnext/projects/doctype/project_update/project_update.py @@ -7,36 +7,88 @@ from frappe.model.document import Document class ProjectUpdate(Document): - pass + pass + @frappe.whitelist() def daily_reminder(): - project = frappe.db.sql("""SELECT `tabProject`.project_name,`tabProject`.frequency,`tabProject`.expected_start_date,`tabProject`.expected_end_date,`tabProject`.percent_complete FROM `tabProject`;""") - for projects in project: - project_name = projects[0] - frequency = projects[1] - date_start = projects[2] - date_end = projects [3] - progress = projects [4] - draft = frappe.db.sql("""SELECT count(docstatus) from `tabProject Update` WHERE `tabProject Update`.project = %s AND `tabProject Update`.docstatus = 0;""",project_name) - for drafts in draft: - number_of_drafts = drafts[0] - update = frappe.db.sql("""SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""",project_name) - email_sending(project_name,frequency,date_start,date_end,progress,number_of_drafts,update) + project = frappe.db.sql( + """SELECT `tabProject`.project_name,`tabProject`.frequency,`tabProject`.expected_start_date,`tabProject`.expected_end_date,`tabProject`.percent_complete FROM `tabProject`;""" + ) + for projects in project: + project_name = projects[0] + frequency = projects[1] + date_start = projects[2] + date_end = projects[3] + progress = projects[4] + draft = frappe.db.sql( + """SELECT count(docstatus) from `tabProject Update` WHERE `tabProject Update`.project = %s AND `tabProject Update`.docstatus = 0;""", + project_name, + ) + for drafts in draft: + number_of_drafts = drafts[0] + update = frappe.db.sql( + """SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""", + project_name, + ) + email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update) -def email_sending(project_name,frequency,date_start,date_end,progress,number_of_drafts,update): - holiday = frappe.db.sql("""SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();""") - msg = "

    Project Name: " + project_name + "

    Frequency: " + " " + frequency + "

    Update Reminder:" + " " + str(date_start) + "

    Expected Date End:" + " " + str(date_end) + "

    Percent Progress:" + " " + str(progress) + "

    Number of Updates:" + " " + str(len(update)) + "

    " + "

    Number of drafts:" + " " + str(number_of_drafts) + "

    " - msg += """

    +def email_sending( + project_name, frequency, date_start, date_end, progress, number_of_drafts, update +): + + holiday = frappe.db.sql( + """SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();""" + ) + msg = ( + "

    Project Name: " + + project_name + + "

    Frequency: " + + " " + + frequency + + "

    Update Reminder:" + + " " + + str(date_start) + + "

    Expected Date End:" + + " " + + str(date_end) + + "

    Percent Progress:" + + " " + + str(progress) + + "

    Number of Updates:" + + " " + + str(len(update)) + + "

    " + + "

    Number of drafts:" + + " " + + str(number_of_drafts) + + "

    " + ) + msg += """

    """ - for updates in update: - msg += "" + "" + for updates in update: + msg += ( + "" + + "" + ) - msg += "
    Project IDDate UpdatedTime UpdatedProject StatusNotes
    " + str(updates[0]) + "" + str(updates[1]) + "" + str(updates[2]) + "" + str(updates[3]) + "" + str(updates[4]) + "
    " + + str(updates[0]) + + "" + + str(updates[1]) + + "" + + str(updates[2]) + + "" + + str(updates[3]) + + "" + + str(updates[4]) + + "
    " - if len(holiday) == 0: - email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project_name) - for emails in email: - frappe.sendmail(recipients=emails,subject=frappe._(project_name + ' ' + 'Summary'),message = msg) - else: - pass + msg += "" + if len(holiday) == 0: + email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project_name) + for emails in email: + frappe.sendmail( + recipients=emails, subject=frappe._(project_name + " " + "Summary"), message=msg + ) + else: + pass diff --git a/erpnext/projects/doctype/project_update/test_project_update.py b/erpnext/projects/doctype/project_update/test_project_update.py index f29c931ac05..8663350c8f2 100644 --- a/erpnext/projects/doctype/project_update/test_project_update.py +++ b/erpnext/projects/doctype/project_update/test_project_update.py @@ -9,5 +9,6 @@ import frappe class TestProjectUpdate(unittest.TestCase): pass -test_records = frappe.get_test_records('Project Update') + +test_records = frappe.get_test_records("Project Update") test_ignore = ["Sales Order"] diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 8fa0538f360..4575fb544c4 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -12,19 +12,24 @@ from frappe.utils import add_days, cstr, date_diff, flt, get_link_to_form, getda from frappe.utils.nestedset import NestedSet -class CircularReferenceError(frappe.ValidationError): pass -class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass +class CircularReferenceError(frappe.ValidationError): + pass + + +class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): + pass + class Task(NestedSet): - nsm_parent_field = 'parent_task' + nsm_parent_field = "parent_task" def get_feed(self): - return '{0}: {1}'.format(_(self.status), self.subject) + return "{0}: {1}".format(_(self.status), self.subject) def get_customer_details(self): cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer) if cust: - ret = {'customer_name': cust and cust[0][0] or ''} + ret = {"customer_name": cust and cust[0][0] or ""} return ret def validate(self): @@ -38,19 +43,37 @@ class Task(NestedSet): self.validate_completed_on() def validate_dates(self): - if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ - frappe.bold("Expected End Date"))) + if ( + self.exp_start_date + and self.exp_end_date + and getdate(self.exp_start_date) > getdate(self.exp_end_date) + ): + frappe.throw( + _("{0} can not be greater than {1}").format( + frappe.bold("Expected Start Date"), frappe.bold("Expected End Date") + ) + ) - if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): - frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ - frappe.bold("Actual End Date"))) + if ( + self.act_start_date + and self.act_end_date + and getdate(self.act_start_date) > getdate(self.act_end_date) + ): + frappe.throw( + _("{0} can not be greater than {1}").format( + frappe.bold("Actual Start Date"), frappe.bold("Actual End Date") + ) + ) def validate_parent_expected_end_date(self): if self.parent_task: parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date") if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date): - frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date))) + frappe.throw( + _( + "Expected End Date should be less than or equal to parent task's Expected End Date {0}." + ).format(getdate(parent_exp_end_date)) + ) def validate_parent_project_dates(self): if not self.project or frappe.flags.in_test: @@ -59,16 +82,24 @@ class Task(NestedSet): expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") if expected_end_date: - validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") - validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") + validate_project_dates( + getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected" + ) + validate_project_dates( + getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual" + ) def validate_status(self): if self.is_template and self.status != "Template": self.status = "Template" - if self.status!=self.get_db_value("status") and self.status == "Completed": + if self.status != self.get_db_value("status") and self.status == "Completed": for d in self.depends_on: if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"): - frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task))) + frappe.throw( + _( + "Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled." + ).format(frappe.bold(self.name), frappe.bold(d.task)) + ) close_all_assignments(self.doctype, self.name) @@ -76,7 +107,7 @@ class Task(NestedSet): if flt(self.progress or 0) > 100: frappe.throw(_("Progress % for a task cannot be more than 100.")) - if self.status == 'Completed': + if self.status == "Completed": self.progress = 100 def validate_dependencies_for_template_task(self): @@ -126,34 +157,43 @@ class Task(NestedSet): clear(self.doctype, self.name) def update_total_expense_claim(self): - self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` - where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0] + self.total_expense_claim = frappe.db.sql( + """select sum(total_sanctioned_amount) from `tabExpense Claim` + where project = %s and task = %s and docstatus=1""", + (self.project, self.name), + )[0][0] def update_time_and_costing(self): - tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date, + tl = frappe.db.sql( + """select min(from_time) as start_date, max(to_time) as end_date, sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount, - sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""" - ,self.name, as_dict=1)[0] + sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1""", + self.name, + as_dict=1, + )[0] if self.status == "Open": self.status = "Working" - self.total_costing_amount= tl.total_costing_amount - self.total_billing_amount= tl.total_billing_amount - self.actual_time= tl.time - self.act_start_date= tl.start_date - self.act_end_date= tl.end_date + self.total_costing_amount = tl.total_costing_amount + self.total_billing_amount = tl.total_billing_amount + self.actual_time = tl.time + self.act_start_date = tl.start_date + self.act_end_date = tl.end_date def update_project(self): if self.project and not self.flags.from_project: frappe.get_cached_doc("Project", self.project).update_project() def check_recursion(self): - if self.flags.ignore_recursion_check: return - check_list = [['task', 'parent'], ['parent', 'task']] + if self.flags.ignore_recursion_check: + return + check_list = [["task", "parent"], ["parent", "task"]] for d in check_list: task_list, count = [self.name], 0 - while (len(task_list) > count ): - tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " % - (d[0], d[1], '%s'), cstr(task_list[count])) + while len(task_list) > count: + tasks = frappe.db.sql( + " select %s from `tabTask Depends On` where %s = %s " % (d[0], d[1], "%s"), + cstr(task_list[count]), + ) count = count + 1 for b in tasks: if b[0] == self.name: @@ -167,15 +207,24 @@ class Task(NestedSet): def reschedule_dependent_tasks(self): end_date = self.exp_end_date or self.act_end_date if end_date: - for task_name in frappe.db.sql(""" + for task_name in frappe.db.sql( + """ select name from `tabTask` as parent where parent.project = %(project)s and parent.name in ( select parent from `tabTask Depends On` as child where child.task = %(task)s and child.project = %(project)s) - """, {'project': self.project, 'task':self.name }, as_dict=1): + """, + {"project": self.project, "task": self.name}, + as_dict=1, + ): task = frappe.get_doc("Task", task_name.name) - if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open": + if ( + task.exp_start_date + and task.exp_end_date + and task.exp_start_date < getdate(end_date) + and task.status == "Open" + ): task_duration = date_diff(task.exp_end_date, task.exp_start_date) task.exp_start_date = add_days(end_date, 1) task.exp_end_date = add_days(task.exp_start_date, task_duration) @@ -183,19 +232,19 @@ class Task(NestedSet): task.save() def has_webform_permission(self): - project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user") + project_user = frappe.db.get_value( + "Project User", {"parent": self.project, "user": frappe.session.user}, "user" + ) if project_user: return True def populate_depends_on(self): if self.parent_task: - parent = frappe.get_doc('Task', self.parent_task) + parent = frappe.get_doc("Task", self.parent_task) if self.name not in [row.task for row in parent.depends_on]: - parent.append("depends_on", { - "doctype": "Task Depends On", - "task": self.name, - "subject": self.subject - }) + parent.append( + "depends_on", {"doctype": "Task Depends On", "task": self.name, "subject": self.subject} + ) parent.save() def on_trash(self): @@ -208,12 +257,14 @@ class Task(NestedSet): self.update_project() def update_status(self): - if self.status not in ('Cancelled', 'Completed') and self.exp_end_date: + if self.status not in ("Cancelled", "Completed") and self.exp_end_date: from datetime import datetime + if self.exp_end_date < datetime.now().date(): - self.db_set('status', 'Overdue', update_modified=False) + self.db_set("status", "Overdue", update_modified=False) self.update_project() + @frappe.whitelist() def check_if_child_exists(name): child_tasks = frappe.get_all("Task", filters={"parent_task": name}) @@ -225,24 +276,29 @@ def check_if_child_exists(name): @frappe.validate_and_sanitize_search_inputs def get_project(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond + meta = frappe.get_meta(doctype) searchfields = meta.get_search_fields() - search_columns = ", " + ", ".join(searchfields) if searchfields else '' + search_columns = ", " + ", ".join(searchfields) if searchfields else "" search_cond = " or " + " or ".join(field + " like %(txt)s" for field in searchfields) - return frappe.db.sql(""" select name {search_columns} from `tabProject` + return frappe.db.sql( + """ select name {search_columns} from `tabProject` where %(key)s like %(txt)s %(mcond)s {search_condition} order by name - limit %(start)s, %(page_len)s""".format(search_columns = search_columns, - search_condition=search_cond), { - 'key': searchfield, - 'txt': '%' + txt + '%', - 'mcond':get_match_cond(doctype), - 'start': start, - 'page_len': page_len - }) + limit %(start)s, %(page_len)s""".format( + search_columns=search_columns, search_condition=search_cond + ), + { + "key": searchfield, + "txt": "%" + txt + "%", + "mcond": get_match_cond(doctype), + "start": start, + "page_len": page_len, + }, + ) @frappe.whitelist() @@ -253,8 +309,13 @@ def set_multiple_status(names, status): task.status = status task.save() + def set_tasks_as_overdue(): - tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"]) + tasks = frappe.get_all( + "Task", + filters={"status": ["not in", ["Cancelled", "Completed"]]}, + fields=["name", "status", "review_date"], + ) for task in tasks: if task.status == "Pending Review": if getdate(task.review_date) > getdate(today()): @@ -265,18 +326,24 @@ def set_tasks_as_overdue(): @frappe.whitelist() def make_timesheet(source_name, target_doc=None, ignore_permissions=False): def set_missing_values(source, target): - target.append("time_logs", { - "hours": source.actual_time, - "completed": source.status == "Completed", - "project": source.project, - "task": source.name - }) + target.append( + "time_logs", + { + "hours": source.actual_time, + "completed": source.status == "Completed", + "project": source.project, + "task": source.name, + }, + ) - doclist = get_mapped_doc("Task", source_name, { - "Task": { - "doctype": "Timesheet" - } - }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions) + doclist = get_mapped_doc( + "Task", + source_name, + {"Task": {"doctype": "Timesheet"}}, + target_doc, + postprocess=set_missing_values, + ignore_permissions=ignore_permissions, + ) return doclist @@ -284,60 +351,69 @@ def make_timesheet(source_name, target_doc=None, ignore_permissions=False): @frappe.whitelist() def get_children(doctype, parent, task=None, project=None, is_root=False): - filters = [['docstatus', '<', '2']] + filters = [["docstatus", "<", "2"]] if task: - filters.append(['parent_task', '=', task]) + filters.append(["parent_task", "=", task]) elif parent and not is_root: # via expand child - filters.append(['parent_task', '=', parent]) + filters.append(["parent_task", "=", parent]) else: - filters.append(['ifnull(`parent_task`, "")', '=', '']) + filters.append(['ifnull(`parent_task`, "")', "=", ""]) if project: - filters.append(['project', '=', project]) + filters.append(["project", "=", project]) - tasks = frappe.get_list(doctype, fields=[ - 'name as value', - 'subject as title', - 'is_group as expandable' - ], filters=filters, order_by='name') + tasks = frappe.get_list( + doctype, + fields=["name as value", "subject as title", "is_group as expandable"], + filters=filters, + order_by="name", + ) # return tasks return tasks + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict - args.update({ - "name_field": "subject" - }) + args.update({"name_field": "subject"}) args = make_tree_args(**args) - if args.parent_task == 'All Tasks' or args.parent_task == args.project: + if args.parent_task == "All Tasks" or args.parent_task == args.project: args.parent_task = None frappe.get_doc(args).insert() + @frappe.whitelist() def add_multiple_tasks(data, parent): data = json.loads(data) - new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""} - new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or "" + new_doc = {"doctype": "Task", "parent_task": parent if parent != "All Tasks" else ""} + new_doc["project"] = frappe.db.get_value("Task", {"name": parent}, "project") or "" for d in data: - if not d.get("subject"): continue - new_doc['subject'] = d.get("subject") + if not d.get("subject"): + continue + new_doc["subject"] = d.get("subject") new_task = frappe.get_doc(new_doc) new_task.insert() + def on_doctype_update(): frappe.db.add_index("Task", ["lft", "rgt"]) + def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date): if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: - frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) + frappe.throw( + _("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date) + ) if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: - frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) + frappe.throw( + _("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date) + ) diff --git a/erpnext/projects/doctype/task/task_dashboard.py b/erpnext/projects/doctype/task/task_dashboard.py index 40d04e13ebc..07477da17a4 100644 --- a/erpnext/projects/doctype/task/task_dashboard.py +++ b/erpnext/projects/doctype/task/task_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'task', - 'transactions': [ - { - 'label': _('Activity'), - 'items': ['Timesheet'] - }, - { - 'label': _('Accounting'), - 'items': ['Expense Claim'] - } - ] + "fieldname": "task", + "transactions": [ + {"label": _("Activity"), "items": ["Timesheet"]}, + {"label": _("Accounting"), "items": ["Expense Claim"]}, + ], } diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index a0ac7c14978..aa72ac3104e 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -16,9 +16,7 @@ class TestTask(unittest.TestCase): task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name) task1.reload() - task1.append("depends_on", { - "task": task3.name - }) + task1.append("depends_on", {"task": task3.name}) self.assertRaises(CircularReferenceError, task1.save) @@ -27,9 +25,7 @@ class TestTask(unittest.TestCase): task4 = create_task("_Test Task 4", nowdate(), add_days(nowdate(), 15), task1.name) - task3.append("depends_on", { - "task": task4.name - }) + task3.append("depends_on", {"task": task4.name}) def test_reschedule_dependent_task(self): project = frappe.get_value("Project", {"project_name": "_Test Project"}) @@ -44,20 +40,22 @@ class TestTask(unittest.TestCase): task3.get("depends_on")[0].project = project task3.save() - task1.update({ - "exp_end_date": add_days(nowdate(), 20) - }) + task1.update({"exp_end_date": add_days(nowdate(), 20)}) task1.save() - self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"), - getdate(add_days(nowdate(), 21))) - self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"), - getdate(add_days(nowdate(), 25))) + self.assertEqual( + frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate(add_days(nowdate(), 21)) + ) + self.assertEqual( + frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate(add_days(nowdate(), 25)) + ) - self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), - getdate(add_days(nowdate(), 26))) - self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), - getdate(add_days(nowdate(), 30))) + self.assertEqual( + frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate(add_days(nowdate(), 26)) + ) + self.assertEqual( + frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate(add_days(nowdate(), 30)) + ) def test_close_assignment(self): if not frappe.db.exists("Task", "Test Close Assignment"): @@ -67,18 +65,27 @@ class TestTask(unittest.TestCase): def assign(): from frappe.desk.form import assign_to - assign_to.add({ - "assign_to": ["test@example.com"], - "doctype": task.doctype, - "name": task.name, - "description": "Close this task" - }) + + assign_to.add( + { + "assign_to": ["test@example.com"], + "doctype": task.doctype, + "name": task.name, + "description": "Close this task", + } + ) def get_owner_and_status(): - return frappe.db.get_value("ToDo", - filters={"reference_type": task.doctype, "reference_name": task.name, - "description": "Close this task"}, - fieldname=("owner", "status"), as_dict=True) + return frappe.db.get_value( + "ToDo", + filters={ + "reference_type": task.doctype, + "reference_name": task.name, + "description": "Close this task", + }, + fieldname=("owner", "status"), + as_dict=True, + ) assign() todo = get_owner_and_status() @@ -97,18 +104,36 @@ class TestTask(unittest.TestCase): task = create_task("Testing Overdue", add_days(nowdate(), -10), add_days(nowdate(), -5)) from erpnext.projects.doctype.task.task import set_tasks_as_overdue + set_tasks_as_overdue() self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue") -def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True): + +def create_task( + subject, + start=None, + end=None, + depends_on=None, + project=None, + parent_task=None, + is_group=0, + is_template=0, + begin=0, + duration=0, + save=True, +): if not frappe.db.exists("Task", subject): - task = frappe.new_doc('Task') + task = frappe.new_doc("Task") task.status = "Open" task.subject = subject task.exp_start_date = start or nowdate() task.exp_end_date = end or nowdate() - task.project = project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"}) + task.project = ( + project or None + if is_template + else frappe.get_value("Project", {"project_name": "_Test Project"}) + ) task.is_template = is_template task.start = begin task.duration = duration @@ -120,9 +145,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None, pa task = frappe.get_doc("Task", subject) if depends_on: - task.append("depends_on", { - "task": depends_on - }) + task.append("depends_on", {"task": depends_on}) if save: task.save() return task diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 8b603570217..57bfd5b6074 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -27,8 +27,8 @@ from erpnext.projects.doctype.timesheet.timesheet import ( class TestTimesheet(unittest.TestCase): @classmethod def setUpClass(cls): - make_earning_salary_component(setup=True, company_list=['_Test Company']) - make_deduction_salary_component(setup=True, company_list=['_Test Company']) + make_earning_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, company_list=["_Test Company"]) def setUp(self): for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]: @@ -62,7 +62,7 @@ class TestTimesheet(unittest.TestCase): emp = make_employee("test_employee_6@salary.com", company="_Test Company") salary_structure = make_salary_structure_for_timesheet(emp) - timesheet = make_timesheet(emp, simulate = True, is_billable=1) + timesheet = make_timesheet(emp, simulate=True, is_billable=1) salary_slip = make_salary_slip(timesheet.name) salary_slip.submit() @@ -73,27 +73,27 @@ class TestTimesheet(unittest.TestCase): self.assertEqual(salary_slip.timesheets[0].time_sheet, timesheet.name) self.assertEqual(salary_slip.timesheets[0].working_hours, 2) - timesheet = frappe.get_doc('Timesheet', timesheet.name) - self.assertEqual(timesheet.status, 'Payslip') + timesheet = frappe.get_doc("Timesheet", timesheet.name) + self.assertEqual(timesheet.status, "Payslip") salary_slip.cancel() - timesheet = frappe.get_doc('Timesheet', timesheet.name) - self.assertEqual(timesheet.status, 'Submitted') + timesheet = frappe.get_doc("Timesheet", timesheet.name) + self.assertEqual(timesheet.status, "Submitted") def test_sales_invoice_from_timesheet(self): emp = make_employee("test_employee_6@salary.com") timesheet = make_timesheet(emp, simulate=True, is_billable=1) - sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer') + sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer") sales_invoice.due_date = nowdate() sales_invoice.submit() - timesheet = frappe.get_doc('Timesheet', timesheet.name) + timesheet = frappe.get_doc("Timesheet", timesheet.name) self.assertEqual(sales_invoice.total_billing_amount, 100) - self.assertEqual(timesheet.status, 'Billed') - self.assertEqual(sales_invoice.customer, '_Test Customer') + self.assertEqual(timesheet.status, "Billed") + self.assertEqual(sales_invoice.customer, "_Test Customer") item = sales_invoice.items[0] - self.assertEqual(item.item_code, '_Test Item') + self.assertEqual(item.item_code, "_Test Item") self.assertEqual(item.qty, 2.00) self.assertEqual(item.rate, 50.00) @@ -101,19 +101,21 @@ class TestTimesheet(unittest.TestCase): emp = make_employee("test_employee_6@salary.com") project = frappe.get_value("Project", {"project_name": "_Test Project"}) - timesheet = make_timesheet(emp, simulate=True, is_billable=1, project=project, company='_Test Company') + timesheet = make_timesheet( + emp, simulate=True, is_billable=1, project=project, company="_Test Company" + ) sales_invoice = create_sales_invoice(do_not_save=True) sales_invoice.project = project sales_invoice.submit() - ts = frappe.get_doc('Timesheet', timesheet.name) + ts = frappe.get_doc("Timesheet", timesheet.name) self.assertEqual(ts.per_billed, 100) self.assertEqual(ts.time_logs[0].sales_invoice, sales_invoice.name) def test_timesheet_time_overlap(self): emp = make_employee("test_employee_6@salary.com") - settings = frappe.get_single('Projects Settings') + settings = frappe.get_single("Projects Settings") initial_setting = settings.ignore_employee_time_overlap settings.ignore_employee_time_overlap = 0 settings.save() @@ -122,24 +124,24 @@ class TestTimesheet(unittest.TestCase): timesheet = frappe.new_doc("Timesheet") timesheet.employee = emp timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime(), "to_time": now_datetime() + datetime.timedelta(hours=3), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime(), "to_time": now_datetime() + datetime.timedelta(hours=3), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) self.assertRaises(frappe.ValidationError, timesheet.save) @@ -158,27 +160,27 @@ class TestTimesheet(unittest.TestCase): timesheet = frappe.new_doc("Timesheet") timesheet.employee = emp timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime(), "to_time": now_datetime() + datetime.timedelta(hours=3), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": now_datetime() + datetime.timedelta(hours=3), "to_time": now_datetime() + datetime.timedelta(hours=4), - "company": "_Test Company" - } + "company": "_Test Company", + }, ) - timesheet.save() # should not throw an error + timesheet.save() # should not throw an error def test_to_time(self): emp = make_employee("test_employee_6@salary.com") @@ -187,14 +189,14 @@ class TestTimesheet(unittest.TestCase): timesheet = frappe.new_doc("Timesheet") timesheet.employee = emp timesheet.append( - 'time_logs', + "time_logs", { "billable": 1, "activity_type": "_Test Activity Type", "from_time": from_time, "hours": 2, - "company": "_Test Company" - } + "company": "_Test Company", + }, ) timesheet.save() @@ -207,39 +209,51 @@ def make_salary_structure_for_timesheet(employee, company=None): frequency = "Monthly" if not frappe.db.exists("Salary Component", "Timesheet Component"): - frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert() + frappe.get_doc( + {"doctype": "Salary Component", "salary_component": "Timesheet Component"} + ).insert() - salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True) + salary_structure = make_salary_structure( + salary_structure_name, frequency, company=company, dont_submit=True + ) salary_structure.salary_component = "Timesheet Component" salary_structure.salary_slip_based_on_timesheet = 1 salary_structure.hour_rate = 50.0 salary_structure.save() salary_structure.submit() - if not frappe.db.get_value("Salary Structure Assignment", - {'employee':employee, 'docstatus': 1}): - frappe.db.set_value('Employee', employee, 'date_of_joining', - add_months(nowdate(), -5)) - create_salary_structure_assignment(employee, salary_structure.name) + if not frappe.db.get_value("Salary Structure Assignment", {"employee": employee, "docstatus": 1}): + frappe.db.set_value("Employee", employee, "date_of_joining", add_months(nowdate(), -5)) + create_salary_structure_assignment(employee, salary_structure.name) return salary_structure -def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None): +def make_timesheet( + employee, + simulate=False, + is_billable=0, + activity_type="_Test Activity Type", + project=None, + task=None, + company=None, +): update_activity_type(activity_type) timesheet = frappe.new_doc("Timesheet") timesheet.employee = employee - timesheet.company = company or '_Test Company' - timesheet_detail = timesheet.append('time_logs', {}) + timesheet.company = company or "_Test Company" + timesheet_detail = timesheet.append("time_logs", {}) timesheet_detail.is_billable = is_billable timesheet_detail.activity_type = activity_type timesheet_detail.from_time = now_datetime() timesheet_detail.hours = 2 - timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(hours= timesheet_detail.hours) + timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta( + hours=timesheet_detail.hours + ) timesheet_detail.project = project timesheet_detail.task = task - for data in timesheet.get('time_logs'): + for data in timesheet.get("time_logs"): if simulate: while True: try: @@ -247,7 +261,7 @@ def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Te break except OverlapError: data.from_time = data.from_time + datetime.timedelta(minutes=10) - data.to_time = data.from_time + datetime.timedelta(hours= data.hours) + data.to_time = data.from_time + datetime.timedelta(hours=data.hours) else: timesheet.save(ignore_permissions=True) @@ -255,7 +269,8 @@ def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Te return timesheet + def update_activity_type(activity_type): - activity_type = frappe.get_doc('Activity Type',activity_type) + activity_type = frappe.get_doc("Activity Type", activity_type) activity_type.billing_rate = 50.0 activity_type.save(ignore_permissions=True) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index b44d5017431..2ef966b3192 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -14,8 +14,13 @@ from erpnext.hr.utils import validate_active_employee from erpnext.setup.utils import get_exchange_rate -class OverlapError(frappe.ValidationError): pass -class OverWorkLoggedError(frappe.ValidationError): pass +class OverlapError(frappe.ValidationError): + pass + + +class OverWorkLoggedError(frappe.ValidationError): + pass + class Timesheet(Document): def validate(self): @@ -32,7 +37,7 @@ class Timesheet(Document): def set_employee_name(self): if self.employee and not self.employee_name: - self.employee_name = frappe.db.get_value('Employee', self.employee, 'employee_name') + self.employee_name = frappe.db.get_value("Employee", self.employee, "employee_name") def calculate_total_amounts(self): self.total_hours = 0.0 @@ -70,11 +75,7 @@ class Timesheet(Document): args.billing_hours = 0 def set_status(self): - self.status = { - "0": "Draft", - "1": "Submitted", - "2": "Cancelled" - }[str(self.docstatus or 0)] + self.status = {"0": "Draft", "1": "Submitted", "2": "Cancelled"}[str(self.docstatus or 0)] if self.per_billed == 100: self.status = "Billed" @@ -135,7 +136,7 @@ class Timesheet(Document): frappe.throw(_("To date cannot be before from date")) def validate_time_logs(self): - for data in self.get('time_logs'): + for data in self.get("time_logs"): self.set_to_time(data) self.validate_overlap(data) self.set_project(data) @@ -150,7 +151,7 @@ class Timesheet(Document): data.to_time = _to_time def validate_overlap(self, data): - settings = frappe.get_single('Projects Settings') + settings = frappe.get_single("Projects Settings") self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap) self.validate_overlap_for("employee", data, self.employee, settings.ignore_employee_time_overlap) @@ -159,7 +160,11 @@ class Timesheet(Document): def validate_project(self, data): if self.parent_project and self.parent_project != data.project: - frappe.throw(_("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format(data.idx, self.parent_project)) + frappe.throw( + _("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format( + data.idx, self.parent_project + ) + ) def validate_overlap_for(self, fieldname, args, value, ignore_validation=False): if not value or ignore_validation: @@ -167,8 +172,12 @@ class Timesheet(Document): existing = self.get_overlap_for(fieldname, args, value) if existing: - frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") - .format(args.idx, self.name, existing.name), OverlapError) + frappe.throw( + _("Row {0}: From Time and To Time of {1} is overlapping with {2}").format( + args.idx, self.name, existing.name + ), + OverlapError, + ) def get_overlap_for(self, fieldname, args, value): timesheet = frappe.qb.DocType("Timesheet") @@ -179,20 +188,22 @@ class Timesheet(Document): existing = ( frappe.qb.from_(timesheet) - .join(timelog) - .on(timelog.parent == timesheet.name) - .select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time')) - .where( - (timelog.name != (args.name or "No Name")) - & (timesheet.name != (args.parent or "No Name")) - & (timesheet.docstatus < 2) - & (timesheet[fieldname] == value) - & ( - ((from_time > timelog.from_time) & (from_time < timelog.to_time)) - | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) - | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) - ) + .join(timelog) + .on(timelog.parent == timesheet.name) + .select( + timesheet.name.as_("name"), timelog.from_time.as_("from_time"), timelog.to_time.as_("to_time") + ) + .where( + (timelog.name != (args.name or "No Name")) + & (timesheet.name != (args.parent or "No Name")) + & (timesheet.docstatus < 2) + & (timesheet[fieldname] == value) + & ( + ((from_time > timelog.from_time) & (from_time < timelog.to_time)) + | ((to_time > timelog.from_time) & (to_time < timelog.to_time)) + | ((from_time <= timelog.from_time) & (to_time >= timelog.to_time)) ) + ) ).run(as_dict=True) if self.check_internal_overlap(fieldname, args): @@ -202,8 +213,7 @@ class Timesheet(Document): def check_internal_overlap(self, fieldname, args): for time_log in self.time_logs: - if not (time_log.from_time and time_log.to_time - and args.from_time and args.to_time): + if not (time_log.from_time and time_log.to_time and args.from_time and args.to_time): continue from_time = get_datetime(time_log.from_time) @@ -211,10 +221,14 @@ class Timesheet(Document): args_from_time = get_datetime(args.from_time) args_to_time = get_datetime(args.to_time) - if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and ( - (args_from_time > from_time and args_from_time < to_time) - or (args_to_time > from_time and args_to_time < to_time) - or (args_from_time <= from_time and args_to_time >= to_time) + if ( + (args.get(fieldname) == time_log.get(fieldname)) + and (args.idx != time_log.idx) + and ( + (args_from_time > from_time and args_from_time < to_time) + or (args_to_time > from_time and args_to_time < to_time) + or (args_from_time <= from_time and args_to_time >= to_time) + ) ): return True return False @@ -226,8 +240,12 @@ class Timesheet(Document): hours = data.billing_hours or 0 costing_hours = data.billing_hours or data.hours or 0 if rate: - data.billing_rate = flt(rate.get('billing_rate')) if flt(data.billing_rate) == 0 else data.billing_rate - data.costing_rate = flt(rate.get('costing_rate')) if flt(data.costing_rate) == 0 else data.costing_rate + data.billing_rate = ( + flt(rate.get("billing_rate")) if flt(data.billing_rate) == 0 else data.billing_rate + ) + data.costing_rate = ( + flt(rate.get("costing_rate")) if flt(data.costing_rate) == 0 else data.costing_rate + ) data.billing_amount = data.billing_rate * hours data.costing_amount = data.costing_rate * costing_hours @@ -235,6 +253,7 @@ class Timesheet(Document): if not ts_detail.is_billable: ts_detail.billing_rate = 0.0 + @frappe.whitelist() def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None): condition = "" @@ -269,22 +288,22 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to ORDER BY tsd.from_time ASC """ - filters = { - "project": project, - "parent": parent, - "from_time": from_time, - "to_time": to_time - } + filters = {"project": project, "parent": parent, "from_time": from_time, "to_time": to_time} return frappe.db.sql(query, filters, as_dict=1) @frappe.whitelist() def get_timesheet_detail_rate(timelog, currency): - timelog_detail = frappe.db.sql("""SELECT tsd.billing_amount as billing_amount, + timelog_detail = frappe.db.sql( + """SELECT tsd.billing_amount as billing_amount, ts.currency as currency FROM `tabTimesheet Detail` tsd INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent - WHERE tsd.name = '{0}'""".format(timelog), as_dict = 1)[0] + WHERE tsd.name = '{0}'""".format( + timelog + ), + as_dict=1, + )[0] if timelog_detail.currency: exchange_rate = get_exchange_rate(timelog_detail.currency, currency) @@ -292,44 +311,60 @@ def get_timesheet_detail_rate(timelog, currency): return timelog_detail.billing_amount * exchange_rate return timelog_detail.billing_amount + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_timesheet(doctype, txt, searchfield, start, page_len, filters): - if not filters: filters = {} + if not filters: + filters = {} condition = "" if filters.get("project"): condition = "and tsd.project = %(project)s" - return frappe.db.sql("""select distinct tsd.parent from `tabTimesheet Detail` tsd, + return frappe.db.sql( + """select distinct tsd.parent from `tabTimesheet Detail` tsd, `tabTimesheet` ts where ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and tsd.docstatus = 1 and ts.total_billable_amount > 0 and tsd.parent LIKE %(txt)s {condition} - order by tsd.parent limit %(start)s, %(page_len)s""" - .format(condition=condition), { - 'txt': '%' + txt + '%', - "start": start, "page_len": page_len, 'project': filters.get("project") - }) + order by tsd.parent limit %(start)s, %(page_len)s""".format( + condition=condition + ), + { + "txt": "%" + txt + "%", + "start": start, + "page_len": page_len, + "project": filters.get("project"), + }, + ) + @frappe.whitelist() def get_timesheet_data(name, project): data = None - if project and project!='': + if project and project != "": data = get_projectwise_timesheet_data(project, name) else: - data = frappe.get_all('Timesheet', - fields = ["(total_billable_amount - total_billed_amount) as billing_amt", "total_billable_hours as billing_hours"], filters = {'name': name}) + data = frappe.get_all( + "Timesheet", + fields=[ + "(total_billable_amount - total_billed_amount) as billing_amt", + "total_billable_hours as billing_hours", + ], + filters={"name": name}, + ) return { - 'billing_hours': data[0].billing_hours if data else None, - 'billing_amount': data[0].billing_amt if data else None, - 'timesheet_detail': data[0].name if data and project and project!= '' else None + "billing_hours": data[0].billing_hours if data else None, + "billing_amount": data[0].billing_amt if data else None, + "timesheet_detail": data[0].name if data and project and project != "" else None, } + @frappe.whitelist() def make_sales_invoice(source_name, item_code=None, customer=None, currency=None): target = frappe.new_doc("Sales Invoice") - timesheet = frappe.get_doc('Timesheet', source_name) + timesheet = frappe.get_doc("Timesheet", source_name) if not timesheet.total_billable_hours: frappe.throw(_("Invoice can't be made for zero billing hour")) @@ -349,28 +384,28 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None target.currency = currency if item_code: - target.append('items', { - 'item_code': item_code, - 'qty': hours, - 'rate': billing_rate - }) + target.append("items", {"item_code": item_code, "qty": hours, "rate": billing_rate}) for time_log in timesheet.time_logs: if time_log.is_billable: - target.append('timesheets', { - 'time_sheet': timesheet.name, - 'billing_hours': time_log.billing_hours, - 'billing_amount': time_log.billing_amount, - 'timesheet_detail': time_log.name, - 'activity_type': time_log.activity_type, - 'description': time_log.description - }) + target.append( + "timesheets", + { + "time_sheet": timesheet.name, + "billing_hours": time_log.billing_hours, + "billing_amount": time_log.billing_amount, + "timesheet_detail": time_log.name, + "activity_type": time_log.activity_type, + "description": time_log.description, + }, + ) target.run_method("calculate_billing_amount_for_timesheet") target.run_method("set_missing_values") return target + @frappe.whitelist() def make_salary_slip(source_name, target_doc=None): target = frappe.new_doc("Salary Slip") @@ -379,8 +414,9 @@ def make_salary_slip(source_name, target_doc=None): return target + def set_missing_values(time_sheet, target): - doc = frappe.get_doc('Timesheet', time_sheet) + doc = frappe.get_doc("Timesheet", time_sheet) target.employee = doc.employee target.employee_name = doc.employee_name target.salary_slip_based_on_timesheet = 1 @@ -388,26 +424,33 @@ def set_missing_values(time_sheet, target): target.end_date = doc.end_date target.posting_date = doc.modified target.total_working_hours = doc.total_hours - target.append('timesheets', { - 'time_sheet': doc.name, - 'working_hours': doc.total_hours - }) + target.append("timesheets", {"time_sheet": doc.name, "working_hours": doc.total_hours}) + @frappe.whitelist() def get_activity_cost(employee=None, activity_type=None, currency=None): - base_currency = frappe.defaults.get_global_default('currency') - rate = frappe.db.get_values("Activity Cost", {"employee": employee, - "activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True) + base_currency = frappe.defaults.get_global_default("currency") + rate = frappe.db.get_values( + "Activity Cost", + {"employee": employee, "activity_type": activity_type}, + ["costing_rate", "billing_rate"], + as_dict=True, + ) if not rate: - rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type}, - ["costing_rate", "billing_rate"], as_dict=True) - if rate and currency and currency!=base_currency: + rate = frappe.db.get_values( + "Activity Type", + {"activity_type": activity_type}, + ["costing_rate", "billing_rate"], + as_dict=True, + ) + if rate and currency and currency != base_currency: exchange_rate = get_exchange_rate(base_currency, currency) rate[0]["costing_rate"] = rate[0]["costing_rate"] * exchange_rate rate[0]["billing_rate"] = rate[0]["billing_rate"] * exchange_rate return rate[0] if rate else {} + @frappe.whitelist() def get_events(start, end, filters=None): """Returns events for Gantt / Calendar view rendering. @@ -417,9 +460,11 @@ def get_events(start, end, filters=None): """ filters = json.loads(filters) from frappe.desk.calendar import get_event_conditions + conditions = get_event_conditions("Timesheet", filters) - return frappe.db.sql("""select `tabTimesheet Detail`.name as name, + return frappe.db.sql( + """select `tabTimesheet Detail`.name as name, `tabTimesheet Detail`.docstatus as status, `tabTimesheet Detail`.parent as parent, from_time as start_date, hours, activity_type, `tabTimesheet Detail`.project, to_time as end_date, @@ -428,29 +473,37 @@ def get_events(start, end, filters=None): where `tabTimesheet Detail`.parent = `tabTimesheet`.name and `tabTimesheet`.docstatus < 2 and (from_time <= %(end)s and to_time >= %(start)s) {conditions} {match_cond} - """.format(conditions=conditions, match_cond = get_match_cond('Timesheet')), - { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + """.format( + conditions=conditions, match_cond=get_match_cond("Timesheet") + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) -def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"): + +def get_timesheets_list( + doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified" +): user = frappe.session.user # find customer name from contact. - customer = '' + customer = "" timesheets = [] - contact = frappe.db.exists('Contact', {'user': user}) + contact = frappe.db.exists("Contact", {"user": user}) if contact: # find customer - contact = frappe.get_doc('Contact', contact) - customer = contact.get_link_for('Customer') + contact = frappe.get_doc("Contact", contact) + customer = contact.get_link_for("Customer") if customer: - sales_invoices = [d.name for d in frappe.get_all('Sales Invoice', filters={'customer': customer})] or [None] - projects = [d.name for d in frappe.get_all('Project', filters={'customer': customer})] + sales_invoices = [ + d.name for d in frappe.get_all("Sales Invoice", filters={"customer": customer}) + ] or [None] + projects = [d.name for d in frappe.get_all("Project", filters={"customer": customer})] # Return timesheet related data to web portal. - timesheets = frappe.db.sql(''' + timesheets = frappe.db.sql( + """ SELECT ts.name, tsd.activity_type, ts.status, ts.total_billable_hours, COALESCE(ts.sales_invoice, tsd.sales_invoice) AS sales_invoice, tsd.project @@ -463,16 +516,22 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20 ) ORDER BY `end_date` ASC LIMIT {0}, {1} - '''.format(limit_start, limit_page_length), dict(sales_invoices=sales_invoices, projects=projects), as_dict=True) #nosec + """.format( + limit_start, limit_page_length + ), + dict(sales_invoices=sales_invoices, projects=projects), + as_dict=True, + ) # nosec return timesheets + def get_list_context(context=None): return { "show_sidebar": True, "show_search": True, - 'no_breadcrumbs': True, + "no_breadcrumbs": True, "title": _("Timesheets"), "get_list": get_timesheets_list, - "row_template": "templates/includes/timesheet/timesheet_row.html" + "row_template": "templates/includes/timesheet/timesheet_row.html", } diff --git a/erpnext/projects/doctype/timesheet/timesheet_dashboard.py b/erpnext/projects/doctype/timesheet/timesheet_dashboard.py index d9a341d4dff..6d6b57bebd5 100644 --- a/erpnext/projects/doctype/timesheet/timesheet_dashboard.py +++ b/erpnext/projects/doctype/timesheet/timesheet_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'time_sheet', - 'transactions': [ - { - 'label': _('References'), - 'items': ['Sales Invoice', 'Salary Slip'] - } - ] + "fieldname": "time_sheet", + "transactions": [{"label": _("References"), "items": ["Sales Invoice", "Salary Slip"]}], } diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index 46479d0a19b..bc8f2afb8c9 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -2,7 +2,6 @@ # For license information, please see license.txt - import frappe from frappe import _ from frappe.utils import flt, time_diff_in_hours @@ -15,52 +14,45 @@ def get_columns(): "fieldtype": "Link", "fieldname": "employee", "options": "Employee", - "width": 300 + "width": 300, }, { "label": _("Employee Name"), "fieldtype": "data", "fieldname": "employee_name", "hidden": 1, - "width": 200 + "width": 200, }, { "label": _("Timesheet"), "fieldtype": "Link", "fieldname": "timesheet", "options": "Timesheet", - "width": 150 - }, - { - "label": _("Working Hours"), - "fieldtype": "Float", - "fieldname": "total_hours", - "width": 150 + "width": 150, }, + {"label": _("Working Hours"), "fieldtype": "Float", "fieldname": "total_hours", "width": 150}, { "label": _("Billable Hours"), "fieldtype": "Float", "fieldname": "total_billable_hours", - "width": 150 + "width": 150, }, - { - "label": _("Billing Amount"), - "fieldtype": "Currency", - "fieldname": "amount", - "width": 150 - } + {"label": _("Billing Amount"), "fieldtype": "Currency", "fieldname": "amount", "width": 150}, ] + def get_data(filters): data = [] - if(filters.from_date > filters.to_date): + if filters.from_date > filters.to_date: frappe.msgprint(_("From Date can not be greater than To Date")) return data timesheets = get_timesheets(filters) filters.from_date = frappe.utils.get_datetime(filters.from_date) - filters.to_date = frappe.utils.add_to_date(frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1) + filters.to_date = frappe.utils.add_to_date( + frappe.utils.get_datetime(filters.to_date), days=1, seconds=-1 + ) timesheet_details = get_timesheet_details(filters, timesheets.keys()) @@ -88,46 +80,58 @@ def get_data(filters): total_amount += billing_duration * flt(row.billing_rate) if total_hours: - data.append({ - "employee": timesheets.get(ts).employee, - "employee_name": timesheets.get(ts).employee_name, - "timesheet": ts, - "total_billable_hours": total_billing_hours, - "total_hours": total_hours, - "amount": total_amount - }) + data.append( + { + "employee": timesheets.get(ts).employee, + "employee_name": timesheets.get(ts).employee_name, + "timesheet": ts, + "total_billable_hours": total_billing_hours, + "total_hours": total_hours, + "amount": total_amount, + } + ) return data + def get_timesheets(filters): record_filters = [ - ["start_date", "<=", filters.to_date], - ["end_date", ">=", filters.from_date], - ["docstatus", "=", 1] - ] + ["start_date", "<=", filters.to_date], + ["end_date", ">=", filters.from_date], + ["docstatus", "=", 1], + ] if "employee" in filters: record_filters.append(["employee", "=", filters.employee]) - timesheets = frappe.get_all("Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"]) + timesheets = frappe.get_all( + "Timesheet", filters=record_filters, fields=["employee", "employee_name", "name"] + ) timesheet_map = frappe._dict() for d in timesheets: timesheet_map.setdefault(d.name, d) return timesheet_map + def get_timesheet_details(filters, timesheet_list): - timesheet_details_filter = { - "parent": ["in", timesheet_list] - } + timesheet_details_filter = {"parent": ["in", timesheet_list]} if "project" in filters: timesheet_details_filter["project"] = filters.project timesheet_details = frappe.get_all( "Timesheet Detail", - filters = timesheet_details_filter, - fields=["from_time", "to_time", "hours", "is_billable", "billing_hours", "billing_rate", "parent"] + filters=timesheet_details_filter, + fields=[ + "from_time", + "to_time", + "hours", + "is_billable", + "billing_hours", + "billing_rate", + "parent", + ], ) timesheet_details_map = frappe._dict() @@ -136,6 +140,7 @@ def get_timesheet_details(filters, timesheet_list): return timesheet_details_map + def get_billable_and_total_duration(activity, start_time, end_time): precision = frappe.get_precision("Timesheet Detail", "hours") activity_duration = time_diff_in_hours(end_time, start_time) diff --git a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py index f73376871aa..b31a063c6af 100644 --- a/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py +++ b/erpnext/projects/report/daily_timesheet_summary/daily_timesheet_summary.py @@ -20,21 +20,37 @@ def execute(filters=None): return columns, data + def get_column(): - return [_("Timesheet") + ":Link/Timesheet:120", _("Employee") + "::150", _("Employee Name") + "::150", - _("From Datetime") + "::140", _("To Datetime") + "::140", _("Hours") + "::70", - _("Activity Type") + "::120", _("Task") + ":Link/Task:150", - _("Project") + ":Link/Project:120", _("Status") + "::70"] + return [ + _("Timesheet") + ":Link/Timesheet:120", + _("Employee") + "::150", + _("Employee Name") + "::150", + _("From Datetime") + "::140", + _("To Datetime") + "::140", + _("Hours") + "::70", + _("Activity Type") + "::120", + _("Task") + ":Link/Task:150", + _("Project") + ":Link/Project:120", + _("Status") + "::70", + ] + def get_data(conditions, filters): - time_sheet = frappe.db.sql(""" select `tabTimesheet`.name, `tabTimesheet`.employee, `tabTimesheet`.employee_name, + time_sheet = frappe.db.sql( + """ select `tabTimesheet`.name, `tabTimesheet`.employee, `tabTimesheet`.employee_name, `tabTimesheet Detail`.from_time, `tabTimesheet Detail`.to_time, `tabTimesheet Detail`.hours, `tabTimesheet Detail`.activity_type, `tabTimesheet Detail`.task, `tabTimesheet Detail`.project, `tabTimesheet`.status from `tabTimesheet Detail`, `tabTimesheet` where - `tabTimesheet Detail`.parent = `tabTimesheet`.name and %s order by `tabTimesheet`.name"""%(conditions), filters, as_list=1) + `tabTimesheet Detail`.parent = `tabTimesheet`.name and %s order by `tabTimesheet`.name""" + % (conditions), + filters, + as_list=1, + ) return time_sheet + def get_conditions(filters): conditions = "`tabTimesheet`.docstatus = 1" if filters.get("from_date"): diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py index 3ab2bb652b0..5c3dc2da118 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py @@ -13,14 +13,24 @@ def execute(filters=None): charts = get_chart_data(data) return columns, data, None, charts + def get_data(filters): conditions = get_conditions(filters) - tasks = frappe.get_all("Task", - filters = conditions, - fields = ["name", "subject", "exp_start_date", "exp_end_date", - "status", "priority", "completed_on", "progress"], - order_by="creation" - ) + tasks = frappe.get_all( + "Task", + filters=conditions, + fields=[ + "name", + "subject", + "exp_start_date", + "exp_end_date", + "status", + "priority", + "completed_on", + "progress", + ], + order_by="creation", + ) for task in tasks: if task.exp_end_date: if task.completed_on: @@ -39,6 +49,7 @@ def get_data(filters): tasks.sort(key=lambda x: x["delay"], reverse=True) return tasks + def get_conditions(filters): conditions = frappe._dict() keys = ["priority", "status"] @@ -51,6 +62,7 @@ def get_conditions(filters): conditions.exp_start_date = ["<=", filters.get("to_date")] return conditions + def get_chart_data(data): delay, on_track = 0, 0 for entry in data: @@ -61,74 +73,29 @@ def get_chart_data(data): charts = { "data": { "labels": ["On Track", "Delayed"], - "datasets": [ - { - "name": "Delayed", - "values": [on_track, delay] - } - ] + "datasets": [{"name": "Delayed", "values": [on_track, delay]}], }, "type": "percentage", - "colors": ["#84D5BA", "#CB4B5F"] + "colors": ["#84D5BA", "#CB4B5F"], } return charts + def get_columns(): columns = [ - { - "fieldname": "name", - "fieldtype": "Link", - "label": "Task", - "options": "Task", - "width": 150 - }, - { - "fieldname": "subject", - "fieldtype": "Data", - "label": "Subject", - "width": 200 - }, - { - "fieldname": "status", - "fieldtype": "Data", - "label": "Status", - "width": 100 - }, - { - "fieldname": "priority", - "fieldtype": "Data", - "label": "Priority", - "width": 80 - }, - { - "fieldname": "progress", - "fieldtype": "Data", - "label": "Progress (%)", - "width": 120 - }, + {"fieldname": "name", "fieldtype": "Link", "label": "Task", "options": "Task", "width": 150}, + {"fieldname": "subject", "fieldtype": "Data", "label": "Subject", "width": 200}, + {"fieldname": "status", "fieldtype": "Data", "label": "Status", "width": 100}, + {"fieldname": "priority", "fieldtype": "Data", "label": "Priority", "width": 80}, + {"fieldname": "progress", "fieldtype": "Data", "label": "Progress (%)", "width": 120}, { "fieldname": "exp_start_date", "fieldtype": "Date", "label": "Expected Start Date", - "width": 150 + "width": 150, }, - { - "fieldname": "exp_end_date", - "fieldtype": "Date", - "label": "Expected End Date", - "width": 150 - }, - { - "fieldname": "completed_on", - "fieldtype": "Date", - "label": "Actual End Date", - "width": 130 - }, - { - "fieldname": "delay", - "fieldtype": "Data", - "label": "Delay (In Days)", - "width": 120 - } + {"fieldname": "exp_end_date", "fieldtype": "Date", "label": "Expected End Date", "width": 150}, + {"fieldname": "completed_on", "fieldtype": "Date", "label": "Actual End Date", "width": 130}, + {"fieldname": "delay", "fieldtype": "Data", "label": "Delay (In Days)", "width": 120}, ] return columns diff --git a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py index 0d97ddf85ae..91a0607b17c 100644 --- a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -19,25 +18,17 @@ class TestDelayedTasksSummary(unittest.TestCase): task1.save() def test_delayed_tasks_summary(self): - filters = frappe._dict({ - "from_date": add_months(nowdate(), -1), - "to_date": nowdate(), - "priority": "Low", - "status": "Open" - }) - expected_data = [ + filters = frappe._dict( { - "subject": "_Test Task 99", + "from_date": add_months(nowdate(), -1), + "to_date": nowdate(), + "priority": "Low", "status": "Open", - "priority": "Low", - "delay": 1 - }, - { - "subject": "_Test Task 98", - "status": "Completed", - "priority": "Low", - "delay": -1 } + ) + expected_data = [ + {"subject": "_Test Task 99", "status": "Open", "priority": "Low", "delay": 1}, + {"subject": "_Test Task 98", "status": "Completed", "priority": "Low", "delay": -1}, ] report = execute(filters) data = list(filter(lambda x: x.subject == "_Test Task 99", report[1]))[0] diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 2854ea31fe0..6e42b0fe834 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -11,8 +11,10 @@ from six import iteritems def execute(filters=None): return EmployeeHoursReport(filters).run() + class EmployeeHoursReport: - '''Employee Hours Utilization Report Based On Timesheet''' + """Employee Hours Utilization Report Based On Timesheet""" + def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -26,13 +28,17 @@ class EmployeeHoursReport: self.day_span = (self.to_date - self.from_date).days if self.day_span <= 0: - frappe.throw(_('From Date must come before To Date')) + frappe.throw(_("From Date must come before To Date")) def validate_standard_working_hours(self): - self.standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours') + self.standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") if not self.standard_working_hours: - msg = _('The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.').format( - frappe.bold('Standard Working Hours'), frappe.utils.get_link_to_form('HR Settings', 'HR Settings')) + msg = _( + "The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}." + ).format( + frappe.bold("Standard Working Hours"), + frappe.utils.get_link_to_form("HR Settings", "HR Settings"), + ) frappe.throw(msg) @@ -47,55 +53,50 @@ class EmployeeHoursReport: def generate_columns(self): self.columns = [ { - 'label': _('Employee'), - 'options': 'Employee', - 'fieldname': 'employee', - 'fieldtype': 'Link', - 'width': 230 + "label": _("Employee"), + "options": "Employee", + "fieldname": "employee", + "fieldtype": "Link", + "width": 230, }, { - 'label': _('Department'), - 'options': 'Department', - 'fieldname': 'department', - 'fieldtype': 'Link', - 'width': 120 + "label": _("Department"), + "options": "Department", + "fieldname": "department", + "fieldtype": "Link", + "width": 120, + }, + {"label": _("Total Hours (T)"), "fieldname": "total_hours", "fieldtype": "Float", "width": 120}, + { + "label": _("Billed Hours (B)"), + "fieldname": "billed_hours", + "fieldtype": "Float", + "width": 170, }, { - 'label': _('Total Hours (T)'), - 'fieldname': 'total_hours', - 'fieldtype': 'Float', - 'width': 120 + "label": _("Non-Billed Hours (NB)"), + "fieldname": "non_billed_hours", + "fieldtype": "Float", + "width": 170, }, { - 'label': _('Billed Hours (B)'), - 'fieldname': 'billed_hours', - 'fieldtype': 'Float', - 'width': 170 + "label": _("Untracked Hours (U)"), + "fieldname": "untracked_hours", + "fieldtype": "Float", + "width": 170, }, { - 'label': _('Non-Billed Hours (NB)'), - 'fieldname': 'non_billed_hours', - 'fieldtype': 'Float', - 'width': 170 + "label": _("% Utilization (B + NB) / T"), + "fieldname": "per_util", + "fieldtype": "Percentage", + "width": 200, }, { - 'label': _('Untracked Hours (U)'), - 'fieldname': 'untracked_hours', - 'fieldtype': 'Float', - 'width': 170 + "label": _("% Utilization (B / T)"), + "fieldname": "per_util_billed_only", + "fieldtype": "Percentage", + "width": 200, }, - { - 'label': _('% Utilization (B + NB) / T'), - 'fieldname': 'per_util', - 'fieldtype': 'Percentage', - 'width': 200 - }, - { - 'label': _('% Utilization (B / T)'), - 'fieldname': 'per_util_billed_only', - 'fieldtype': 'Percentage', - 'width': 200 - } ] def generate_data(self): @@ -112,35 +113,36 @@ class EmployeeHoursReport: for emp, data in iteritems(self.stats_by_employee): row = frappe._dict() - row['employee'] = emp + row["employee"] = emp row.update(data) self.data.append(row) # Sort by descending order of percentage utilization - self.data.sort(key=lambda x: x['per_util'], reverse=True) + self.data.sort(key=lambda x: x["per_util"], reverse=True) def filter_stats_by_department(self): filtered_data = frappe._dict() for emp, data in self.stats_by_employee.items(): - if data['department'] == self.filters.department: + if data["department"] == self.filters.department: filtered_data[emp] = data # Update stats self.stats_by_employee = filtered_data def generate_filtered_time_logs(self): - additional_filters = '' + additional_filters = "" - filter_fields = ['employee', 'project', 'company'] + filter_fields = ["employee", "project", "company"] for field in filter_fields: if self.filters.get(field): - if field == 'project': + if field == "project": additional_filters += f"AND ttd.{field} = '{self.filters.get(field)}'" else: additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'" - self.filtered_time_logs = frappe.db.sql(''' + self.filtered_time_logs = frappe.db.sql( + """ SELECT tt.employee AS employee, ttd.hours AS hours, ttd.is_billable AS is_billable, ttd.project AS project FROM `tabTimesheet Detail` AS ttd JOIN `tabTimesheet` AS tt @@ -149,47 +151,46 @@ class EmployeeHoursReport: AND tt.start_date BETWEEN '{0}' AND '{1}' AND tt.end_date BETWEEN '{0}' AND '{1}' {2} - '''.format(self.filters.from_date, self.filters.to_date, additional_filters)) + """.format( + self.filters.from_date, self.filters.to_date, additional_filters + ) + ) def generate_stats_by_employee(self): self.stats_by_employee = frappe._dict() for emp, hours, is_billable, project in self.filtered_time_logs: - self.stats_by_employee.setdefault( - emp, frappe._dict() - ).setdefault('billed_hours', 0.0) + self.stats_by_employee.setdefault(emp, frappe._dict()).setdefault("billed_hours", 0.0) - self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0) + self.stats_by_employee[emp].setdefault("non_billed_hours", 0.0) if is_billable: - self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2) + self.stats_by_employee[emp]["billed_hours"] += flt(hours, 2) else: - self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2) + self.stats_by_employee[emp]["non_billed_hours"] += flt(hours, 2) def set_employee_department_and_name(self): for emp in self.stats_by_employee: - emp_name = frappe.db.get_value( - 'Employee', emp, 'employee_name' - ) - emp_dept = frappe.db.get_value( - 'Employee', emp, 'department' - ) + emp_name = frappe.db.get_value("Employee", emp, "employee_name") + emp_dept = frappe.db.get_value("Employee", emp, "department") - self.stats_by_employee[emp]['department'] = emp_dept - self.stats_by_employee[emp]['employee_name'] = emp_name + self.stats_by_employee[emp]["department"] = emp_dept + self.stats_by_employee[emp]["employee_name"] = emp_name def calculate_utilizations(self): TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2) for emp, data in iteritems(self.stats_by_employee): - data['total_hours'] = TOTAL_HOURS - data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2) + data["total_hours"] = TOTAL_HOURS + data["untracked_hours"] = flt(TOTAL_HOURS - data["billed_hours"] - data["non_billed_hours"], 2) # To handle overtime edge-case - if data['untracked_hours'] < 0: - data['untracked_hours'] = 0.0 + if data["untracked_hours"] < 0: + data["untracked_hours"] = 0.0 - data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) - data['per_util_billed_only'] = flt((data['billed_hours'] / TOTAL_HOURS) * 100, 2) + data["per_util"] = flt( + ((data["billed_hours"] + data["non_billed_hours"]) / TOTAL_HOURS) * 100, 2 + ) + data["per_util_billed_only"] = flt((data["billed_hours"] / TOTAL_HOURS) * 100, 2) def generate_report_summary(self): self.report_summary = [] @@ -203,11 +204,11 @@ class EmployeeHoursReport: total_untracked = 0.0 for row in self.data: - avg_utilization += row['per_util'] - avg_utilization_billed_only += row['per_util_billed_only'] - total_billed += row['billed_hours'] - total_non_billed += row['non_billed_hours'] - total_untracked += row['untracked_hours'] + avg_utilization += row["per_util"] + avg_utilization_billed_only += row["per_util_billed_only"] + total_billed += row["billed_hours"] + total_non_billed += row["non_billed_hours"] + total_untracked += row["untracked_hours"] avg_utilization /= len(self.data) avg_utilization = flt(avg_utilization, 2) @@ -218,27 +219,19 @@ class EmployeeHoursReport: THRESHOLD_PERCENTAGE = 70.0 self.report_summary = [ { - 'value': f'{avg_utilization}%', - 'indicator': 'Red' if avg_utilization < THRESHOLD_PERCENTAGE else 'Green', - 'label': _('Avg Utilization'), - 'datatype': 'Percentage' + "value": f"{avg_utilization}%", + "indicator": "Red" if avg_utilization < THRESHOLD_PERCENTAGE else "Green", + "label": _("Avg Utilization"), + "datatype": "Percentage", }, { - 'value': f'{avg_utilization_billed_only}%', - 'indicator': 'Red' if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else 'Green', - 'label': _('Avg Utilization (Billed Only)'), - 'datatype': 'Percentage' + "value": f"{avg_utilization_billed_only}%", + "indicator": "Red" if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else "Green", + "label": _("Avg Utilization (Billed Only)"), + "datatype": "Percentage", }, - { - 'value': total_billed, - 'label': _('Total Billed Hours'), - 'datatype': 'Float' - }, - { - 'value': total_non_billed, - 'label': _('Total Non-Billed Hours'), - 'datatype': 'Float' - } + {"value": total_billed, "label": _("Total Billed Hours"), "datatype": "Float"}, + {"value": total_non_billed, "label": _("Total Non-Billed Hours"), "datatype": "Float"}, ] def generate_chart_data(self): @@ -249,33 +242,21 @@ class EmployeeHoursReport: non_billed_hours = [] untracked_hours = [] - for row in self.data: - labels.append(row.get('employee_name')) - billed_hours.append(row.get('billed_hours')) - non_billed_hours.append(row.get('non_billed_hours')) - untracked_hours.append(row.get('untracked_hours')) + labels.append(row.get("employee_name")) + billed_hours.append(row.get("billed_hours")) + non_billed_hours.append(row.get("non_billed_hours")) + untracked_hours.append(row.get("untracked_hours")) self.chart = { - 'data': { - 'labels': labels[:30], - 'datasets': [ - { - 'name': _('Billed Hours'), - 'values': billed_hours[:30] - }, - { - 'name': _('Non-Billed Hours'), - 'values': non_billed_hours[:30] - }, - { - 'name': _('Untracked Hours'), - 'values': untracked_hours[:30] - } - ] + "data": { + "labels": labels[:30], + "datasets": [ + {"name": _("Billed Hours"), "values": billed_hours[:30]}, + {"name": _("Non-Billed Hours"), "values": non_billed_hours[:30]}, + {"name": _("Untracked Hours"), "values": untracked_hours[:30]}, + ], }, - 'type': 'bar', - 'barOptions': { - 'stacked': True - } + "type": "bar", + "barOptions": {"stacked": True}, } diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index 99593822052..4cddc4a3495 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -12,191 +11,189 @@ from erpnext.projects.report.employee_hours_utilization_based_on_timesheet.emplo class TestEmployeeUtilization(unittest.TestCase): - @classmethod - def setUpClass(cls): - # Create test employee - cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company") - cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company") + @classmethod + def setUpClass(cls): + # Create test employee + cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company") + cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company") - # Create test project - cls.test_project = make_project({"project_name": "_Test Project"}) + # Create test project + cls.test_project = make_project({"project_name": "_Test Project"}) - # Create test timesheets - cls.create_test_timesheets() + # Create test timesheets + cls.create_test_timesheets() - frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9) + frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9) - @classmethod - def create_test_timesheets(cls): - timesheet1 = frappe.new_doc("Timesheet") - timesheet1.employee = cls.test_emp1 - timesheet1.company = '_Test Company' + @classmethod + def create_test_timesheets(cls): + timesheet1 = frappe.new_doc("Timesheet") + timesheet1.employee = cls.test_emp1 + timesheet1.company = "_Test Company" - timesheet1.append("time_logs", { - "activity_type": get_random("Activity Type"), - "hours": 5, - "is_billable": 1, - "from_time": '2021-04-01 13:30:00.000000', - "to_time": '2021-04-01 18:30:00.000000' - }) + timesheet1.append( + "time_logs", + { + "activity_type": get_random("Activity Type"), + "hours": 5, + "is_billable": 1, + "from_time": "2021-04-01 13:30:00.000000", + "to_time": "2021-04-01 18:30:00.000000", + }, + ) - timesheet1.save() - timesheet1.submit() + timesheet1.save() + timesheet1.submit() - timesheet2 = frappe.new_doc("Timesheet") - timesheet2.employee = cls.test_emp2 - timesheet2.company = '_Test Company' + timesheet2 = frappe.new_doc("Timesheet") + timesheet2.employee = cls.test_emp2 + timesheet2.company = "_Test Company" - timesheet2.append("time_logs", { - "activity_type": get_random("Activity Type"), - "hours": 10, - "is_billable": 0, - "from_time": '2021-04-01 13:30:00.000000', - "to_time": '2021-04-01 23:30:00.000000', - "project": cls.test_project.name - }) + timesheet2.append( + "time_logs", + { + "activity_type": get_random("Activity Type"), + "hours": 10, + "is_billable": 0, + "from_time": "2021-04-01 13:30:00.000000", + "to_time": "2021-04-01 23:30:00.000000", + "project": cls.test_project.name, + }, + ) - timesheet2.save() - timesheet2.submit() + timesheet2.save() + timesheet2.submit() - @classmethod - def tearDownClass(cls): - # Delete time logs - frappe.db.sql(""" + @classmethod + def tearDownClass(cls): + # Delete time logs + frappe.db.sql( + """ DELETE FROM `tabTimesheet Detail` WHERE parent IN ( SELECT name FROM `tabTimesheet` WHERE company = '_Test Company' ) - """) + """ + ) - frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'") - frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") + frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'") + frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") - def test_utilization_report_with_required_filters_only(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03" - } + def test_utilization_report_with_required_filters_only(self): + filters = {"company": "_Test Company", "from_date": "2021-04-01", "to_date": "2021-04-03"} - report = execute(filters) + report = execute(filters) - expected_data = self.get_expected_data_for_test_employees() - self.assertEqual(report[1], expected_data) + expected_data = self.get_expected_data_for_test_employees() + self.assertEqual(report[1], expected_data) - def test_utilization_report_for_single_employee(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03", - "employee": self.test_emp1 - } + def test_utilization_report_for_single_employee(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "employee": self.test_emp1, + } - report = execute(filters) + report = execute(filters) - emp1_data = frappe.get_doc('Employee', self.test_emp1) - expected_data = [ - { - 'employee': self.test_emp1, - 'employee_name': 'test1@employeeutil.com', - 'billed_hours': 5.0, - 'non_billed_hours': 0.0, - 'department': emp1_data.department, - 'total_hours': 18.0, - 'untracked_hours': 13.0, - 'per_util': 27.78, - 'per_util_billed_only': 27.78 - } - ] + emp1_data = frappe.get_doc("Employee", self.test_emp1) + expected_data = [ + { + "employee": self.test_emp1, + "employee_name": "test1@employeeutil.com", + "billed_hours": 5.0, + "non_billed_hours": 0.0, + "department": emp1_data.department, + "total_hours": 18.0, + "untracked_hours": 13.0, + "per_util": 27.78, + "per_util_billed_only": 27.78, + } + ] - self.assertEqual(report[1], expected_data) + self.assertEqual(report[1], expected_data) - def test_utilization_report_for_project(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03", - "project": self.test_project.name - } + def test_utilization_report_for_project(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "project": self.test_project.name, + } - report = execute(filters) + report = execute(filters) - emp2_data = frappe.get_doc('Employee', self.test_emp2) - expected_data = [ - { - 'employee': self.test_emp2, - 'employee_name': 'test2@employeeutil.com', - 'billed_hours': 0.0, - 'non_billed_hours': 10.0, - 'department': emp2_data.department, - 'total_hours': 18.0, - 'untracked_hours': 8.0, - 'per_util': 55.56, - 'per_util_billed_only': 0.0 - } - ] + emp2_data = frappe.get_doc("Employee", self.test_emp2) + expected_data = [ + { + "employee": self.test_emp2, + "employee_name": "test2@employeeutil.com", + "billed_hours": 0.0, + "non_billed_hours": 10.0, + "department": emp2_data.department, + "total_hours": 18.0, + "untracked_hours": 8.0, + "per_util": 55.56, + "per_util_billed_only": 0.0, + } + ] - self.assertEqual(report[1], expected_data) + self.assertEqual(report[1], expected_data) - def test_utilization_report_for_department(self): - emp1_data = frappe.get_doc('Employee', self.test_emp1) - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03", - "department": emp1_data.department - } + def test_utilization_report_for_department(self): + emp1_data = frappe.get_doc("Employee", self.test_emp1) + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "department": emp1_data.department, + } - report = execute(filters) + report = execute(filters) - expected_data = self.get_expected_data_for_test_employees() - self.assertEqual(report[1], expected_data) + expected_data = self.get_expected_data_for_test_employees() + self.assertEqual(report[1], expected_data) - def test_report_summary_data(self): - filters = { - "company": "_Test Company", - "from_date": "2021-04-01", - "to_date": "2021-04-03" - } + def test_report_summary_data(self): + filters = {"company": "_Test Company", "from_date": "2021-04-01", "to_date": "2021-04-03"} - report = execute(filters) - summary = report[4] - expected_summary_values = ['41.67%', '13.89%', 5.0, 10.0] + report = execute(filters) + summary = report[4] + expected_summary_values = ["41.67%", "13.89%", 5.0, 10.0] - self.assertEqual(len(summary), 4) + self.assertEqual(len(summary), 4) - for i in range(4): - self.assertEqual( - summary[i]['value'], expected_summary_values[i] - ) + for i in range(4): + self.assertEqual(summary[i]["value"], expected_summary_values[i]) - def get_expected_data_for_test_employees(self): - emp1_data = frappe.get_doc('Employee', self.test_emp1) - emp2_data = frappe.get_doc('Employee', self.test_emp2) + def get_expected_data_for_test_employees(self): + emp1_data = frappe.get_doc("Employee", self.test_emp1) + emp2_data = frappe.get_doc("Employee", self.test_emp2) - return [ - { - 'employee': self.test_emp2, - 'employee_name': 'test2@employeeutil.com', - 'billed_hours': 0.0, - 'non_billed_hours': 10.0, - 'department': emp2_data.department, - 'total_hours': 18.0, - 'untracked_hours': 8.0, - 'per_util': 55.56, - 'per_util_billed_only': 0.0 - }, - { - 'employee': self.test_emp1, - 'employee_name': 'test1@employeeutil.com', - 'billed_hours': 5.0, - 'non_billed_hours': 0.0, - 'department': emp1_data.department, - 'total_hours': 18.0, - 'untracked_hours': 13.0, - 'per_util': 27.78, - 'per_util_billed_only': 27.78 - } - ] + return [ + { + "employee": self.test_emp2, + "employee_name": "test2@employeeutil.com", + "billed_hours": 0.0, + "non_billed_hours": 10.0, + "department": emp2_data.department, + "total_hours": 18.0, + "untracked_hours": 8.0, + "per_util": 55.56, + "per_util_billed_only": 0.0, + }, + { + "employee": self.test_emp1, + "employee_name": "test1@employeeutil.com", + "billed_hours": 5.0, + "non_billed_hours": 0.0, + "department": emp1_data.department, + "total_hours": 18.0, + "untracked_hours": 13.0, + "per_util": 27.78, + "per_util_billed_only": 27.78, + }, + ] diff --git a/erpnext/projects/report/project_profitability/project_profitability.py b/erpnext/projects/report/project_profitability/project_profitability.py index 9520cd17be2..64795f7926d 100644 --- a/erpnext/projects/report/project_profitability/project_profitability.py +++ b/erpnext/projects/report/project_profitability/project_profitability.py @@ -14,17 +14,23 @@ def execute(filters=None): charts = get_chart_data(data) return columns, data, None, charts + def get_data(filters): data = get_rows(filters) data = calculate_cost_and_profit(data) return data + def get_rows(filters): conditions = get_conditions(filters) standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") if not standard_working_hours: - msg = _("The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.").format( - frappe.bold("Standard Working Hours"), frappe.utils.get_link_to_form("HR Settings", "HR Settings")) + msg = _( + "The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}." + ).format( + frappe.bold("Standard Working Hours"), + frappe.utils.get_link_to_form("HR Settings", "HR Settings"), + ) frappe.msgprint(msg) return [] @@ -45,12 +51,17 @@ def get_rows(filters): `tabSalary Slip Timesheet` as sst join `tabTimesheet` on tabTimesheet.name = sst.time_sheet join `tabSales Invoice Timesheet` as sit on sit.time_sheet = tabTimesheet.name join `tabSales Invoice` as si on si.name = sit.parent and si.status != "Cancelled" - join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format(standard_working_hours) + join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format( + standard_working_hours + ) if conditions: sql += """ WHERE - {0}) as t""".format(conditions) - return frappe.db.sql(sql,filters, as_dict=True) + {0}) as t""".format( + conditions + ) + return frappe.db.sql(sql, filters, as_dict=True) + def calculate_cost_and_profit(data): for row in data: @@ -58,6 +69,7 @@ def calculate_cost_and_profit(data): row.profit = flt(row.base_grand_total) - flt(row.base_gross_pay) * flt(row.utilization) return data + def get_conditions(filters): conditions = [] @@ -77,11 +89,14 @@ def get_conditions(filters): conditions.append("tabTimesheet.employee={0}".format(frappe.db.escape(filters.get("employee")))) if filters.get("project"): - conditions.append("tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project")))) + conditions.append( + "tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project"))) + ) conditions = " and ".join(conditions) return conditions + def get_chart_data(data): if not data: return None @@ -94,20 +109,13 @@ def get_chart_data(data): utilization.append(entry.get("utilization")) charts = { - "data": { - "labels": labels, - "datasets": [ - { - "name": "Utilization", - "values": utilization - } - ] - }, + "data": {"labels": labels, "datasets": [{"name": "Utilization", "values": utilization}]}, "type": "bar", - "colors": ["#84BDD5"] + "colors": ["#84BDD5"], } return charts + def get_columns(): return [ { @@ -115,98 +123,78 @@ def get_columns(): "label": _("Customer"), "fieldtype": "Link", "options": "Customer", - "width": 150 + "width": 150, }, { "fieldname": "employee", "label": _("Employee"), "fieldtype": "Link", "options": "Employee", - "width": 130 - }, - { - "fieldname": "employee_name", - "label": _("Employee Name"), - "fieldtype": "Data", - "width": 120 + "width": 130, }, + {"fieldname": "employee_name", "label": _("Employee Name"), "fieldtype": "Data", "width": 120}, { "fieldname": "voucher_no", "label": _("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 + "width": 120, }, { "fieldname": "timesheet", "label": _("Timesheet"), "fieldtype": "Link", "options": "Timesheet", - "width": 120 + "width": 120, }, { "fieldname": "project", "label": _("Project"), "fieldtype": "Link", "options": "Project", - "width": 100 + "width": 100, }, { "fieldname": "base_grand_total", "label": _("Bill Amount"), "fieldtype": "Currency", "options": "currency", - "width": 100 + "width": 100, }, { "fieldname": "base_gross_pay", "label": _("Cost"), "fieldtype": "Currency", "options": "currency", - "width": 100 + "width": 100, }, { "fieldname": "profit", "label": _("Profit"), "fieldtype": "Currency", "options": "currency", - "width": 100 - }, - { - "fieldname": "utilization", - "label": _("Utilization"), - "fieldtype": "Percentage", - "width": 100 + "width": 100, }, + {"fieldname": "utilization", "label": _("Utilization"), "fieldtype": "Percentage", "width": 100}, { "fieldname": "fractional_cost", "label": _("Fractional Cost"), "fieldtype": "Int", - "width": 120 + "width": 120, }, { "fieldname": "total_billed_hours", "label": _("Total Billed Hours"), "fieldtype": "Int", - "width": 150 - }, - { - "fieldname": "start_date", - "label": _("Start Date"), - "fieldtype": "Date", - "width": 100 - }, - { - "fieldname": "end_date", - "label": _("End Date"), - "fieldtype": "Date", - "width": 100 + "width": 150, }, + {"fieldname": "start_date", "label": _("Start Date"), "fieldtype": "Date", "width": 100}, + {"fieldname": "end_date", "label": _("End Date"), "fieldtype": "Date", "width": 100}, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 80 - } + "width": 80, + }, ] diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index c80f301962c..f544739b4e5 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -16,13 +15,15 @@ from erpnext.projects.report.project_profitability.project_profitability import class TestProjectProfitability(FrappeTestCase): def setUp(self): - frappe.db.sql('delete from `tabTimesheet`') - emp = make_employee('test_employee_9@salary.com', company='_Test Company') + frappe.db.sql("delete from `tabTimesheet`") + emp = make_employee("test_employee_9@salary.com", company="_Test Company") - if not frappe.db.exists('Salary Component', 'Timesheet Component'): - frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert() + if not frappe.db.exists("Salary Component", "Timesheet Component"): + frappe.get_doc( + {"doctype": "Salary Component", "salary_component": "Timesheet Component"} + ).insert() - make_salary_structure_for_timesheet(emp, company='_Test Company') + make_salary_structure_for_timesheet(emp, company="_Test Company") date = getdate() self.timesheet = make_timesheet(emp, is_billable=1) @@ -31,21 +32,21 @@ class TestProjectProfitability(FrappeTestCase): holidays = self.salary_slip.get_holidays_for_employee(date, date) if holidays: - frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1) self.salary_slip.submit() - self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') + self.sales_invoice = make_sales_invoice(self.timesheet.name, "_Test Item", "_Test Customer") self.sales_invoice.due_date = date self.sales_invoice.submit() - frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) - frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 0) + frappe.db.set_value("HR Settings", None, "standard_working_hours", 8) + frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) def test_project_profitability(self): filters = { - 'company': '_Test Company', - 'start_date': add_days(self.timesheet.start_date, -3), - 'end_date': self.timesheet.start_date + "company": "_Test Company", + "start_date": add_days(self.timesheet.start_date, -3), + "end_date": self.timesheet.start_date, } report = execute(filters) @@ -61,7 +62,9 @@ class TestProjectProfitability(FrappeTestCase): self.assertEqual(self.salary_slip.total_working_days, row.total_working_days) standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") - utilization = timesheet.total_billed_hours/(self.salary_slip.total_working_days * standard_working_hours) + utilization = timesheet.total_billed_hours / ( + self.salary_slip.total_working_days * standard_working_hours + ) self.assertEqual(utilization, row.utilization) profit = self.sales_invoice.base_grand_total - self.salary_slip.base_gross_pay * utilization diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py index ce1b70160c8..606c0c2d81d 100644 --- a/erpnext/projects/report/project_summary/project_summary.py +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -10,18 +10,35 @@ def execute(filters=None): columns = get_columns() data = [] - data = frappe.db.get_all("Project", filters=filters, fields=["name", 'status', "percent_complete", "expected_start_date", "expected_end_date", "project_type"], order_by="expected_end_date") + data = frappe.db.get_all( + "Project", + filters=filters, + fields=[ + "name", + "status", + "percent_complete", + "expected_start_date", + "expected_end_date", + "project_type", + ], + order_by="expected_end_date", + ) for project in data: project["total_tasks"] = frappe.db.count("Task", filters={"project": project.name}) - project["completed_tasks"] = frappe.db.count("Task", filters={"project": project.name, "status": "Completed"}) - project["overdue_tasks"] = frappe.db.count("Task", filters={"project": project.name, "status": "Overdue"}) + project["completed_tasks"] = frappe.db.count( + "Task", filters={"project": project.name, "status": "Completed"} + ) + project["overdue_tasks"] = frappe.db.count( + "Task", filters={"project": project.name, "status": "Overdue"} + ) chart = get_chart_data(data) report_summary = get_report_summary(data) return columns, data, None, chart, report_summary + def get_columns(): return [ { @@ -29,59 +46,35 @@ def get_columns(): "label": _("Project"), "fieldtype": "Link", "options": "Project", - "width": 200 + "width": 200, }, { "fieldname": "project_type", "label": _("Type"), "fieldtype": "Link", "options": "Project Type", - "width": 120 - }, - { - "fieldname": "status", - "label": _("Status"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "total_tasks", - "label": _("Total Tasks"), - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "status", "label": _("Status"), "fieldtype": "Data", "width": 120}, + {"fieldname": "total_tasks", "label": _("Total Tasks"), "fieldtype": "Data", "width": 120}, { "fieldname": "completed_tasks", "label": _("Tasks Completed"), "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "overdue_tasks", - "label": _("Tasks Overdue"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "percent_complete", - "label": _("Completion"), - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "overdue_tasks", "label": _("Tasks Overdue"), "fieldtype": "Data", "width": 120}, + {"fieldname": "percent_complete", "label": _("Completion"), "fieldtype": "Data", "width": 120}, { "fieldname": "expected_start_date", "label": _("Start Date"), "fieldtype": "Date", - "width": 120 - }, - { - "fieldname": "expected_end_date", - "label": _("End Date"), - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "expected_end_date", "label": _("End Date"), "fieldtype": "Date", "width": 120}, ] + def get_chart_data(data): labels = [] total = [] @@ -96,29 +89,19 @@ def get_chart_data(data): return { "data": { - 'labels': labels[:30], - 'datasets': [ - { - "name": "Overdue", - "values": overdue[:30] - }, - { - "name": "Completed", - "values": completed[:30] - }, - { - "name": "Total Tasks", - "values": total[:30] - }, - ] + "labels": labels[:30], + "datasets": [ + {"name": "Overdue", "values": overdue[:30]}, + {"name": "Completed", "values": completed[:30]}, + {"name": "Total Tasks", "values": total[:30]}, + ], }, "type": "bar", "colors": ["#fc4f51", "#78d6ff", "#7575ff"], - "barOptions": { - "stacked": True - } + "barOptions": {"stacked": True}, } + def get_report_summary(data): if not data: return None @@ -152,5 +135,5 @@ def get_report_summary(data): "indicator": "Green" if total_overdue == 0 else "Red", "label": _("Overdue Tasks"), "datatype": "Int", - } + }, ] diff --git a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py index 31bcc3b2ca3..da609ca769d 100644 --- a/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py +++ b/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py @@ -14,29 +14,56 @@ def execute(filters=None): data = [] for project in proj_details: - data.append([project.name, pr_item_map.get(project.name, 0), - se_item_map.get(project.name, 0), dn_item_map.get(project.name, 0), - project.project_name, project.status, project.company, - project.customer, project.estimated_costing, project.expected_start_date, - project.expected_end_date]) + data.append( + [ + project.name, + pr_item_map.get(project.name, 0), + se_item_map.get(project.name, 0), + dn_item_map.get(project.name, 0), + project.project_name, + project.status, + project.company, + project.customer, + project.estimated_costing, + project.expected_start_date, + project.expected_end_date, + ] + ) return columns, data + def get_columns(): - return [_("Project Id") + ":Link/Project:140", _("Cost of Purchased Items") + ":Currency:160", - _("Cost of Issued Items") + ":Currency:160", _("Cost of Delivered Items") + ":Currency:160", - _("Project Name") + "::120", _("Project Status") + "::120", _("Company") + ":Link/Company:100", - _("Customer") + ":Link/Customer:140", _("Project Value") + ":Currency:120", - _("Project Start Date") + ":Date:120", _("Completion Date") + ":Date:120"] + return [ + _("Project Id") + ":Link/Project:140", + _("Cost of Purchased Items") + ":Currency:160", + _("Cost of Issued Items") + ":Currency:160", + _("Cost of Delivered Items") + ":Currency:160", + _("Project Name") + "::120", + _("Project Status") + "::120", + _("Company") + ":Link/Company:100", + _("Customer") + ":Link/Customer:140", + _("Project Value") + ":Currency:120", + _("Project Start Date") + ":Date:120", + _("Completion Date") + ":Date:120", + ] + def get_project_details(): - return frappe.db.sql(""" select name, project_name, status, company, customer, estimated_costing, - expected_start_date, expected_end_date from tabProject where docstatus < 2""", as_dict=1) + return frappe.db.sql( + """ select name, project_name, status, company, customer, estimated_costing, + expected_start_date, expected_end_date from tabProject where docstatus < 2""", + as_dict=1, + ) + def get_purchased_items_cost(): - pr_items = frappe.db.sql("""select project, sum(base_net_amount) as amount + pr_items = frappe.db.sql( + """select project, sum(base_net_amount) as amount from `tabPurchase Receipt Item` where ifnull(project, '') != '' - and docstatus = 1 group by project""", as_dict=1) + and docstatus = 1 group by project""", + as_dict=1, + ) pr_item_map = {} for item in pr_items: @@ -44,11 +71,15 @@ def get_purchased_items_cost(): return pr_item_map + def get_issued_items_cost(): - se_items = frappe.db.sql("""select se.project, sum(se_item.amount) as amount + se_items = frappe.db.sql( + """select se.project, sum(se_item.amount) as amount from `tabStock Entry` se, `tabStock Entry Detail` se_item where se.name = se_item.parent and se.docstatus = 1 and ifnull(se_item.t_warehouse, '') = '' - and ifnull(se.project, '') != '' group by se.project""", as_dict=1) + and ifnull(se.project, '') != '' group by se.project""", + as_dict=1, + ) se_item_map = {} for item in se_items: @@ -56,18 +87,24 @@ def get_issued_items_cost(): return se_item_map + def get_delivered_items_cost(): - dn_items = frappe.db.sql("""select dn.project, sum(dn_item.base_net_amount) as amount + dn_items = frappe.db.sql( + """select dn.project, sum(dn_item.base_net_amount) as amount from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where dn.name = dn_item.parent and dn.docstatus = 1 and ifnull(dn.project, '') != '' - group by dn.project""", as_dict=1) + group by dn.project""", + as_dict=1, + ) - si_items = frappe.db.sql("""select si.project, sum(si_item.base_net_amount) as amount + si_items = frappe.db.sql( + """select si.project, sum(si_item.base_net_amount) as amount from `tabSales Invoice` si, `tabSales Invoice Item` si_item where si.name = si_item.parent and si.docstatus = 1 and si.update_stock = 1 and si.is_pos = 1 and ifnull(si.project, '') != '' - group by si.project""", as_dict=1) - + group by si.project""", + as_dict=1, + ) dn_item_map = {} for item in dn_items: diff --git a/erpnext/projects/utils.py b/erpnext/projects/utils.py index 5d7455039af..000ea662756 100644 --- a/erpnext/projects/utils.py +++ b/erpnext/projects/utils.py @@ -17,14 +17,15 @@ def query_task(doctype, txt, searchfield, start, page_len, filters): match_conditions = build_match_conditions("Task") match_conditions = ("and" + match_conditions) if match_conditions else "" - return frappe.db.sql("""select name, subject from `tabTask` + return frappe.db.sql( + """select name, subject from `tabTask` where (`%s` like %s or `subject` like %s) %s order by case when `subject` like %s then 0 else 1 end, case when `%s` like %s then 0 else 1 end, `%s`, subject - limit %s, %s""" % - (searchfield, "%s", "%s", match_conditions, "%s", - searchfield, "%s", searchfield, "%s", "%s"), - (search_string, search_string, order_by_string, order_by_string, start, page_len)) + limit %s, %s""" + % (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"), + (search_string, search_string, order_by_string, order_by_string, start, page_len), + ) diff --git a/erpnext/projects/web_form/tasks/tasks.py b/erpnext/projects/web_form/tasks/tasks.py index 67cad05a482..b42297314a9 100644 --- a/erpnext/projects/web_form/tasks/tasks.py +++ b/erpnext/projects/web_form/tasks/tasks.py @@ -1,12 +1,15 @@ - import frappe def get_context(context): if frappe.form_dict.project: - context.parents = [{'title': frappe.form_dict.project, 'route': '/projects?project='+ frappe.form_dict.project}] + context.parents = [ + {"title": frappe.form_dict.project, "route": "/projects?project=" + frappe.form_dict.project} + ] context.success_url = "/projects?project=" + frappe.form_dict.project - elif context.doc and context.doc.get('project'): - context.parents = [{'title': context.doc.project, 'route': '/projects?project='+ context.doc.project}] + elif context.doc and context.doc.get("project"): + context.parents = [ + {"title": context.doc.project, "route": "/projects?project=" + context.doc.project} + ] context.success_url = "/projects?project=" + context.doc.project diff --git a/erpnext/quality_management/doctype/quality_action/quality_action.py b/erpnext/quality_management/doctype/quality_action/quality_action.py index 87245f9a3f8..f7cd94dad31 100644 --- a/erpnext/quality_management/doctype/quality_action/quality_action.py +++ b/erpnext/quality_management/doctype/quality_action/quality_action.py @@ -7,4 +7,4 @@ from frappe.model.document import Document class QualityAction(Document): def validate(self): - self.status = 'Open' if any([d.status=='Open' for d in self.resolutions]) else 'Completed' + self.status = "Open" if any([d.status == "Open" for d in self.resolutions]) else "Completed" diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py index ec5d67f4f03..cc8ce26b58f 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py @@ -9,15 +9,12 @@ from frappe.model.document import Document class QualityFeedback(Document): @frappe.whitelist() def set_parameters(self): - if self.template and not getattr(self, 'parameters', []): - for d in frappe.get_doc('Quality Feedback Template', self.template).parameters: - self.append('parameters', dict( - parameter = d.parameter, - rating = 1 - )) + if self.template and not getattr(self, "parameters", []): + for d in frappe.get_doc("Quality Feedback Template", self.template).parameters: + self.append("parameters", dict(parameter=d.parameter, rating=1)) def validate(self): if not self.document_name: - self.document_type ='User' + self.document_type = "User" self.document_name = frappe.session.user self.set_parameters() diff --git a/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py index fe36cc6e5b1..58d06326a72 100644 --- a/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py +++ b/erpnext/quality_management/doctype/quality_feedback/test_quality_feedback.py @@ -8,21 +8,22 @@ import frappe class TestQualityFeedback(unittest.TestCase): def test_quality_feedback(self): - template = frappe.get_doc(dict( - doctype = 'Quality Feedback Template', - template = 'Test Template', - parameters = [ - dict(parameter='Test Parameter 1'), - dict(parameter='Test Parameter 2') - ] - )).insert() + template = frappe.get_doc( + dict( + doctype="Quality Feedback Template", + template="Test Template", + parameters=[dict(parameter="Test Parameter 1"), dict(parameter="Test Parameter 2")], + ) + ).insert() - feedback = frappe.get_doc(dict( - doctype = 'Quality Feedback', - template = template.name, - document_type = 'User', - document_name = frappe.session.user - )).insert() + feedback = frappe.get_doc( + dict( + doctype="Quality Feedback", + template=template.name, + document_type="User", + document_name=frappe.session.user, + ) + ).insert() self.assertEqual(template.parameters[0].parameter, feedback.parameters[0].parameter) diff --git a/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py b/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py index 67fdaca6d9b..40606cdca76 100644 --- a/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py +++ b/erpnext/quality_management/doctype/quality_goal/test_quality_goal.py @@ -13,12 +13,13 @@ class TestQualityGoal(unittest.TestCase): self.assertTrue(goal) goal.delete() + def get_quality_goal(): - return frappe.get_doc(dict( - doctype = 'Quality Goal', - goal = 'Test Quality Module', - frequency = 'Daily', - objectives = [ - dict(objective = 'Check test cases', target='100', uom='Percent') - ] - )).insert() + return frappe.get_doc( + dict( + doctype="Quality Goal", + goal="Test Quality Module", + frequency="Daily", + objectives=[dict(objective="Check test cases", target="100", uom="Percent")], + ) + ).insert() diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index 0f535ba2e11..72f9e6d6e44 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -8,7 +8,7 @@ from frappe.utils.nestedset import NestedSet class QualityProcedure(NestedSet): - nsm_parent_field = 'parent_quality_procedure' + nsm_parent_field = "parent_quality_procedure" def before_save(self): self.check_for_incorrect_child() @@ -29,14 +29,19 @@ class QualityProcedure(NestedSet): def on_trash(self): # clear from child table (sub procedures) - frappe.db.sql('''update `tabQuality Procedure Process` - set `procedure`='' where `procedure`=%s''', self.name) + frappe.db.sql( + """update `tabQuality Procedure Process` + set `procedure`='' where `procedure`=%s""", + self.name, + ) NestedSet.on_trash(self, allow_root_deletion=True) def set_parent(self): for process in self.processes: # Set parent for only those children who don't have a parent - has_parent = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") + has_parent = frappe.db.get_value( + "Quality Procedure", process.procedure, "parent_quality_procedure" + ) if not has_parent and process.procedure: frappe.db.set_value(self.doctype, process.procedure, "parent_quality_procedure", self.name) @@ -45,10 +50,17 @@ class QualityProcedure(NestedSet): if process.procedure: self.is_group = 1 # Check if any child process belongs to another parent. - parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") + parent_quality_procedure = frappe.db.get_value( + "Quality Procedure", process.procedure, "parent_quality_procedure" + ) if parent_quality_procedure and parent_quality_procedure != self.name: - frappe.throw(_("{0} already has a Parent Procedure {1}.").format(frappe.bold(process.procedure), frappe.bold(parent_quality_procedure)), - title=_("Invalid Child Procedure")) + frappe.throw( + _("{0} already has a Parent Procedure {1}.").format( + frappe.bold(process.procedure), frappe.bold(parent_quality_procedure) + ), + title=_("Invalid Child Procedure"), + ) + @frappe.whitelist() def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=False): @@ -56,16 +68,23 @@ def get_children(doctype, parent=None, parent_quality_procedure=None, is_root=Fa parent = "" if parent: - parent_procedure = frappe.get_doc('Quality Procedure', parent) + parent_procedure = frappe.get_doc("Quality Procedure", parent) # return the list in order - return [dict( - value=d.procedure, - expandable=frappe.db.get_value('Quality Procedure', d.procedure, 'is_group')) - for d in parent_procedure.processes if d.procedure - ] + return [ + dict( + value=d.procedure, expandable=frappe.db.get_value("Quality Procedure", d.procedure, "is_group") + ) + for d in parent_procedure.processes + if d.procedure + ] else: - return frappe.get_all(doctype, fields=['name as value', 'is_group as expandable'], - filters = dict(parent_quality_procedure = parent), order_by='name asc') + return frappe.get_all( + doctype, + fields=["name as value", "is_group as expandable"], + filters=dict(parent_quality_procedure=parent), + order_by="name asc", + ) + @frappe.whitelist() def add_node(): @@ -74,7 +93,7 @@ def add_node(): args = frappe.form_dict args = make_tree_args(**args) - if args.parent_quality_procedure == 'All Quality Procedures': + if args.parent_quality_procedure == "All Quality Procedures": args.parent_quality_procedure = None return frappe.get_doc(args).insert() diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py index 6130895e38d..daf7a694a35 100644 --- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py @@ -11,16 +11,21 @@ from .quality_procedure import add_node class TestQualityProcedure(unittest.TestCase): def test_add_node(self): try: - procedure = frappe.get_doc(dict( - doctype = 'Quality Procedure', - quality_procedure_name = 'Test Procedure 1', - processes = [ - dict(process_description = 'Test Step 1') - ] - )).insert() + procedure = frappe.get_doc( + dict( + doctype="Quality Procedure", + quality_procedure_name="Test Procedure 1", + processes=[dict(process_description="Test Step 1")], + ) + ).insert() - frappe.form_dict = dict(doctype = 'Quality Procedure', quality_procedure_name = 'Test Child 1', - parent_quality_procedure = procedure.name, cmd='test', is_root='false') + frappe.form_dict = dict( + doctype="Quality Procedure", + quality_procedure_name="Test Child 1", + parent_quality_procedure=procedure.name, + cmd="test", + is_root="false", + ) node = add_node() procedure.reload() @@ -39,12 +44,13 @@ class TestQualityProcedure(unittest.TestCase): finally: procedure.delete() + def create_procedure(): - return frappe.get_doc(dict( - doctype = 'Quality Procedure', - quality_procedure_name = 'Test Procedure 1', - is_group = 1, - processes = [ - dict(process_description = 'Test Step 1') - ] - )).insert() + return frappe.get_doc( + dict( + doctype="Quality Procedure", + quality_procedure_name="Test Procedure 1", + is_group=1, + processes=[dict(process_description="Test Step 1")], + ) + ).insert() diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.py b/erpnext/quality_management/doctype/quality_review/quality_review.py index b896f8dfe0c..f691005566d 100644 --- a/erpnext/quality_management/doctype/quality_review/quality_review.py +++ b/erpnext/quality_management/doctype/quality_review/quality_review.py @@ -10,55 +10,52 @@ class QualityReview(Document): def validate(self): # fetch targets from goal if not self.reviews: - for d in frappe.get_doc('Quality Goal', self.goal).objectives: - self.append('reviews', dict( - objective = d.objective, - target = d.target, - uom = d.uom - )) + for d in frappe.get_doc("Quality Goal", self.goal).objectives: + self.append("reviews", dict(objective=d.objective, target=d.target, uom=d.uom)) self.set_status() def set_status(self): # if any child item is failed, fail the parent - if not len(self.reviews or []) or any([d.status=='Open' for d in self.reviews]): - self.status = 'Open' - elif any([d.status=='Failed' for d in self.reviews]): - self.status = 'Failed' + if not len(self.reviews or []) or any([d.status == "Open" for d in self.reviews]): + self.status = "Open" + elif any([d.status == "Failed" for d in self.reviews]): + self.status = "Failed" else: - self.status = 'Passed' + self.status = "Passed" + def review(): day = frappe.utils.getdate().day weekday = frappe.utils.getdate().strftime("%A") month = frappe.utils.getdate().strftime("%B") - for goal in frappe.get_list("Quality Goal", fields=['name', 'frequency', 'date', 'weekday']): - if goal.frequency == 'Daily': + for goal in frappe.get_list("Quality Goal", fields=["name", "frequency", "date", "weekday"]): + if goal.frequency == "Daily": create_review(goal.name) - elif goal.frequency == 'Weekly' and goal.weekday == weekday: + elif goal.frequency == "Weekly" and goal.weekday == weekday: create_review(goal.name) - elif goal.frequency == 'Monthly' and goal.date == str(day): + elif goal.frequency == "Monthly" and goal.date == str(day): create_review(goal.name) - elif goal.frequency == 'Quarterly' and day==1 and get_quarter(month): + elif goal.frequency == "Quarterly" and day == 1 and get_quarter(month): create_review(goal.name) + def create_review(goal): goal = frappe.get_doc("Quality Goal", goal) - review = frappe.get_doc({ - "doctype": "Quality Review", - "goal": goal.name, - "date": frappe.utils.getdate() - }) + review = frappe.get_doc( + {"doctype": "Quality Review", "goal": goal.name, "date": frappe.utils.getdate()} + ) review.insert(ignore_permissions=True) + def get_quarter(month): - if month in ["January", "April", "July", "October"]: + if month in ["January", "April", "July", "October"]: return True else: return False diff --git a/erpnext/quality_management/doctype/quality_review/test_quality_review.py b/erpnext/quality_management/doctype/quality_review/test_quality_review.py index 8a254dba2a5..c76e7f2731a 100644 --- a/erpnext/quality_management/doctype/quality_review/test_quality_review.py +++ b/erpnext/quality_management/doctype/quality_review/test_quality_review.py @@ -15,7 +15,7 @@ class TestQualityReview(unittest.TestCase): review() # check if review exists - quality_review = frappe.get_doc('Quality Review', dict(goal = quality_goal.name)) + quality_review = frappe.get_doc("Quality Review", dict(goal=quality_goal.name)) self.assertEqual(quality_goal.objectives[0].target, quality_review.reviews[0].target) quality_review.delete() diff --git a/erpnext/regional/__init__.py b/erpnext/regional/__init__.py index c460286078d..ec2db811240 100644 --- a/erpnext/regional/__init__.py +++ b/erpnext/regional/__init__.py @@ -13,6 +13,7 @@ def check_deletion_permission(doc, method): if region in ["Nepal", "France"] and doc.docstatus != 0: frappe.throw(_("Deletion is not permitted for country {0}").format(region)) + def create_transaction_log(doc, method): """ Appends the transaction to a chain of hashed logs for legal resons. @@ -24,10 +25,11 @@ def create_transaction_log(doc, method): data = str(doc.as_dict()) - frappe.get_doc({ - "doctype": "Transaction Log", - "reference_doctype": doc.doctype, - "document_name": doc.name, - "data": data - }).insert(ignore_permissions=True) - + frappe.get_doc( + { + "doctype": "Transaction Log", + "reference_doctype": doc.doctype, + "document_name": doc.name, + "data": data, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/regional/address_template/setup.py b/erpnext/regional/address_template/setup.py index 0f9a1b19f53..ff26460d767 100644 --- a/erpnext/regional/address_template/setup.py +++ b/erpnext/regional/address_template/setup.py @@ -9,12 +9,14 @@ def set_up_address_templates(default_country=None): is_default = 1 if country == default_country else 0 update_address_template(country, html, is_default) + def get_address_templates(): """ Return country and path for all HTML files in this directory. Returns a list of dicts. """ + def country(file_name): """Convert 'united_states.html' to 'United States'.""" suffix_pos = file_name.find(".html") @@ -47,9 +49,6 @@ def update_address_template(country, html, is_default=0): frappe.db.set_value("Address Template", country, "template", html) frappe.db.set_value("Address Template", country, "is_default", is_default) else: - frappe.get_doc(dict( - doctype="Address Template", - country=country, - is_default=is_default, - template=html - )).insert() + frappe.get_doc( + dict(doctype="Address Template", country=country, is_default=is_default, template=html) + ).insert() diff --git a/erpnext/regional/address_template/test_regional_address_template.py b/erpnext/regional/address_template/test_regional_address_template.py index 9ad3d470d4a..523653b5846 100644 --- a/erpnext/regional/address_template/test_regional_address_template.py +++ b/erpnext/regional/address_template/test_regional_address_template.py @@ -1,4 +1,3 @@ - from unittest import TestCase import frappe @@ -10,13 +9,11 @@ def ensure_country(country): if frappe.db.exists("Country", country): return frappe.get_doc("Country", country) else: - c = frappe.get_doc({ - "doctype": "Country", - "country_name": country - }) + c = frappe.get_doc({"doctype": "Country", "country_name": country}) c.insert() return c + class TestRegionalAddressTemplate(TestCase): def test_get_address_templates(self): """Get the countries and paths from the templates directory.""" @@ -35,11 +32,9 @@ class TestRegionalAddressTemplate(TestCase): """Update an existing Address Template.""" country = ensure_country("Germany") if not frappe.db.exists("Address Template", country.name): - template = frappe.get_doc({ - "doctype": "Address Template", - "country": country.name, - "template": "EXISTING" - }).insert() + template = frappe.get_doc( + {"doctype": "Address Template", "country": country.name, "template": "EXISTING"} + ).insert() update_address_template(country.name, "NEW") doc = frappe.get_doc("Address Template", country.name) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index b770566ecc9..897d8d86da4 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -9,4 +9,4 @@ from frappe.model.document import Document class EInvoiceSettings(Document): def validate(self): if self.enable and not self.credentials: - frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.')) + frappe.throw(_("You must add atleast one credentials to be able to use E Invoicing.")) diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py index 3b73a5c23ec..e6bdabdf2b7 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py @@ -9,25 +9,28 @@ from frappe.model.document import Document class GSTHSNCode(Document): pass + @frappe.whitelist() def update_taxes_in_item_master(taxes, hsn_code): - items = frappe.get_list("Item", filters={ - 'gst_hsn_code': hsn_code - }) + items = frappe.get_list("Item", filters={"gst_hsn_code": hsn_code}) taxes = frappe.parse_json(taxes) frappe.enqueue(update_item_document, items=items, taxes=taxes) return 1 + def update_item_document(items, taxes): for item in items: - item_to_be_updated=frappe.get_doc("Item", item.name) + item_to_be_updated = frappe.get_doc("Item", item.name) item_to_be_updated.taxes = [] for tax in taxes: tax = frappe._dict(tax) - item_to_be_updated.append("taxes", { - 'item_tax_template': tax.item_tax_template, - 'tax_category': tax.tax_category, - 'valid_from': tax.valid_from - }) + item_to_be_updated.append( + "taxes", + { + "item_tax_template": tax.item_tax_template, + "tax_category": tax.tax_category, + "valid_from": tax.valid_from, + }, + ) item_to_be_updated.save() diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.py b/erpnext/regional/doctype/gst_settings/gst_settings.py index 13ef3e04885..ff09ed01300 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.py +++ b/erpnext/regional/doctype/gst_settings/gst_settings.py @@ -11,15 +11,21 @@ from frappe.model.document import Document from frappe.utils import date_diff, get_url, nowdate -class EmailMissing(frappe.ValidationError): pass +class EmailMissing(frappe.ValidationError): + pass + class GSTSettings(Document): def onload(self): data = frappe._dict() - data.total_addresses = frappe.db.sql('''select count(*) from tabAddress where country = "India"''') - data.total_addresses_with_gstin = frappe.db.sql('''select distinct count(*) - from tabAddress where country = "India" and ifnull(gstin, '')!='' ''') - self.set_onload('data', data) + data.total_addresses = frappe.db.sql( + '''select count(*) from tabAddress where country = "India"''' + ) + data.total_addresses_with_gstin = frappe.db.sql( + """select distinct count(*) + from tabAddress where country = "India" and ifnull(gstin, '')!='' """ + ) + self.set_onload("data", data) def validate(self): # Validate duplicate accounts @@ -27,37 +33,44 @@ class GSTSettings(Document): def validate_duplicate_accounts(self): account_list = [] - for account in self.get('gst_accounts'): - for fieldname in ['cgst_account', 'sgst_account', 'igst_account', 'cess_account']: + for account in self.get("gst_accounts"): + for fieldname in ["cgst_account", "sgst_account", "igst_account", "cess_account"]: if account.get(fieldname) in account_list: - frappe.throw(_("Account {0} appears multiple times").format( - frappe.bold(account.get(fieldname)))) + frappe.throw( + _("Account {0} appears multiple times").format(frappe.bold(account.get(fieldname))) + ) if account.get(fieldname): account_list.append(account.get(fieldname)) + @frappe.whitelist() def send_reminder(): - frappe.has_permission('GST Settings', throw=True) + frappe.has_permission("GST Settings", throw=True) - last_sent = frappe.db.get_single_value('GST Settings', 'gstin_email_sent_on') + last_sent = frappe.db.get_single_value("GST Settings", "gstin_email_sent_on") if last_sent and date_diff(nowdate(), last_sent) < 3: frappe.throw(_("Please wait 3 days before resending the reminder.")) - frappe.db.set_value('GST Settings', 'GST Settings', 'gstin_email_sent_on', nowdate()) + frappe.db.set_value("GST Settings", "GST Settings", "gstin_email_sent_on", nowdate()) # enqueue if large number of customers, suppliser - frappe.enqueue('erpnext.regional.doctype.gst_settings.gst_settings.send_gstin_reminder_to_all_parties') - frappe.msgprint(_('Email Reminders will be sent to all parties with email contacts')) + frappe.enqueue( + "erpnext.regional.doctype.gst_settings.gst_settings.send_gstin_reminder_to_all_parties" + ) + frappe.msgprint(_("Email Reminders will be sent to all parties with email contacts")) + def send_gstin_reminder_to_all_parties(): parties = [] - for address_name in frappe.db.sql('''select name - from tabAddress where country = "India" and ifnull(gstin, '')='' '''): - address = frappe.get_doc('Address', address_name[0]) + for address_name in frappe.db.sql( + """select name + from tabAddress where country = "India" and ifnull(gstin, '')='' """ + ): + address = frappe.get_doc("Address", address_name[0]) for link in address.links: party = frappe.get_doc(link.link_doctype, link.link_name) - if link.link_doctype in ('Customer', 'Supplier'): + if link.link_doctype in ("Customer", "Supplier"): t = (link.link_doctype, link.link_name, address.email_id) if not t in parties: parties.append(t) @@ -74,29 +87,30 @@ def send_gstin_reminder_to_all_parties(): @frappe.whitelist() def send_gstin_reminder(party_type, party): - '''Send GSTIN reminder to one party (called from Customer, Supplier form)''' + """Send GSTIN reminder to one party (called from Customer, Supplier form)""" frappe.has_permission(party_type, throw=True) - email = _send_gstin_reminder(party_type ,party) + email = _send_gstin_reminder(party_type, party) if email: - frappe.msgprint(_('Reminder to update GSTIN Sent'), title='Reminder sent', indicator='green') + frappe.msgprint(_("Reminder to update GSTIN Sent"), title="Reminder sent", indicator="green") + def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None): - '''Send GST Reminder email''' - email_id = frappe.db.get_value('Contact', get_default_contact(party_type, party), 'email_id') + """Send GST Reminder email""" + email_id = frappe.db.get_value("Contact", get_default_contact(party_type, party), "email_id") if not email_id: # get email from address email_id = default_email_id if not email_id: - frappe.throw(_('Email not found in default contact'), exc=EmailMissing) + frappe.throw(_("Email not found in default contact"), exc=EmailMissing) if sent_to and email_id in sent_to: return frappe.sendmail( - subject='Please update your GSTIN', + subject="Please update your GSTIN", recipients=email_id, - message=''' + message="""

    Hello,

    Please help us send you GST Ready Invoices.

    @@ -109,7 +123,9 @@ def _send_gstin_reminder(party_type, party, default_email_id=None, sent_to=None)
    ERPNext is a free and open source ERP system.

    - '''.format(os.path.join(get_url(), '/regional/india/update-gstin'), party) + """.format( + os.path.join(get_url(), "/regional/india/update-gstin"), party + ), ) return email_id diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 05b0c3c8f09..8c891c886ab 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -19,7 +19,7 @@ class GSTR3BReport(Document): self.get_data() def get_data(self): - self.report_dict = json.loads(get_json('gstr_3b_report_template')) + self.report_dict = json.loads(get_json("gstr_3b_report_template")) self.gst_details = self.get_company_gst_details() self.report_dict["gstin"] = self.gst_details.get("gstin") @@ -43,40 +43,46 @@ class GSTR3BReport(Document): self.json_output = frappe.as_json(self.report_dict) def set_inward_nil_exempt(self, inward_nil_exempt): - self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt(inward_nil_exempt.get("gst").get("inter"), 2) - self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt(inward_nil_exempt.get("gst").get("intra"), 2) - self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt(inward_nil_exempt.get("non_gst").get("inter"), 2) - self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt(inward_nil_exempt.get("non_gst").get("intra"), 2) + self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt( + inward_nil_exempt.get("gst").get("inter"), 2 + ) + self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt( + inward_nil_exempt.get("gst").get("intra"), 2 + ) + self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt( + inward_nil_exempt.get("non_gst").get("inter"), 2 + ) + self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt( + inward_nil_exempt.get("non_gst").get("intra"), 2 + ) def set_itc_details(self, itc_details): itc_eligible_type_map = { - 'IMPG': 'Import Of Capital Goods', - 'IMPS': 'Import Of Service', - 'ISRC': 'ITC on Reverse Charge', - 'ISD': 'Input Service Distributor', - 'OTH': 'All Other ITC' + "IMPG": "Import Of Capital Goods", + "IMPS": "Import Of Service", + "ISRC": "ITC on Reverse Charge", + "ISD": "Input Service Distributor", + "OTH": "All Other ITC", } - itc_ineligible_map = { - 'RUL': 'Ineligible As Per Section 17(5)', - 'OTH': 'Ineligible Others' - } + itc_ineligible_map = {"RUL": "Ineligible As Per Section 17(5)", "OTH": "Ineligible Others"} net_itc = self.report_dict["itc_elg"]["itc_net"] for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_eligible_type_map.get(d["ty"]) - for key in ['iamt', 'camt', 'samt', 'csamt']: + for key in ["iamt", "camt", "samt", "csamt"]: d[key] = flt(itc_details.get(itc_type, {}).get(key)) net_itc[key] += flt(d[key], 2) for d in self.report_dict["itc_elg"]["itc_inelg"]: itc_type = itc_ineligible_map.get(d["ty"]) - for key in ['iamt', 'camt', 'samt', 'csamt']: + for key in ["iamt", "camt", "samt", "csamt"]: d[key] = flt(itc_details.get(itc_type, {}).get(key)) def get_itc_reversal_entries(self): - reversal_entries = frappe.db.sql(""" + reversal_entries = frappe.db.sql( + """ SELECT ja.account, j.reversal_type, sum(credit_in_account_currency) as amount FROM `tabJournal Entry` j, `tabJournal Entry Account` ja where j.docstatus = 1 @@ -85,24 +91,27 @@ class GSTR3BReport(Document): and j.voucher_type = 'Reversal Of ITC' and month(j.posting_date) = %s and year(j.posting_date) = %s and j.company = %s and j.company_gstin = %s - GROUP BY ja.account, j.reversal_type""", (self.month_no, self.year, self.company, - self.gst_details.get("gstin")), as_dict=1) + GROUP BY ja.account, j.reversal_type""", + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) net_itc = self.report_dict["itc_elg"]["itc_net"] for entry in reversal_entries: - if entry.reversal_type == 'As per rules 42 & 43 of CGST Rules': + if entry.reversal_type == "As per rules 42 & 43 of CGST Rules": index = 0 else: index = 1 - for key in ['camt', 'samt', 'iamt', 'csamt']: + for key in ["camt", "samt", "iamt", "csamt"]: if entry.account in self.account_heads.get(key): self.report_dict["itc_elg"]["itc_rev"][index][key] += flt(entry.amount) net_itc[key] -= flt(entry.amount) def get_itc_details(self): - itc_amounts = frappe.db.sql(""" + itc_amounts = frappe.db.sql( + """ SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax, sum(itc_central_tax) as itc_central_tax, sum(itc_state_tax) as itc_state_tax, @@ -113,21 +122,28 @@ class GSTR3BReport(Document): and month(posting_date) = %s and year(posting_date) = %s and company = %s and company_gstin = %s GROUP BY eligibility_for_itc - """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + """, + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) itc_details = {} for d in itc_amounts: - itc_details.setdefault(d.eligibility_for_itc, { - 'iamt': d.itc_integrated_tax, - 'camt': d.itc_central_tax, - 'samt': d.itc_state_tax, - 'csamt': d.itc_cess_amount - }) + itc_details.setdefault( + d.eligibility_for_itc, + { + "iamt": d.itc_integrated_tax, + "camt": d.itc_central_tax, + "samt": d.itc_state_tax, + "csamt": d.itc_cess_amount, + }, + ) return itc_details def get_inward_nil_exempt(self, state): - inward_nil_exempt = frappe.db.sql(""" + inward_nil_exempt = frappe.db.sql( + """ SELECT p.place_of_supply, p.supplier_address, i.base_amount, i.is_nil_exempt, i.is_non_gst FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i @@ -138,17 +154,13 @@ class GSTR3BReport(Document): month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s """, - (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) inward_nil_exempt_details = { - "gst": { - "intra": 0.0, - "inter": 0.0 - }, - "non_gst": { - "intra": 0.0, - "inter": 0.0 - } + "gst": {"intra": 0.0, "inter": 0.0}, + "non_gst": {"intra": 0.0, "inter": 0.0}, } address_state_map = get_address_state_map() @@ -159,11 +171,13 @@ class GSTR3BReport(Document): supplier_state = address_state_map.get(d.supplier_address) or state - if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ - and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]): + if (d.is_nil_exempt == 1 or d.get("gst_category") == "Registered Composition") and cstr( + supplier_state + ) == cstr(d.place_of_supply.split("-")[1]): inward_nil_exempt_details["gst"]["intra"] += d.base_amount - elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ - and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]): + elif (d.is_nil_exempt == 1 or d.get("gst_category") == "Registered Composition") and cstr( + supplier_state + ) != cstr(d.place_of_supply.split("-")[1]): inward_nil_exempt_details["gst"]["inter"] += d.base_amount elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]): inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount @@ -180,12 +194,13 @@ class GSTR3BReport(Document): def get_outward_tax_invoices(self, doctype, reverse_charge=None): self.invoices = [] self.invoice_detail_map = {} - condition = '' + condition = "" if reverse_charge: condition += "AND reverse_charge = 'Y'" - invoice_details = frappe.db.sql(""" + invoice_details = frappe.db.sql( + """ SELECT name, gst_category, export_type, place_of_supply FROM @@ -199,8 +214,12 @@ class GSTR3BReport(Document): AND is_opening = 'No' {reverse_charge} ORDER BY name - """.format(doctype=doctype, reverse_charge=condition), (self.month_no, self.year, - self.company, self.gst_details.get("gstin")), as_dict=1) + """.format( + doctype=doctype, reverse_charge=condition + ), + (self.month_no, self.year, self.company, self.gst_details.get("gstin")), + as_dict=1, + ) for d in invoice_details: self.invoice_detail_map.setdefault(d.name, d) @@ -211,20 +230,27 @@ class GSTR3BReport(Document): self.is_nil_exempt = [] self.is_non_gst = [] - if self.get('invoices'): - item_details = frappe.db.sql(""" + if self.get("invoices"): + item_details = frappe.db.sql( + """ SELECT item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, is_non_gst FROM `tab%s Item` WHERE parent in (%s) - """ % (doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in item_details: if d.item_code not in self.invoice_items.get(d.parent, {}): self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get( + "base_net_amount", 0 + ) if d.is_nil_exempt and d.item_code not in self.is_nil_exempt: self.is_nil_exempt.append(d.item_code) @@ -234,16 +260,17 @@ class GSTR3BReport(Document): def get_outward_tax_details(self, doctype): if doctype == "Sales Invoice": - tax_template = 'Sales Taxes and Charges' + tax_template = "Sales Taxes and Charges" elif doctype == "Purchase Invoice": - tax_template = 'Purchase Taxes and Charges' + tax_template = "Purchase Taxes and Charges" self.items_based_on_tax_rate = {} self.invoice_cess = frappe._dict() self.cgst_sgst_invoices = [] - if self.get('invoices'): - tax_details = frappe.db.sql(""" + if self.get("invoices"): + tax_details = frappe.db.sql( + """ SELECT parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount FROM `tab%s` @@ -251,24 +278,28 @@ class GSTR3BReport(Document): parenttype = %s and docstatus = 1 and parent in (%s) ORDER BY account_head - """ % (tax_template, '%s', ', '.join(['%s']*len(self.invoices))), - tuple([doctype] + list(self.invoices))) + """ + % (tax_template, "%s", ", ".join(["%s"] * len(self.invoices))), + tuple([doctype] + list(self.invoices)), + ) for parent, account, item_wise_tax_detail, tax_amount in tax_details: - if account in self.account_heads.get('csamt'): + if account in self.account_heads.get("csamt"): self.invoice_cess.setdefault(parent, tax_amount) else: if item_wise_tax_detail: try: item_wise_tax_detail = json.loads(item_wise_tax_detail) cgst_or_sgst = False - if account in self.account_heads.get('camt') \ - or account in self.account_heads.get('samt'): + if account in self.account_heads.get("camt") or account in self.account_heads.get("samt"): cgst_or_sgst = True for item_code, tax_amounts in item_wise_tax_detail.items(): - if not (cgst_or_sgst or account in self.account_heads.get('iamt') or - (item_code in self.is_non_gst + self.is_nil_exempt)): + if not ( + cgst_or_sgst + or account in self.account_heads.get("iamt") + or (item_code in self.is_non_gst + self.is_nil_exempt) + ): continue tax_rate = tax_amounts[0] @@ -278,66 +309,76 @@ class GSTR3BReport(Document): if parent not in self.cgst_sgst_invoices: self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: continue - - if self.get('invoice_items'): + if self.get("invoice_items"): # Build itemised tax for export invoices, nil and exempted where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \ - == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas": + if ( + invoice not in self.items_based_on_tax_rate + and self.invoice_detail_map.get(invoice, {}).get("export_type") == "Without Payment of Tax" + and self.invoice_detail_map.get(invoice, {}).get("gst_category") == "Overseas" + ): self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) else: for item in items.keys(): - if item in self.is_nil_exempt + self.is_non_gst and \ - item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, []) - self.items_based_on_tax_rate[invoice][0].append(item) + if ( + item in self.is_nil_exempt + self.is_non_gst + and item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []) + ): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, []) + self.items_based_on_tax_rate[invoice][0].append(item) def set_outward_taxable_supplies(self): inter_state_supply_details = {} for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): - gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category') - place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory' - export_type = self.invoice_detail_map.get(inv, {}).get('export_type') + gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category") + place_of_supply = ( + self.invoice_detail_map.get(inv, {}).get("place_of_supply") or "00-Other Territory" + ) + export_type = self.invoice_detail_map.get(inv, {}).get("export_type") for rate, items in items_based_on_rate.items(): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: if item_code in self.is_nil_exempt: - self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value + self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] += taxable_value elif item_code in self.is_non_gst: - self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value - elif rate == 0 or (gst_category == 'Overseas' and export_type == 'Without Payment of Tax'): - self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value + self.report_dict["sup_details"]["osup_nongst"]["txval"] += taxable_value + elif rate == 0 or (gst_category == "Overseas" and export_type == "Without Payment of Tax"): + self.report_dict["sup_details"]["osup_zero"]["txval"] += taxable_value else: if inv in self.cgst_sgst_invoices: - tax_rate = rate/2 - self.report_dict['sup_details']['osup_det']['camt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['osup_det']['samt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['osup_det']['txval'] += taxable_value + tax_rate = rate / 2 + self.report_dict["sup_details"]["osup_det"]["camt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["osup_det"]["samt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value else: - self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100) - self.report_dict['sup_details']['osup_det']['txval'] += taxable_value + self.report_dict["sup_details"]["osup_det"]["iamt"] += taxable_value * rate / 100 + self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value - if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \ - self.gst_details.get("gst_state") != place_of_supply.split("-")[1]: - inter_state_supply_details.setdefault((gst_category, place_of_supply), { - "txval": 0.0, - "pos": place_of_supply.split("-")[0], - "iamt": 0.0 - }) - inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value - inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100) + if ( + gst_category in ["Unregistered", "Registered Composition", "UIN Holders"] + and self.gst_details.get("gst_state") != place_of_supply.split("-")[1] + ): + inter_state_supply_details.setdefault( + (gst_category, place_of_supply), + {"txval": 0.0, "pos": place_of_supply.split("-")[0], "iamt": 0.0}, + ) + inter_state_supply_details[(gst_category, place_of_supply)]["txval"] += taxable_value + inter_state_supply_details[(gst_category, place_of_supply)]["iamt"] += ( + taxable_value * rate / 100 + ) if self.invoice_cess.get(inv): - self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2) + self.report_dict["sup_details"]["osup_det"]["csamt"] += flt(self.invoice_cess.get(inv), 2) self.set_inter_state_supply(inter_state_supply_details) @@ -347,13 +388,13 @@ class GSTR3BReport(Document): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: if inv in self.cgst_sgst_invoices: - tax_rate = rate/2 - self.report_dict['sup_details']['isup_rev']['camt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['isup_rev']['samt'] += (taxable_value * tax_rate /100) - self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value + tax_rate = rate / 2 + self.report_dict["sup_details"]["isup_rev"]["camt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["isup_rev"]["samt"] += taxable_value * tax_rate / 100 + self.report_dict["sup_details"]["isup_rev"]["txval"] += taxable_value else: - self.report_dict['sup_details']['isup_rev']['iamt'] += (taxable_value * rate /100) - self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value + self.report_dict["sup_details"]["isup_rev"]["iamt"] += taxable_value * rate / 100 + self.report_dict["sup_details"]["isup_rev"]["txval"] += taxable_value def set_inter_state_supply(self, inter_state_supply): for key, value in iteritems(inter_state_supply): @@ -367,29 +408,33 @@ class GSTR3BReport(Document): self.report_dict["inter_sup"]["uin_details"].append(value) def get_company_gst_details(self): - gst_details = frappe.get_all("Address", + gst_details = frappe.get_all( + "Address", fields=["gstin", "gst_state", "gst_state_number"], - filters={ - "name":self.company_address - }) + filters={"name": self.company_address}, + ) if gst_details: return gst_details[0] else: - frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address)) + frappe.throw( + _("Please enter GSTIN and state for the Company Address {0}").format(self.company_address) + ) def get_account_heads(self): account_map = { - 'sgst_account': 'samt', - 'cess_account': 'csamt', - 'cgst_account': 'camt', - 'igst_account': 'iamt' + "sgst_account": "samt", + "cess_account": "csamt", + "cgst_account": "camt", + "igst_account": "iamt", } account_heads = {} - gst_settings_accounts = frappe.get_all("GST Account", - filters={'company': self.company, 'is_reverse_charge_account': 0}, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) + gst_settings_accounts = frappe.get_all( + "GST Account", + filters={"company": self.company, "is_reverse_charge_account": 0}, + fields=["cgst_account", "sgst_account", "igst_account", "cess_account"], + ) if not gst_settings_accounts: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -406,42 +451,48 @@ class GSTR3BReport(Document): for doctype in ["Sales Invoice", "Purchase Invoice"]: if doctype == "Sales Invoice": - party_type = 'Customer' - party = 'customer' + party_type = "Customer" + party = "customer" else: - party_type = 'Supplier' - party = 'supplier' + party_type = "Supplier" + party = "supplier" docnames = frappe.db.sql( - """ + """ SELECT t1.name FROM `tab{doctype}` t1, `tab{party_type}` t2 WHERE t1.docstatus = 1 and t1.is_opening = 'No' and month(t1.posting_date) = %s and year(t1.posting_date) = %s and t1.company = %s and t1.place_of_supply IS NULL and t1.{party} = t2.name and t2.gst_category != 'Overseas' - """.format(doctype = doctype, party_type = party_type, - party=party) ,(self.month_no, self.year, self.company), as_dict=1) #nosec + """.format( + doctype=doctype, party_type=party_type, party=party + ), + (self.month_no, self.year, self.company), + as_dict=1, + ) # nosec for d in docnames: missing_field_invoices.append(d.name) return ",".join(missing_field_invoices) + def get_address_state_map(): - return frappe._dict( - frappe.get_all('Address', fields=['name', 'gst_state'], as_list=1) - ) + return frappe._dict(frappe.get_all("Address", fields=["name", "gst_state"], as_list=1)) + def get_json(template): - file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template)) - with open(file_path, 'r') as f: + file_path = os.path.join(os.path.dirname(__file__), "{template}.json".format(template=template)) + with open(file_path, "r") as f: return cstr(f.read()) + def get_state_code(state): state_code = state_numbers.get(state) return state_code + def get_period(month, year=None): month_no = { "January": 1, @@ -455,7 +506,7 @@ def get_period(month, year=None): "September": 9, "October": 10, "November": 11, - "December": 12 + "December": 12, }.get(month) if year: @@ -466,12 +517,13 @@ def get_period(month, year=None): @frappe.whitelist() def view_report(name): - json_data = frappe.get_value("GSTR 3B Report", name, 'json_output') + json_data = frappe.get_value("GSTR 3B Report", name, "json_output") return json.loads(json_data) + @frappe.whitelist() def make_json(name): - json_data = frappe.get_value("GSTR 3B Report", name, 'json_output') + json_data = frappe.get_value("GSTR 3B Report", name, "json_output") file_name = "GST3B.json" frappe.local.response.filename = file_name frappe.local.response.filecontent = json_data diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index e12e3d7b800..3862c625303 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -13,6 +13,7 @@ from erpnext.stock.doctype.item.test_item import make_item test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] + class TestGSTR3BReport(unittest.TestCase): def setUp(self): frappe.set_user("Administrator") @@ -22,7 +23,7 @@ class TestGSTR3BReport(unittest.TestCase): frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'") make_company() - make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000}) + make_item("Milk", properties={"is_nil_exempt": 1, "standard_rate": 0.000000}) set_account_heads() make_customers() make_suppliers() @@ -40,7 +41,7 @@ class TestGSTR3BReport(unittest.TestCase): 9: "September", 10: "October", 11: "November", - 12: "December" + 12: "December", } make_sales_invoice() @@ -50,13 +51,15 @@ class TestGSTR3BReport(unittest.TestCase): report = frappe.get_doc("GSTR 3B Report", "GSTR3B-March-2019-_Test Address GST-Billing") report.save() else: - report = frappe.get_doc({ - "doctype": "GSTR 3B Report", - "company": "_Test Company GST", - "company_address": "_Test Address GST-Billing", - "year": getdate().year, - "month": month_number_mapping.get(getdate().month) - }).insert() + report = frappe.get_doc( + { + "doctype": "GSTR 3B Report", + "company": "_Test Company GST", + "company_address": "_Test Address GST-Billing", + "year": getdate().year, + "month": month_number_mapping.get(getdate().month), + } + ).insert() output = json.loads(report.json_output) @@ -68,32 +71,36 @@ class TestGSTR3BReport(unittest.TestCase): self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50) def test_gst_rounding(self): - gst_settings = frappe.get_doc('GST Settings') + gst_settings = frappe.get_doc("GST Settings") gst_settings.round_off_gst_values = 1 gst_settings.save() current_country = frappe.flags.country - frappe.flags.country = 'India' + frappe.flags.country = "India" - si = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', + si = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", rate=216, - do_not_save=1 + do_not_save=1, ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) si.save() # Check for 39 instead of 38.88 @@ -105,20 +112,313 @@ class TestGSTR3BReport(unittest.TestCase): def test_gst_category_auto_update(self): if not frappe.db.exists("Customer", "_Test GST Customer With GSTIN"): - customer = frappe.get_doc({ + customer = frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "_Test GST Customer With GSTIN", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() + + self.assertEqual(customer.gst_category, "Unregistered") + + if not frappe.db.exists("Address", "_Test GST Category-1-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test GST Category-1", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gstin": "29AZWPS7135H1ZG", + "gst_state": "Karnataka", + "gst_state_number": "29", + } + ).insert() + + address.append( + "links", {"link_doctype": "Customer", "link_name": "_Test GST Customer With GSTIN"} + ) + + address.save() + + customer.load_from_db() + self.assertEqual(customer.gst_category, "Registered Regular") + + +def make_sales_invoice(): + si = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) + + si.submit() + + si1 = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST SEZ Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + + si1.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) + + si1.submit() + + si2 = create_sales_invoice( + company="_Test Company GST", + customer="_Test Unregistered Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + + si2.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) + + si2.submit() + + si3 = create_sales_invoice( + company="_Test Company GST", + customer="_Test GST Customer", + currency="INR", + item="Milk", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, + ) + si3.submit() + + +def create_purchase_invoices(): + pi = make_purchase_invoice( + company="_Test Company GST", + supplier="_Test Registered Supplier", + currency="INR", + warehouse="Finished Goods - _GST", + cost_center="Main - _GST", + expense_account="Cost of Goods Sold - _GST", + do_not_save=1, + ) + + pi.eligibility_for_itc = "All Other ITC" + + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Input Tax CGST - _GST", + "cost_center": "Main - _GST", + "description": "CGST @ 9.0", + "rate": 9, + }, + ) + + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Input Tax SGST - _GST", + "cost_center": "Main - _GST", + "description": "SGST @ 9.0", + "rate": 9, + }, + ) + + pi.submit() + + pi1 = make_purchase_invoice( + company="_Test Company GST", + supplier="_Test Registered Supplier", + currency="INR", + warehouse="Finished Goods - _GST", + cost_center="Main - _GST", + expense_account="Cost of Goods Sold - _GST", + item="Milk", + do_not_save=1, + ) + + pi1.shipping_address = "_Test Supplier GST-1-Billing" + pi1.save() + + pi1.submit() + + pi2 = make_purchase_invoice( + company="_Test Company GST", + customer="_Test Registered Supplier", + currency="INR", + item="Milk", + warehouse="Finished Goods - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + rate=250, + qty=1, + do_not_save=1, + ) + pi2.submit() + + +def make_suppliers(): + if not frappe.db.exists("Supplier", "_Test Registered Supplier"): + frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Registered Supplier", + "gst_category": "Registered Regular", + "supplier_type": "Individual", + "doctype": "Supplier", + } + ).insert() + + if not frappe.db.exists("Supplier", "_Test Unregistered Supplier"): + frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Unregistered Supplier", + "gst_category": "Unregistered", + "supplier_type": "Individual", + "doctype": "Supplier", + } + ).insert() + + if not frappe.db.exists("Address", "_Test Supplier GST-1-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Supplier GST-1", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gstin": "29AACCV0498C1Z9", + "gst_state": "Karnataka", + } + ).insert() + + address.append("links", {"link_doctype": "Supplier", "link_name": "_Test Registered Supplier"}) + + address.is_shipping_address = 1 + address.save() + + if not frappe.db.exists("Address", "_Test Supplier GST-2-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Supplier GST-2", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gst_state": "Karnataka", + } + ).insert() + + address.append("links", {"link_doctype": "Supplier", "link_name": "_Test Unregistered Supplier"}) + + address.save() + + +def make_customers(): + if not frappe.db.exists("Customer", "_Test GST Customer"): + frappe.get_doc( + { "customer_group": "_Test Customer Group", - "customer_name": "_Test GST Customer With GSTIN", + "customer_name": "_Test GST Customer", + "gst_category": "Registered Regular", "customer_type": "Individual", "doctype": "Customer", - "territory": "_Test Territory" - }).insert() + "territory": "_Test Territory", + } + ).insert() - self.assertEqual(customer.gst_category, 'Unregistered') + if not frappe.db.exists("Customer", "_Test GST SEZ Customer"): + frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "_Test GST SEZ Customer", + "gst_category": "SEZ", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() - if not frappe.db.exists('Address', '_Test GST Category-1-Billing'): - address = frappe.get_doc({ + if not frappe.db.exists("Customer", "_Test Unregistered Customer"): + frappe.get_doc( + { + "customer_group": "_Test Customer Group", + "customer_name": "_Test Unregistered Customer", + "gst_category": "Unregistered", + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + } + ).insert() + + if not frappe.db.exists("Address", "_Test GST-1-Billing"): + address = frappe.get_doc( + { "address_line1": "_Test Address Line 1", - "address_title": "_Test GST Category-1", + "address_title": "_Test GST-1", "address_type": "Billing", "city": "_Test City", "state": "Test State", @@ -128,315 +428,54 @@ class TestGSTR3BReport(unittest.TestCase): "phone": "+91 0000000000", "gstin": "29AZWPS7135H1ZG", "gst_state": "Karnataka", - "gst_state_number": "29" - }).insert() + "gst_state_number": "29", + } + ).insert() - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test GST Customer With GSTIN" - }) - - address.save() - - customer.load_from_db() - self.assertEqual(customer.gst_category, 'Registered Regular') - - -def make_sales_invoice(): - si = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) - - si.submit() - - si1 = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST SEZ Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - - si1.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) - - si1.submit() - - si2 = create_sales_invoice(company="_Test Company GST", - customer = '_Test Unregistered Customer', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - - si2.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) - - si2.submit() - - si3 = create_sales_invoice(company="_Test Company GST", - customer = '_Test GST Customer', - currency = 'INR', - item = 'Milk', - warehouse = 'Finished Goods - _GST', - debit_to = 'Debtors - _GST', - income_account = 'Sales - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - do_not_save=1 - ) - si3.submit() - -def create_purchase_invoices(): - pi = make_purchase_invoice( - company="_Test Company GST", - supplier = '_Test Registered Supplier', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - cost_center = 'Main - _GST', - expense_account = 'Cost of Goods Sold - _GST', - do_not_save=1, - ) - - pi.eligibility_for_itc = "All Other ITC" - - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Input Tax CGST - _GST", - "cost_center": "Main - _GST", - "description": "CGST @ 9.0", - "rate": 9 - }) - - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Input Tax SGST - _GST", - "cost_center": "Main - _GST", - "description": "SGST @ 9.0", - "rate": 9 - }) - - pi.submit() - - pi1 = make_purchase_invoice( - company="_Test Company GST", - supplier = '_Test Registered Supplier', - currency = 'INR', - warehouse = 'Finished Goods - _GST', - cost_center = 'Main - _GST', - expense_account = 'Cost of Goods Sold - _GST', - item = "Milk", - do_not_save=1 - ) - - pi1.shipping_address = "_Test Supplier GST-1-Billing" - pi1.save() - - pi1.submit() - - pi2 = make_purchase_invoice(company="_Test Company GST", - customer = '_Test Registered Supplier', - currency = 'INR', - item = 'Milk', - warehouse = 'Finished Goods - _GST', - expense_account = 'Cost of Goods Sold - _GST', - cost_center = 'Main - _GST', - rate=250, - qty=1, - do_not_save=1 - ) - pi2.submit() - -def make_suppliers(): - if not frappe.db.exists("Supplier", "_Test Registered Supplier"): - frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Registered Supplier", - "gst_category": "Registered Regular", - "supplier_type": "Individual", - "doctype": "Supplier", - }).insert() - - if not frappe.db.exists("Supplier", "_Test Unregistered Supplier"): - frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Unregistered Supplier", - "gst_category": "Unregistered", - "supplier_type": "Individual", - "doctype": "Supplier", - }).insert() - - if not frappe.db.exists('Address', '_Test Supplier GST-1-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Supplier GST-1", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "29AACCV0498C1Z9", - "gst_state": "Karnataka", - }).insert() - - address.append("links", { - "link_doctype": "Supplier", - "link_name": "_Test Registered Supplier" - }) - - address.is_shipping_address = 1 - address.save() - - if not frappe.db.exists('Address', '_Test Supplier GST-2-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Supplier GST-2", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Karnataka", - }).insert() - - address.append("links", { - "link_doctype": "Supplier", - "link_name": "_Test Unregistered Supplier" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test GST Customer"}) address.save() -def make_customers(): - if not frappe.db.exists("Customer", "_Test GST Customer"): - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test GST Customer", - "gst_category": "Registered Regular", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() + if not frappe.db.exists("Address", "_Test GST-2-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test GST-2", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gst_state": "Haryana", + } + ).insert() - if not frappe.db.exists("Customer", "_Test GST SEZ Customer"): - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test GST SEZ Customer", - "gst_category": "SEZ", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() - - if not frappe.db.exists("Customer", "_Test Unregistered Customer"): - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": "_Test Unregistered Customer", - "gst_category": "Unregistered", - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" - }).insert() - - if not frappe.db.exists('Address', '_Test GST-1-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test GST-1", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "29AZWPS7135H1ZG", - "gst_state": "Karnataka", - "gst_state_number": "29" - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test GST Customer" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test Unregistered Customer"}) address.save() - if not frappe.db.exists('Address', '_Test GST-2-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test GST-2", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Haryana", - }).insert() + if not frappe.db.exists("Address", "_Test GST-3-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test GST-3", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gst_state": "Gujarat", + } + ).insert() - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Unregistered Customer" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test GST SEZ Customer"}) address.save() - if not frappe.db.exists('Address', '_Test GST-3-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test GST-3", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Gujarat", - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test GST SEZ Customer" - }) - - address.save() def make_company(): if frappe.db.exists("Company", "_Test Company GST"): @@ -449,43 +488,47 @@ def make_company(): company.country = "India" company.insert() - if not frappe.db.exists('Address', '_Test Address GST-Billing'): - address = frappe.get_doc({ - "address_title": "_Test Address GST", - "address_line1": "_Test Address Line 1", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "27AAECE4835E1ZR", - "gst_state": "Maharashtra", - "gst_state_number": "27" - }).insert() + if not frappe.db.exists("Address", "_Test Address GST-Billing"): + address = frappe.get_doc( + { + "address_title": "_Test Address GST", + "address_line1": "_Test Address Line 1", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+91 0000000000", + "gstin": "27AAECE4835E1ZR", + "gst_state": "Maharashtra", + "gst_state_number": "27", + } + ).insert() - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company GST" - }) + address.append("links", {"link_doctype": "Company", "link_name": "_Test Company GST"}) address.save() + def set_account_heads(): gst_settings = frappe.get_doc("GST Settings") gst_account = frappe.get_all( "GST Account", fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company GST"}) + filters={"company": "_Test Company GST"}, + ) if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company GST", - "cgst_account": "Output Tax CGST - _GST", - "sgst_account": "Output Tax SGST - _GST", - "igst_account": "Output Tax IGST - _GST" - }) + gst_settings.append( + "gst_accounts", + { + "company": "_Test Company GST", + "cgst_account": "Output Tax CGST - _GST", + "sgst_account": "Output Tax SGST - _GST", + "igst_account": "Output Tax IGST - _GST", + }, + ) gst_settings.save() diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py index 97b8488c2fe..77c4d7c6ca3 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -27,11 +27,10 @@ class ImportSupplierInvoice(Document): self.name = "Import Invoice on " + format_datetime(self.creation) def import_xml_data(self): - zip_file = frappe.get_doc("File", { - "file_url": self.zip_file, - "attached_to_doctype": self.doctype, - "attached_to_name": self.name - }) + zip_file = frappe.get_doc( + "File", + {"file_url": self.zip_file, "attached_to_doctype": self.doctype, "attached_to_name": self.name}, + ) self.publish("File Import", _("Processing XML Files"), 1, 3) @@ -65,10 +64,10 @@ class ImportSupplierInvoice(Document): "bill_no": line.Numero.text, "total_discount": 0, "items": [], - "buying_price_list": self.default_buying_price_list + "buying_price_list": self.default_buying_price_list, } - if not invoices_args.get("bill_no", ''): + if not invoices_args.get("bill_no", ""): frappe.throw(_("Numero has not set in the XML file")) supp_dict = get_supplier_details(file_content) @@ -84,15 +83,23 @@ class ImportSupplierInvoice(Document): self.file_count += 1 if pi_name: self.purchase_invoices_count += 1 - file_save = save_file(file_name, encoded_content, "Purchase Invoice", - pi_name, folder=None, decode=False, is_private=0, df=None) + file_save = save_file( + file_name, + encoded_content, + "Purchase Invoice", + pi_name, + folder=None, + decode=False, + is_private=0, + df=None, + ) def prepare_items_for_invoice(self, file_content, invoices_args): qty = 1 - rate, tax_rate = [0 ,0] + rate, tax_rate = [0, 0] uom = self.default_uom - #read file for item information + # read file for item information for line in file_content.find_all("DettaglioLinee"): if line.find("PrezzoUnitario") and line.find("PrezzoTotale"): rate = flt(line.PrezzoUnitario.text) or 0 @@ -103,30 +110,34 @@ class ImportSupplierInvoice(Document): if line.find("UnitaMisura"): uom = create_uom(line.UnitaMisura.text) - if (rate < 0 and line_total < 0): + if rate < 0 and line_total < 0: qty *= -1 invoices_args["return_invoice"] = 1 if line.find("AliquotaIVA"): tax_rate = flt(line.AliquotaIVA.text) - line_str = re.sub('[^A-Za-z0-9]+', '-', line.Descrizione.text) + line_str = re.sub("[^A-Za-z0-9]+", "-", line.Descrizione.text) item_name = line_str[0:140] - invoices_args['items'].append({ - "item_code": self.item_code, - "item_name": item_name, - "description": line_str, - "qty": qty, - "uom": uom, - "rate": abs(rate), - "conversion_factor": 1.0, - "tax_rate": tax_rate - }) + invoices_args["items"].append( + { + "item_code": self.item_code, + "item_name": item_name, + "description": line_str, + "qty": qty, + "uom": uom, + "rate": abs(rate), + "conversion_factor": 1.0, + "tax_rate": tax_rate, + } + ) for disc_line in line.find_all("ScontoMaggiorazione"): if disc_line.find("Percentuale"): - invoices_args["total_discount"] += flt((flt(disc_line.Percentuale.text) / 100) * (rate * qty)) + invoices_args["total_discount"] += flt( + (flt(disc_line.Percentuale.text) / 100) * (rate * qty) + ) @frappe.whitelist() def process_file_data(self): @@ -134,10 +145,13 @@ class ImportSupplierInvoice(Document): frappe.enqueue_doc(self.doctype, self.name, "import_xml_data", queue="long", timeout=3600) def publish(self, title, message, count, total): - frappe.publish_realtime("import_invoice_update", {"title": title, "message": message, "count": count, "total": total}) + frappe.publish_realtime( + "import_invoice_update", {"title": title, "message": message, "count": count, "total": total} + ) + def get_file_content(file_name, zip_file_object): - content = '' + content = "" encoded_content = zip_file_object.read(file_name) try: @@ -150,113 +164,122 @@ def get_file_content(file_name, zip_file_object): return content + def get_supplier_details(file_content): supplier_info = {} for line in file_content.find_all("CedentePrestatore"): - supplier_info['tax_id'] = line.DatiAnagrafici.IdPaese.text + line.DatiAnagrafici.IdCodice.text + supplier_info["tax_id"] = line.DatiAnagrafici.IdPaese.text + line.DatiAnagrafici.IdCodice.text if line.find("CodiceFiscale"): - supplier_info['fiscal_code'] = line.DatiAnagrafici.CodiceFiscale.text + supplier_info["fiscal_code"] = line.DatiAnagrafici.CodiceFiscale.text if line.find("RegimeFiscale"): - supplier_info['fiscal_regime'] = line.DatiAnagrafici.RegimeFiscale.text + supplier_info["fiscal_regime"] = line.DatiAnagrafici.RegimeFiscale.text if line.find("Denominazione"): - supplier_info['supplier'] = line.DatiAnagrafici.Anagrafica.Denominazione.text + supplier_info["supplier"] = line.DatiAnagrafici.Anagrafica.Denominazione.text if line.find("Nome"): - supplier_info['supplier'] = (line.DatiAnagrafici.Anagrafica.Nome.text - + " " + line.DatiAnagrafici.Anagrafica.Cognome.text) + supplier_info["supplier"] = ( + line.DatiAnagrafici.Anagrafica.Nome.text + " " + line.DatiAnagrafici.Anagrafica.Cognome.text + ) - supplier_info['address_line1'] = line.Sede.Indirizzo.text - supplier_info['city'] = line.Sede.Comune.text + supplier_info["address_line1"] = line.Sede.Indirizzo.text + supplier_info["city"] = line.Sede.Comune.text if line.find("Provincia"): - supplier_info['province'] = line.Sede.Provincia.text + supplier_info["province"] = line.Sede.Provincia.text - supplier_info['pin_code'] = line.Sede.CAP.text - supplier_info['country'] = get_country(line.Sede.Nazione.text) + supplier_info["pin_code"] = line.Sede.CAP.text + supplier_info["country"] = get_country(line.Sede.Nazione.text) return supplier_info + def get_taxes_from_file(file_content, tax_account): taxes = [] - #read file for taxes information + # read file for taxes information for line in file_content.find_all("DatiRiepilogo"): if line.find("AliquotaIVA"): if line.find("EsigibilitaIVA"): descr = line.EsigibilitaIVA.text else: descr = "None" - taxes.append({ - "charge_type": "Actual", - "account_head": tax_account, - "tax_rate": flt(line.AliquotaIVA.text) or 0, - "description": descr, - "tax_amount": flt(line.Imposta.text) if len(line.find("Imposta"))!=0 else 0 - }) + taxes.append( + { + "charge_type": "Actual", + "account_head": tax_account, + "tax_rate": flt(line.AliquotaIVA.text) or 0, + "description": descr, + "tax_amount": flt(line.Imposta.text) if len(line.find("Imposta")) != 0 else 0, + } + ) return taxes + def get_payment_terms_from_file(file_content): terms = [] - #Get mode of payment dict from setup - mop_options = frappe.get_meta('Mode of Payment').fields[4].options - mop_str = re.sub('\n', ',', mop_options) + # Get mode of payment dict from setup + mop_options = frappe.get_meta("Mode of Payment").fields[4].options + mop_str = re.sub("\n", ",", mop_options) mop_dict = dict(item.split("-") for item in mop_str.split(",")) - #read file for payment information + # read file for payment information for line in file_content.find_all("DettaglioPagamento"): - mop_code = line.ModalitaPagamento.text + '-' + mop_dict.get(line.ModalitaPagamento.text) + mop_code = line.ModalitaPagamento.text + "-" + mop_dict.get(line.ModalitaPagamento.text) if line.find("DataScadenzaPagamento"): due_date = dateutil.parser.parse(line.DataScadenzaPagamento.text).strftime("%Y-%m-%d") else: due_date = today() - terms.append({ - "mode_of_payment_code": mop_code, - "bank_account_iban": line.IBAN.text if line.find("IBAN") else "", - "due_date": due_date, - "payment_amount": line.ImportoPagamento.text - }) + terms.append( + { + "mode_of_payment_code": mop_code, + "bank_account_iban": line.IBAN.text if line.find("IBAN") else "", + "due_date": due_date, + "payment_amount": line.ImportoPagamento.text, + } + ) return terms + def get_destination_code_from_file(file_content): - destination_code = '' + destination_code = "" for line in file_content.find_all("DatiTrasmissione"): destination_code = line.CodiceDestinatario.text return destination_code + def create_supplier(supplier_group, args): args = frappe._dict(args) - existing_supplier_name = frappe.db.get_value("Supplier", - filters={"tax_id": args.tax_id}, fieldname="name") + existing_supplier_name = frappe.db.get_value( + "Supplier", filters={"tax_id": args.tax_id}, fieldname="name" + ) if existing_supplier_name: pass else: - existing_supplier_name = frappe.db.get_value("Supplier", - filters={"name": args.supplier}, fieldname="name") + existing_supplier_name = frappe.db.get_value( + "Supplier", filters={"name": args.supplier}, fieldname="name" + ) if existing_supplier_name: filters = [ - ["Dynamic Link", "link_doctype", "=", "Supplier"], - ["Dynamic Link", "link_name", "=", args.existing_supplier_name], - ["Dynamic Link", "parenttype", "=", "Contact"] - ] + ["Dynamic Link", "link_doctype", "=", "Supplier"], + ["Dynamic Link", "link_name", "=", args.existing_supplier_name], + ["Dynamic Link", "parenttype", "=", "Contact"], + ] if not frappe.get_list("Contact", filters): new_contact = frappe.new_doc("Contact") new_contact.first_name = args.supplier[:30] - new_contact.append('links', { - "link_doctype": "Supplier", - "link_name": existing_supplier_name - }) + new_contact.append("links", {"link_doctype": "Supplier", "link_name": existing_supplier_name}) new_contact.insert(ignore_mandatory=True) return existing_supplier_name else: new_supplier = frappe.new_doc("Supplier") - new_supplier.supplier_name = re.sub('&', '&', args.supplier) + new_supplier.supplier_name = re.sub("&", "&", args.supplier) new_supplier.supplier_group = supplier_group new_supplier.tax_id = args.tax_id new_supplier.fiscal_code = args.fiscal_code @@ -265,23 +288,21 @@ def create_supplier(supplier_group, args): new_contact = frappe.new_doc("Contact") new_contact.first_name = args.supplier[:30] - new_contact.append('links', { - "link_doctype": "Supplier", - "link_name": new_supplier.name - }) + new_contact.append("links", {"link_doctype": "Supplier", "link_name": new_supplier.name}) new_contact.insert(ignore_mandatory=True) return new_supplier.name + def create_address(supplier_name, args): args = frappe._dict(args) filters = [ - ["Dynamic Link", "link_doctype", "=", "Supplier"], - ["Dynamic Link", "link_name", "=", supplier_name], - ["Dynamic Link", "parenttype", "=", "Address"] - ] + ["Dynamic Link", "link_doctype", "=", "Supplier"], + ["Dynamic Link", "link_name", "=", supplier_name], + ["Dynamic Link", "parenttype", "=", "Address"], + ] existing_address = frappe.get_list("Address", filters) @@ -300,50 +321,52 @@ def create_address(supplier_name, args): for address in existing_address: address_doc = frappe.get_doc("Address", address["name"]) - if (address_doc.address_line1 == new_address_doc.address_line1 and - address_doc.pincode == new_address_doc.pincode): + if ( + address_doc.address_line1 == new_address_doc.address_line1 + and address_doc.pincode == new_address_doc.pincode + ): return address - new_address_doc.append("links", { - "link_doctype": "Supplier", - "link_name": supplier_name - }) + new_address_doc.append("links", {"link_doctype": "Supplier", "link_name": supplier_name}) new_address_doc.address_type = "Billing" new_address_doc.insert(ignore_mandatory=True) return new_address_doc.name else: return None + def create_purchase_invoice(supplier_name, file_name, args, name): args = frappe._dict(args) - pi = frappe.get_doc({ - "doctype": "Purchase Invoice", - "company": args.company, - "currency": erpnext.get_company_currency(args.company), - "naming_series": args.naming_series, - "supplier": supplier_name, - "is_return": args.is_return, - "posting_date": today(), - "bill_no": args.bill_no, - "buying_price_list": args.buying_price_list, - "bill_date": args.bill_date, - "destination_code": args.destination_code, - "document_type": args.document_type, - "disable_rounded_total": 1, - "items": args["items"], - "taxes": args["taxes"] - }) + pi = frappe.get_doc( + { + "doctype": "Purchase Invoice", + "company": args.company, + "currency": erpnext.get_company_currency(args.company), + "naming_series": args.naming_series, + "supplier": supplier_name, + "is_return": args.is_return, + "posting_date": today(), + "bill_no": args.bill_no, + "buying_price_list": args.buying_price_list, + "bill_date": args.bill_date, + "destination_code": args.destination_code, + "document_type": args.document_type, + "disable_rounded_total": 1, + "items": args["items"], + "taxes": args["taxes"], + } + ) try: pi.set_missing_values() pi.insert(ignore_mandatory=True) - #if discount exists in file, apply any discount on grand total + # if discount exists in file, apply any discount on grand total if args.total_discount > 0: pi.apply_discount_on = "Grand Total" pi.discount_amount = args.total_discount pi.save() - #adjust payment amount to match with grand total calculated + # adjust payment amount to match with grand total calculated calc_total = 0 adj = 0 for term in args.terms: @@ -352,31 +375,37 @@ def create_purchase_invoice(supplier_name, file_name, args, name): adj = calc_total - flt(pi.grand_total) pi.payment_schedule = [] for term in args.terms: - pi.append('payment_schedule',{"mode_of_payment_code": term["mode_of_payment_code"], - "bank_account_iban": term["bank_account_iban"], - "due_date": term["due_date"], - "payment_amount": flt(term["payment_amount"]) - adj }) + pi.append( + "payment_schedule", + { + "mode_of_payment_code": term["mode_of_payment_code"], + "bank_account_iban": term["bank_account_iban"], + "due_date": term["due_date"], + "payment_amount": flt(term["payment_amount"]) - adj, + }, + ) adj = 0 pi.imported_grand_total = calc_total pi.save() return pi.name except Exception as e: frappe.db.set_value("Import Supplier Invoice", name, "status", "Error") - frappe.log_error(message=e, - title="Create Purchase Invoice: " + args.get("bill_no") + "File Name: " + file_name) + frappe.log_error( + message=e, title="Create Purchase Invoice: " + args.get("bill_no") + "File Name: " + file_name + ) return None + def get_country(code): - existing_country_name = frappe.db.get_value("Country", - filters={"code": code}, fieldname="name") + existing_country_name = frappe.db.get_value("Country", filters={"code": code}, fieldname="name") if existing_country_name: return existing_country_name else: frappe.throw(_("Country Code in File does not match with country code set up in the system")) + def create_uom(uom): - existing_uom = frappe.db.get_value("UOM", - filters={"uom_name": uom}, fieldname="uom_name") + existing_uom = frappe.db.get_value("UOM", filters={"uom_name": uom}, fieldname="uom_name") if existing_uom: return existing_uom else: diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index f14888189a0..cc223e91bc8 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -21,24 +21,34 @@ class LowerDeductionCertificate(Document): fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - if not (fiscal_year.year_start_date <= getdate(self.valid_from) \ - <= fiscal_year.year_end_date): + if not (fiscal_year.year_start_date <= getdate(self.valid_from) <= fiscal_year.year_end_date): frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) - if not (fiscal_year.year_start_date <= getdate(self.valid_upto) \ - <= fiscal_year.year_end_date): + if not (fiscal_year.year_start_date <= getdate(self.valid_upto) <= fiscal_year.year_end_date): frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) def validate_supplier_against_tax_category(self): - duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', - {'supplier': self.supplier, 'tax_withholding_category': self.tax_withholding_category, 'name': ("!=", self.name)}, - ['name', 'valid_from', 'valid_upto'], as_dict=True) + duplicate_certificate = frappe.db.get_value( + "Lower Deduction Certificate", + { + "supplier": self.supplier, + "tax_withholding_category": self.tax_withholding_category, + "name": ("!=", self.name), + }, + ["name", "valid_from", "valid_upto"], + as_dict=True, + ) if duplicate_certificate and self.are_dates_overlapping(duplicate_certificate): - certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate.name) - frappe.throw(_("There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period.") - .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.tax_withholding_category))) + certificate_link = get_link_to_form("Lower Deduction Certificate", duplicate_certificate.name) + frappe.throw( + _( + "There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period." + ).format( + certificate_link, frappe.bold(self.supplier), frappe.bold(self.tax_withholding_category) + ) + ) - def are_dates_overlapping(self,duplicate_certificate): + def are_dates_overlapping(self, duplicate_certificate): valid_from = duplicate_certificate.valid_from valid_upto = duplicate_certificate.valid_upto if valid_from <= getdate(self.valid_from) <= valid_upto: 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 dc3ee6f28e2..6cfa47ed67e 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 @@ -20,26 +20,34 @@ class TaxExemption80GCertificate(Document): self.set_title() def validate_duplicates(self): - if self.recipient == 'Donor': - certificate = frappe.db.exists(self.doctype, { - 'donation': self.donation, - 'name': ('!=', self.name) - }) + if self.recipient == "Donor": + certificate = frappe.db.exists( + self.doctype, {"donation": self.donation, "name": ("!=", self.name)} + ) if certificate: - frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format( - get_link_to_form(self.doctype, certificate), frappe.bold(self.donation) - ), title=_('Duplicate Certificate')) + frappe.throw( + _("An 80G Certificate {0} already exists for the donation {1}").format( + get_link_to_form(self.doctype, certificate), frappe.bold(self.donation) + ), + title=_("Duplicate Certificate"), + ) def validate_company_details(self): - fields = ['company_80g_number', 'with_effect_from', 'pan_details'] - company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True) + fields = ["company_80g_number", "with_effect_from", "pan_details"] + company_details = frappe.db.get_value("Company", self.company, fields, as_dict=True) if not company_details.company_80g_number: - frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'), - get_link_to_form('Company', self.company))) + frappe.throw( + _("Please set the {0} for company {1}").format( + frappe.bold("80G Number"), get_link_to_form("Company", self.company) + ) + ) if not company_details.pan_details: - frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'), - get_link_to_form('Company', self.company))) + frappe.throw( + _("Please set the {0} for company {1}").format( + frappe.bold("PAN Number"), get_link_to_form("Company", self.company) + ) + ) @frappe.whitelist() def set_company_address(self): @@ -48,7 +56,7 @@ class TaxExemption80GCertificate(Document): self.company_address_display = address.company_address_display def calculate_total(self): - if self.recipient == 'Donor': + if self.recipient == "Donor": return total = 0 @@ -57,7 +65,7 @@ class TaxExemption80GCertificate(Document): self.total = total def set_title(self): - if self.recipient == 'Member': + if self.recipient == "Member": self.title = self.member_name else: self.title = self.donor_name @@ -65,30 +73,38 @@ class TaxExemption80GCertificate(Document): @frappe.whitelist() def get_payments(self): if not self.member: - frappe.throw(_('Please select a Member first.')) + frappe.throw(_("Please select a Member first.")) fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) - memberships = frappe.db.get_all('Membership', { - 'member': self.member, - 'from_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') + memberships = frappe.db.get_all( + "Membership", + { + "member": self.member, + "from_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", + ) if not memberships: - frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member)) + frappe.msgprint(_("No Membership Payments found against the Member {0}").format(self.member)) total = 0 self.payments = [] for doc in memberships: - self.append('payments', { - 'date': doc.from_date, - 'amount': doc.amount, - 'invoice_id': doc.invoice, - 'payment_id': doc.payment_id, - 'membership': doc.name - }) + self.append( + "payments", + { + "date": doc.from_date, + "amount": doc.amount, + "invoice_id": doc.invoice, + "payment_id": doc.payment_id, + "membership": doc.name, + }, + ) total += flt(doc.amount) self.total = total diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py index 4e328931ec1..c247f3e6409 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py @@ -19,43 +19,37 @@ from erpnext.non_profit.doctype.membership.test_membership import make_membershi class TestTaxExemption80GCertificate(unittest.TestCase): def setUp(self): - frappe.db.sql('delete from `tabTax Exemption 80G Certificate`') - frappe.db.sql('delete from `tabMembership`') + frappe.db.sql("delete from `tabTax Exemption 80G Certificate`") + frappe.db.sql("delete from `tabMembership`") create_donor_type() - settings = frappe.get_doc('Non Profit Settings') - settings.company = '_Test Company' - settings.donation_company = '_Test Company' - settings.default_donor_type = '_Test Donor' - settings.creation_user = 'Administrator' + settings = frappe.get_doc("Non Profit Settings") + settings.company = "_Test Company" + settings.donation_company = "_Test Company" + settings.default_donor_type = "_Test Donor" + settings.creation_user = "Administrator" settings.save() - company = frappe.get_doc('Company', '_Test Company') - company.pan_details = 'BBBTI3374C' - company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087' + company = frappe.get_doc("Company", "_Test Company") + company.pan_details = "BBBTI3374C" + company.company_80g_number = "NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087" company.with_effect_from = getdate() company.save() def test_duplicate_donation_certificate(self): donor = create_donor() create_mode_of_payment() - payment = frappe._dict({ - 'amount': 100, # rzp sends data in paise - 'method': 'Debit Card', - 'id': 'pay_MeXAmsgeKOhq7O' - }) + payment = frappe._dict( + {"amount": 100, "method": "Debit Card", "id": "pay_MeXAmsgeKOhq7O"} # rzp sends data in paise + ) donation = create_razorpay_donation(donor, payment) - args = frappe._dict({ - 'recipient': 'Donor', - 'donor': donor.name, - 'donation': donation.name - }) + args = frappe._dict({"recipient": "Donor", "donor": donor.name, "donation": donation.name}) certificate = create_80g_certificate(args) certificate.insert() # check company details - self.assertEqual(certificate.company_pan_number, 'BBBTI3374C') - self.assertEqual(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087') + self.assertEqual(certificate.company_pan_number, "BBBTI3374C") + self.assertEqual(certificate.company_80g_number, "NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087") # check donation details self.assertEqual(certificate.amount, donation.amount) @@ -68,22 +62,24 @@ class TestTaxExemption80GCertificate(unittest.TestCase): plan = setup_membership() # make test member - member_doc = create_member(frappe._dict({ - 'fullname': "_Test_Member", - 'email': "_test_member_erpnext@example.com", - 'plan_id': plan.name - })) + member_doc = create_member( + frappe._dict( + {"fullname": "_Test_Member", "email": "_test_member_erpnext@example.com", "plan_id": plan.name} + ) + ) member_doc.make_customer_and_link() member = member_doc.name - membership = make_membership(member, { "from_date": getdate() }) + membership = make_membership(member, {"from_date": getdate()}) invoice = membership.generate_invoice(save=True) - args = frappe._dict({ - 'recipient': 'Member', - 'member': member, - 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name') - }) + args = frappe._dict( + { + "recipient": "Member", + "member": member, + "fiscal_year": get_fiscal_year(getdate(), as_dict=True).get("name"), + } + ) certificate = create_80g_certificate(args) certificate.get_payments() certificate.insert() @@ -94,12 +90,14 @@ class TestTaxExemption80GCertificate(unittest.TestCase): def create_80g_certificate(args): - certificate = frappe.get_doc({ - 'doctype': 'Tax Exemption 80G Certificate', - 'recipient': args.recipient, - 'date': getdate(), - 'company': '_Test Company' - }) + certificate = frappe.get_doc( + { + "doctype": "Tax Exemption 80G Certificate", + "recipient": args.recipient, + "date": getdate(), + "company": "_Test Company", + } + ) certificate.update(args) diff --git a/erpnext/regional/france/setup.py b/erpnext/regional/france/setup.py index 5b55a444bc0..da772d6b77e 100644 --- a/erpnext/regional/france/setup.py +++ b/erpnext/regional/france/setup.py @@ -10,24 +10,21 @@ def setup(company=None, patch=True): make_custom_fields() add_custom_roles_for_reports() + def make_custom_fields(): custom_fields = { - 'Company': [ - dict(fieldname='siren_number', label='SIREN Number', - fieldtype='Data', insert_after='website') + "Company": [ + dict(fieldname="siren_number", label="SIREN Number", fieldtype="Data", insert_after="website") ] } create_custom_fields(custom_fields) -def add_custom_roles_for_reports(): - report_name = 'Fichier des Ecritures Comptables [FEC]' - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts Manager') - ] - )).insert() +def add_custom_roles_for_reports(): + report_name = "Fichier des Ecritures Comptables [FEC]" + + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict(doctype="Custom Role", report=report_name, roles=[dict(role="Accounts Manager")]) + ).insert() diff --git a/erpnext/regional/france/utils.py b/erpnext/regional/france/utils.py index 841316586dc..65dfd2db916 100644 --- a/erpnext/regional/france/utils.py +++ b/erpnext/regional/france/utils.py @@ -2,8 +2,7 @@ # For license information, please see license.txt - # don't remove this function it is used in tests def test_method(): - '''test function''' - return 'overridden' + """test function""" + return "overridden" diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py index 35d14135ba5..b8e66c3ece3 100644 --- a/erpnext/regional/germany/setup.py +++ b/erpnext/regional/germany/setup.py @@ -1,4 +1,3 @@ - import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields @@ -10,9 +9,14 @@ def setup(company=None, patch=True): def make_custom_fields(): custom_fields = { - 'Party Account': [ - dict(fieldname='debtor_creditor_number', label='Debtor/Creditor Number', - fieldtype='Data', insert_after='account', translatable=0) + "Party Account": [ + dict( + fieldname="debtor_creditor_number", + label="Debtor/Creditor Number", + fieldtype="Data", + insert_after="account", + translatable=0, + ) ] } @@ -21,12 +25,11 @@ def make_custom_fields(): def add_custom_roles_for_reports(): """Add Access Control to UAE VAT 201.""" - if not frappe.db.get_value('Custom Role', dict(report='DATEV')): - frappe.get_doc(dict( - doctype='Custom Role', - report='DATEV', - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report="DATEV")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="DATEV", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() diff --git a/erpnext/regional/germany/utils/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py index be3d7a3e542..6b2feb08855 100644 --- a/erpnext/regional/germany/utils/datev/datev_constants.py +++ b/erpnext/regional/germany/utils/datev/datev_constants.py @@ -186,7 +186,7 @@ TRANSACTION_COLUMNS = [ # Steuersatz für Steuerschlüssel "Steuersatz", # Beispiel: DE für Deutschland - "Land" + "Land", ] DEBTOR_CREDITOR_COLUMNS = [ @@ -447,7 +447,7 @@ DEBTOR_CREDITOR_COLUMNS = [ "Mahnfrist 1", "Mahnfrist 2", "Mahnfrist 3", - "Letzte Frist" + "Letzte Frist", ] ACCOUNT_NAME_COLUMNS = [ @@ -457,10 +457,11 @@ ACCOUNT_NAME_COLUMNS = [ "Kontenbeschriftung", # Language of the account name # "de-DE" or "en-GB" - "Sprach-ID" + "Sprach-ID", ] -class DataCategory(): + +class DataCategory: """Field of the CSV Header.""" @@ -469,7 +470,8 @@ class DataCategory(): TRANSACTIONS = "21" POSTING_TEXT_CONSTANTS = "67" -class FormatName(): + +class FormatName: """Field of the CSV Header, corresponds to DataCategory.""" @@ -478,19 +480,22 @@ class FormatName(): TRANSACTIONS = "Buchungsstapel" POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten" -class Transactions(): + +class Transactions: DATA_CATEGORY = DataCategory.TRANSACTIONS FORMAT_NAME = FormatName.TRANSACTIONS FORMAT_VERSION = "9" COLUMNS = TRANSACTION_COLUMNS -class DebtorsCreditors(): + +class DebtorsCreditors: DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS FORMAT_NAME = FormatName.DEBTORS_CREDITORS FORMAT_VERSION = "5" COLUMNS = DEBTOR_CREDITOR_COLUMNS -class AccountNames(): + +class AccountNames: DATA_CATEGORY = DataCategory.ACCOUNT_NAMES FORMAT_NAME = FormatName.ACCOUNT_NAMES FORMAT_VERSION = "2" diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py index d46abe91873..9d895e1afb0 100644 --- a/erpnext/regional/germany/utils/datev/datev_csv.py +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -30,133 +30,137 @@ def get_datev_csv(data, filters, csv_class): result = empty_df.append(data_df, sort=True) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: - result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) + result["Belegdatum"] = pd.to_datetime(result["Belegdatum"]) - result['Beleginfo - Inhalt 6'] = pd.to_datetime(result['Beleginfo - Inhalt 6']) - result['Beleginfo - Inhalt 6'] = result['Beleginfo - Inhalt 6'].dt.strftime('%d%m%Y') + result["Beleginfo - Inhalt 6"] = pd.to_datetime(result["Beleginfo - Inhalt 6"]) + result["Beleginfo - Inhalt 6"] = result["Beleginfo - Inhalt 6"].dt.strftime("%d%m%Y") - result['Fälligkeit'] = pd.to_datetime(result['Fälligkeit']) - result['Fälligkeit'] = result['Fälligkeit'].dt.strftime('%d%m%y') + result["Fälligkeit"] = pd.to_datetime(result["Fälligkeit"]) + result["Fälligkeit"] = result["Fälligkeit"].dt.strftime("%d%m%y") - result.sort_values(by='Belegdatum', inplace=True, kind='stable', ignore_index=True) + result.sort_values(by="Belegdatum", inplace=True, kind="stable", ignore_index=True) if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: - result['Sprach-ID'] = 'de-DE' + result["Sprach-ID"] = "de-DE" data = result.to_csv( # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 - sep=str(';'), + sep=str(";"), # European decimal seperator - decimal=',', + decimal=",", # Windows "ANSI" encoding - encoding='latin_1', + encoding="latin_1", # format date as DDMM - date_format='%d%m', + date_format="%d%m", # Windows line terminator - line_terminator='\r\n', + line_terminator="\r\n", # Do not number rows index=False, # Use all columns defined above columns=csv_class.COLUMNS, # Quote most fields, even currency values with "," separator - quoting=QUOTE_NONNUMERIC + quoting=QUOTE_NONNUMERIC, ) - data = data.encode('latin_1', errors='replace') + data = data.encode("latin_1", errors="replace") header = get_header(filters, csv_class) - header = ';'.join(header).encode('latin_1', errors='replace') + header = ";".join(header).encode("latin_1", errors="replace") # 1st Row: Header with meta data # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. # 3rd - nth Row: Data (Nutzdaten) - return header + b'\r\n' + data + return header + b"\r\n" + data def get_header(filters, csv_class): - description = filters.get('voucher_type', csv_class.FORMAT_NAME) - company = filters.get('company') - datev_settings = frappe.get_doc('DATEV Settings', {'client': company}) - default_currency = frappe.get_value('Company', company, 'default_currency') - coa = frappe.get_value('Company', company, 'chart_of_accounts') - coa_short_code = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + description = filters.get("voucher_type", csv_class.FORMAT_NAME) + company = filters.get("company") + datev_settings = frappe.get_doc("DATEV Settings", {"client": company}) + default_currency = frappe.get_value("Company", company, "default_currency") + coa = frappe.get_value("Company", company, "chart_of_accounts") + coa_short_code = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") header = [ # DATEV format - # "DTVF" = created by DATEV software, - # "EXTF" = created by other software + # "DTVF" = created by DATEV software, + # "EXTF" = created by other software '"EXTF"', # version of the DATEV format - # 141 = 1.41, - # 510 = 5.10, - # 720 = 7.20 - '700', + # 141 = 1.41, + # 510 = 5.10, + # 720 = 7.20 + "700", csv_class.DATA_CATEGORY, '"%s"' % csv_class.FORMAT_NAME, # Format version (regarding format name) csv_class.FORMAT_VERSION, # Generated on - datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '000', + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + "000", # Imported on -- stays empty - '', + "", # Origin. Any two symbols, will be replaced by "SV" on import. '"EN"', # I = Exported by '"%s"' % frappe.session.user, # J = Imported by -- stays empty - '', + "", # K = Tax consultant number (Beraternummer) - datev_settings.get('consultant_number', '0000000'), + datev_settings.get("consultant_number", "0000000"), # L = Tax client number (Mandantennummer) - datev_settings.get('client_number', '00000'), + datev_settings.get("client_number", "00000"), # M = Start of the fiscal year (Wirtschaftsjahresbeginn) - frappe.utils.formatdate(filters.get('fiscal_year_start'), 'yyyyMMdd'), + frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) - str(filters.get('account_number_length', 4)), + str(filters.get("account_number_length", 4)), # O = Transaction batch start date (YYYYMMDD) - frappe.utils.formatdate(filters.get('from_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + frappe.utils.formatdate(filters.get("from_date"), "yyyyMMdd") + if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS + else "", # P = Transaction batch end date (YYYYMMDD) - frappe.utils.formatdate(filters.get('to_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + frappe.utils.formatdate(filters.get("to_date"), "yyyyMMdd") + if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS + else "", # Q = Description (for example, "Sales Invoice") Max. 30 chars - '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # R = Diktatkürzel - '', + "", # S = Buchungstyp - # 1 = Transaction batch (Finanzbuchführung), - # 2 = Annual financial statement (Jahresabschluss) - '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # 1 = Transaction batch (Finanzbuchführung), + # 2 = Annual financial statement (Jahresabschluss) + "1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # T = Rechnungslegungszweck - # 0 oder leer = vom Rechnungslegungszweck unabhängig - # 50 = Handelsrecht - # 30 = Steuerrecht - # 64 = IFRS - # 40 = Kalkulatorik - # 11 = Reserviert - # 12 = Reserviert - '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # 0 oder leer = vom Rechnungslegungszweck unabhängig + # 50 = Handelsrecht + # 30 = Steuerrecht + # 64 = IFRS + # 40 = Kalkulatorik + # 11 = Reserviert + # 12 = Reserviert + "0" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # U = Festschreibung # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" - '0', + "0", # V = Default currency, for example, "EUR" - '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # reserviert - '', + "", # Derivatskennzeichen - '', + "", # reserviert - '', + "", # reserviert - '', + "", # SKR '"%s"' % coa_short_code, # Branchen-Lösungs-ID - '', + "", # reserviert - '', + "", # reserviert - '', + "", # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) - '' + "", ] return header @@ -171,12 +175,12 @@ def zip_and_download(zip_filename, csv_files): """ zip_buffer = BytesIO() - zip_file = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + zip_file = zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) for csv_file in csv_files: - zip_file.writestr(csv_file.get('file_name'), csv_file.get('csv_data')) + zip_file.writestr(csv_file.get("file_name"), csv_file.get("csv_data")) zip_file.close() - frappe.response['filecontent'] = zip_buffer.getvalue() - frappe.response['filename'] = zip_filename - frappe.response['type'] = 'binary' + frappe.response["filecontent"] = zip_buffer.getvalue() + frappe.response["filename"] = zip_filename + frappe.response["type"] = "binary" diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 2dc762f0ebd..b547d39281c 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -1,85 +1,84 @@ - from six import iteritems states = [ - '', - 'Andaman and Nicobar Islands', - 'Andhra Pradesh', - 'Arunachal Pradesh', - 'Assam', - 'Bihar', - 'Chandigarh', - 'Chhattisgarh', - 'Dadra and Nagar Haveli and Daman and Diu', - 'Delhi', - 'Goa', - 'Gujarat', - 'Haryana', - 'Himachal Pradesh', - 'Jammu and Kashmir', - 'Jharkhand', - 'Karnataka', - 'Kerala', - 'Ladakh', - 'Lakshadweep Islands', - 'Madhya Pradesh', - 'Maharashtra', - 'Manipur', - 'Meghalaya', - 'Mizoram', - 'Nagaland', - 'Odisha', - 'Other Territory', - 'Pondicherry', - 'Punjab', - 'Rajasthan', - 'Sikkim', - 'Tamil Nadu', - 'Telangana', - 'Tripura', - 'Uttar Pradesh', - 'Uttarakhand', - 'West Bengal', + "", + "Andaman and Nicobar Islands", + "Andhra Pradesh", + "Arunachal Pradesh", + "Assam", + "Bihar", + "Chandigarh", + "Chhattisgarh", + "Dadra and Nagar Haveli and Daman and Diu", + "Delhi", + "Goa", + "Gujarat", + "Haryana", + "Himachal Pradesh", + "Jammu and Kashmir", + "Jharkhand", + "Karnataka", + "Kerala", + "Ladakh", + "Lakshadweep Islands", + "Madhya Pradesh", + "Maharashtra", + "Manipur", + "Meghalaya", + "Mizoram", + "Nagaland", + "Odisha", + "Other Territory", + "Pondicherry", + "Punjab", + "Rajasthan", + "Sikkim", + "Tamil Nadu", + "Telangana", + "Tripura", + "Uttar Pradesh", + "Uttarakhand", + "West Bengal", ] state_numbers = { - "Andaman and Nicobar Islands": "35", - "Andhra Pradesh": "37", - "Arunachal Pradesh": "12", - "Assam": "18", - "Bihar": "10", - "Chandigarh": "04", - "Chhattisgarh": "22", - "Dadra and Nagar Haveli and Daman and Diu": "26", - "Delhi": "07", - "Goa": "30", - "Gujarat": "24", - "Haryana": "06", - "Himachal Pradesh": "02", - "Jammu and Kashmir": "01", - "Jharkhand": "20", - "Karnataka": "29", - "Kerala": "32", - "Ladakh": "38", - "Lakshadweep Islands": "31", - "Madhya Pradesh": "23", - "Maharashtra": "27", - "Manipur": "14", - "Meghalaya": "17", - "Mizoram": "15", - "Nagaland": "13", - "Odisha": "21", - "Other Territory": "97", - "Pondicherry": "34", - "Punjab": "03", - "Rajasthan": "08", - "Sikkim": "11", - "Tamil Nadu": "33", - "Telangana": "36", - "Tripura": "16", - "Uttar Pradesh": "09", - "Uttarakhand": "05", - "West Bengal": "19", + "Andaman and Nicobar Islands": "35", + "Andhra Pradesh": "37", + "Arunachal Pradesh": "12", + "Assam": "18", + "Bihar": "10", + "Chandigarh": "04", + "Chhattisgarh": "22", + "Dadra and Nagar Haveli and Daman and Diu": "26", + "Delhi": "07", + "Goa": "30", + "Gujarat": "24", + "Haryana": "06", + "Himachal Pradesh": "02", + "Jammu and Kashmir": "01", + "Jharkhand": "20", + "Karnataka": "29", + "Kerala": "32", + "Ladakh": "38", + "Lakshadweep Islands": "31", + "Madhya Pradesh": "23", + "Maharashtra": "27", + "Manipur": "14", + "Meghalaya": "17", + "Mizoram": "15", + "Nagaland": "13", + "Odisha": "21", + "Other Territory": "97", + "Pondicherry": "34", + "Punjab": "03", + "Rajasthan": "08", + "Sikkim": "11", + "Tamil Nadu": "33", + "Telangana": "36", + "Tripura": "16", + "Uttar Pradesh": "09", + "Uttarakhand": "05", + "West Bengal": "19", } number_state_mapping = {v: k for k, v in iteritems(state_numbers)} diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index cfad29beeb6..990fe25e59f 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -40,121 +40,157 @@ def validate_eligibility(doc): if isinstance(doc, six.string_types): doc = json.loads(doc) - invalid_doctype = doc.get('doctype') != 'Sales Invoice' + invalid_doctype = doc.get("doctype") != "Sales Invoice" if invalid_doctype: return False - einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable')) + einvoicing_enabled = cint(frappe.db.get_single_value("E Invoice Settings", "enable")) if not einvoicing_enabled: return False - einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' - if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): + einvoicing_eligible_from = ( + frappe.db.get_single_value("E Invoice Settings", "applicable_from") or "2021-04-01" + ) + if getdate(doc.get("posting_date")) < getdate(einvoicing_eligible_from): return False - invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') }) - invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] - company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') + invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")}) + invalid_supply_type = doc.get("gst_category") not in [ + "Registered Regular", + "SEZ", + "Overseas", + "Deemed Export", + ] + company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin") # if export invoice, then taxes can be empty # invoice can only be ineligible if no taxes applied and is not an export invoice - no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' - has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) + no_taxes_applied = not doc.get("taxes") and not doc.get("gst_category") == "Overseas" + has_non_gst_item = any(d for d in doc.get("items", []) if d.get("is_non_gst")) - if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item: + if ( + invalid_company + or invalid_supply_type + or company_transaction + or no_taxes_applied + or has_non_gst_item + ): return False return True + def validate_einvoice_fields(doc): invoice_eligible = validate_eligibility(doc) if not invoice_eligible: return - if doc.docstatus == 0 and doc._action == 'save': + if doc.docstatus == 0 and doc._action == "save": if doc.irn: - frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + frappe.throw(_("You cannot edit the invoice after generating IRN"), title=_("Edit Not Allowed")) if len(doc.name) > 16: raise_document_name_too_long_error() - doc.einvoice_status = 'Pending' + doc.einvoice_status = "Pending" - elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: - frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) + elif doc.docstatus == 1 and doc._action == "submit" and not doc.irn: + frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN")) + + elif doc.irn and doc.docstatus == 2 and doc._action == "cancel" and not doc.irn_cancelled: + frappe.throw( + _("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed") + ) - elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: - frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) def raise_document_name_too_long_error(): - title = _('Document ID Too Long') - msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice') - msg += ', ' - msg += _('document id {} exceed 16 letters.').format(bold(_('should not'))) - msg += '

    ' - msg += _('You must {} your {} in order to have document id of {} length 16.').format( - bold(_('modify')), bold(_('naming series')), bold(_('maximum')) + title = _("Document ID Too Long") + msg = _("As you have E-Invoicing enabled, to be able to generate IRN for this invoice") + msg += ", " + msg += _("document id {} exceed 16 letters.").format(bold(_("should not"))) + msg += "

    " + msg += _("You must {} your {} in order to have document id of {} length 16.").format( + bold(_("modify")), bold(_("naming series")), bold(_("maximum")) ) - msg += _('Please account for ammended documents too.') + msg += _("Please account for ammended documents too.") frappe.throw(msg, title=title) + def read_json(name): - file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) - with open(file_path, 'r') as f: + file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name)) + with open(file_path, "r") as f: return cstr(f.read()) + def get_transaction_details(invoice): - supply_type = '' - if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' - elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' - elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' - elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + supply_type = "" + if invoice.gst_category == "Registered Regular": + supply_type = "B2B" + elif invoice.gst_category == "SEZ": + supply_type = "SEZWOP" + elif invoice.gst_category == "Overseas": + supply_type = "EXPWOP" + elif invoice.gst_category == "Deemed Export": + supply_type = "DEXP" if not supply_type: - rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') - frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), - title=_('Invalid Supply Type')) + rr, sez, overseas, export = ( + bold("Registered Regular"), + bold("SEZ"), + bold("Overseas"), + bold("Deemed Export"), + ) + frappe.throw( + _("GST category should be one of {}, {}, {}, {}").format(rr, sez, overseas, export), + title=_("Invalid Supply Type"), + ) + + return frappe._dict( + dict(tax_scheme="GST", supply_type=supply_type, reverse_charge=invoice.reverse_charge) + ) - return frappe._dict(dict( - tax_scheme='GST', - supply_type=supply_type, - reverse_charge=invoice.reverse_charge - )) def get_doc_details(invoice): - if getdate(invoice.posting_date) < getdate('2021-01-01'): - frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed')) + if getdate(invoice.posting_date) < getdate("2021-01-01"): + frappe.throw( + _("IRN generation is not allowed for invoices dated before 1st Jan 2021"), + title=_("Not Allowed"), + ) - invoice_type = 'CRN' if invoice.is_return else 'INV' + invoice_type = "CRN" if invoice.is_return else "INV" invoice_name = invoice.name - invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') + invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy") + + return frappe._dict( + dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date) + ) - return frappe._dict(dict( - invoice_type=invoice_type, - invoice_name=invoice_name, - invoice_date=invoice_date - )) def validate_address_fields(address, skip_gstin_validation): - if ((not address.gstin and not skip_gstin_validation) + if ( + (not address.gstin and not skip_gstin_validation) or not address.city or not address.pincode or not address.address_title or not address.address_line1 - or not address.gst_state_number): + or not address.gst_state_number + ): frappe.throw( - msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name), - title=_('Missing Address Fields') + msg=_( + "Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again." + ).format(address.name), + title=_("Missing Address Fields"), ) if address.address_line2 and len(address.address_line2) < 2: # to prevent "The field Address 2 must be a string with a minimum length of 3 and a maximum length of 100" address.address_line2 = "" + def get_party_details(address_name, skip_gstin_validation=False): - addr = frappe.get_doc('Address', address_name) + addr = frappe.get_doc("Address", address_name) validate_address_fields(addr, skip_gstin_validation) @@ -162,44 +198,53 @@ def get_party_details(address_name, skip_gstin_validation=False): # according to einvoice standard addr.pincode = 999999 - party_address_details = frappe._dict(dict( - legal_name=sanitize_for_json(addr.address_title), - location=sanitize_for_json(addr.city), - pincode=addr.pincode, gstin=addr.gstin, - state_code=addr.gst_state_number, - address_line1=sanitize_for_json(addr.address_line1), - address_line2=sanitize_for_json(addr.address_line2) - )) + party_address_details = frappe._dict( + dict( + legal_name=sanitize_for_json(addr.address_title), + location=sanitize_for_json(addr.city), + pincode=addr.pincode, + gstin=addr.gstin, + state_code=addr.gst_state_number, + address_line1=sanitize_for_json(addr.address_line1), + address_line2=sanitize_for_json(addr.address_line2), + ) + ) return party_address_details + def get_overseas_address_details(address_name): address_title, address_line1, address_line2, city = frappe.db.get_value( - 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] + "Address", address_name, ["address_title", "address_line1", "address_line2", "city"] ) if not address_title or not address_line1 or not city: frappe.throw( - msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format( - get_link_to_form('Address', address_name) - ), - title=_('Missing Address Fields') + msg=_( + "Address lines and city is mandatory for address {}. Please set them and try again." + ).format(get_link_to_form("Address", address_name)), + title=_("Missing Address Fields"), ) - return frappe._dict(dict( - gstin='URP', - legal_name=sanitize_for_json(address_title), - location=city, - address_line1=sanitize_for_json(address_line1), - address_line2=sanitize_for_json(address_line2), - pincode=999999, state_code=96, place_of_supply=96 - )) + return frappe._dict( + dict( + gstin="URP", + legal_name=sanitize_for_json(address_title), + location=city, + address_line1=sanitize_for_json(address_line1), + address_line2=sanitize_for_json(address_line2), + pincode=999999, + state_code=96, + place_of_supply=96, + ) + ) + def get_item_list(invoice): item_list = [] for d in invoice.items: - einvoice_item_schema = read_json('einv_item_template') + einvoice_item_schema = read_json("einv_item_template") item = frappe._dict({}) item.update(d.as_dict()) @@ -215,29 +260,41 @@ def get_item_list(invoice): item.taxable_value = abs(item.taxable_value) item.discount_amount = 0 - item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N' + item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N" item.serial_no = "" item = update_item_taxes(invoice, item) item.total_value = abs( - item.taxable_value + item.igst_amount + item.sgst_amount + - item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges + item.taxable_value + + item.igst_amount + + item.sgst_amount + + item.cgst_amount + + item.cess_amount + + item.cess_nadv_amount + + item.other_charges ) einv_item = einvoice_item_schema.format(item=item) item_list.append(einv_item) - return ', '.join(item_list) + return ", ".join(item_list) + def update_item_taxes(invoice, item): gst_accounts = get_gst_accounts(invoice.company) gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] for attr in [ - 'tax_rate', 'cess_rate', 'cess_nadv_amount', - 'cgst_amount', 'sgst_amount', 'igst_amount', - 'cess_amount', 'cess_nadv_amount', 'other_charges' - ]: + "tax_rate", + "cess_rate", + "cess_nadv_amount", + "cgst_amount", + "sgst_amount", + "igst_amount", + "cess_amount", + "cess_nadv_amount", + "other_charges", + ]: item[attr] = 0 for t in invoice.taxes: @@ -252,35 +309,39 @@ def update_item_taxes(invoice, item): if t.account_head in gst_accounts.cess_account: item_tax_amount_after_discount = item_tax_detail[1] - if t.charge_type == 'On Item Quantity': + if t.charge_type == "On Item Quantity": item.cess_nadv_amount += abs(item_tax_amount_after_discount) else: item.cess_rate += item_tax_rate item.cess_amount += abs(item_tax_amount_after_discount) - for tax_type in ['igst', 'cgst', 'sgst']: - if t.account_head in gst_accounts[f'{tax_type}_account']: + for tax_type in ["igst", "cgst", "sgst"]: + if t.account_head in gst_accounts[f"{tax_type}_account"]: item.tax_rate += item_tax_rate - item[f'{tax_type}_amount'] += abs(item_tax_amount) + item[f"{tax_type}_amount"] += abs(item_tax_amount) else: # TODO: other charges per item pass return item + def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) - invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) + invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")])) invoice_value_details.invoice_discount_amt = 0 invoice_value_details.round_off = invoice.base_rounding_adjustment - invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) + invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs( + invoice.base_grand_total + ) invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) return invoice_value_details + def update_invoice_taxes(invoice, invoice_value_details): gst_accounts = get_gst_accounts(invoice.company) gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] @@ -299,90 +360,117 @@ def update_invoice_taxes(invoice, invoice_value_details): # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - for tax_type in ['igst', 'cgst', 'sgst']: - if t.account_head in gst_accounts[f'{tax_type}_account']: + for tax_type in ["igst", "cgst", "sgst"]: + if t.account_head in gst_accounts[f"{tax_type}_account"]: - invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount) + invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount) update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) else: invoice_value_details.total_other_charges += abs(tax_amount) return invoice_value_details -def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows): + +def update_other_charges( + tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows +): prev_row_id = cint(tax_row.row_id) - 1 if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows: - if tax_row.charge_type == 'On Previous Row Amount': - amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount + if tax_row.charge_type == "On Previous Row Amount": + amount = invoice.get("taxes")[prev_row_id].tax_amount_after_discount_amount invoice_value_details.total_other_charges -= abs(amount) considered_rows.append(prev_row_id) - if tax_row.charge_type == 'On Previous Row Total': - amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total + if tax_row.charge_type == "On Previous Row Total": + amount = invoice.get("taxes")[prev_row_id].base_total - invoice.base_net_total invoice_value_details.total_other_charges -= abs(amount) considered_rows.append(prev_row_id) + def get_payment_details(invoice): payee_name = invoice.company - mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) + mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments]) paid_amount = invoice.base_paid_amount outstanding_amount = invoice.outstanding_amount - return frappe._dict(dict( - payee_name=payee_name, mode_of_payment=mode_of_payment, - paid_amount=paid_amount, outstanding_amount=outstanding_amount - )) + return frappe._dict( + dict( + payee_name=payee_name, + mode_of_payment=mode_of_payment, + paid_amount=paid_amount, + outstanding_amount=outstanding_amount, + ) + ) + def get_return_doc_reference(invoice): - invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') - return frappe._dict(dict( - invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') - )) + invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date") + return frappe._dict( + dict(invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, "dd/mm/yyyy")) + ) + def get_eway_bill_details(invoice): if invoice.is_return: - frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'), - title=_('Invalid Fields')) + frappe.throw( + _( + "E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice." + ), + title=_("Invalid Fields"), + ) + mode_of_transport = {"": "", "Road": "1", "Air": "2", "Rail": "3", "Ship": "4"} + vehicle_type = {"Regular": "R", "Over Dimensional Cargo (ODC)": "O"} - mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } - vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } + return frappe._dict( + dict( + gstin=invoice.gst_transporter_id, + name=invoice.transporter_name, + mode_of_transport=mode_of_transport[invoice.mode_of_transport], + distance=invoice.distance or 0, + document_name=invoice.lr_no, + document_date=format_date(invoice.lr_date, "dd/mm/yyyy"), + vehicle_no=invoice.vehicle_no, + vehicle_type=vehicle_type[invoice.gst_vehicle_type], + ) + ) - return frappe._dict(dict( - gstin=invoice.gst_transporter_id, - name=invoice.transporter_name, - mode_of_transport=mode_of_transport[invoice.mode_of_transport], - distance=invoice.distance or 0, - document_name=invoice.lr_no, - document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), - vehicle_no=invoice.vehicle_no, - vehicle_type=vehicle_type[invoice.gst_vehicle_type] - )) def validate_mandatory_fields(invoice): if not invoice.company_address: frappe.throw( - _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'), - title=_('Missing Fields') + _( + "Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again." + ), + title=_("Missing Fields"), ) if not invoice.customer_address: frappe.throw( - _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'), - title=_('Missing Fields') + _( + "Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again." + ), + title=_("Missing Fields"), ) - if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): + if not frappe.db.get_value("Address", invoice.company_address, "gstin"): frappe.throw( - _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), - title=_('Missing Fields') + _( + "GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address." + ), + title=_("Missing Fields"), ) - if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'): + if invoice.gst_category != "Overseas" and not frappe.db.get_value( + "Address", invoice.customer_address, "gstin" + ): frappe.throw( - _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'), - title=_('Missing Fields') + _( + "GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address." + ), + title=_("Missing Fields"), ) + def validate_totals(einvoice): - item_list = einvoice['ItemList'] - value_details = einvoice['ValDtls'] + item_list = einvoice["ItemList"] + value_details = einvoice["ValDtls"] total_item_ass_value = 0 total_item_cgst_value = 0 @@ -390,39 +478,88 @@ def validate_totals(einvoice): total_item_igst_value = 0 total_item_value = 0 for item in item_list: - total_item_ass_value += flt(item['AssAmt']) - total_item_cgst_value += flt(item['CgstAmt']) - total_item_sgst_value += flt(item['SgstAmt']) - total_item_igst_value += flt(item['IgstAmt']) - total_item_value += flt(item['TotItemVal']) + total_item_ass_value += flt(item["AssAmt"]) + total_item_cgst_value += flt(item["CgstAmt"]) + total_item_sgst_value += flt(item["SgstAmt"]) + total_item_igst_value += flt(item["IgstAmt"]) + total_item_value += flt(item["TotItemVal"]) - if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1: - frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx)) + if ( + abs(flt(item["AssAmt"]) * flt(item["GstRt"]) / 100) + - (flt(item["CgstAmt"]) + flt(item["SgstAmt"]) + flt(item["IgstAmt"])) + > 1 + ): + frappe.throw( + _( + "Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table." + ).format(item.idx) + ) - if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: - frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) + if abs(flt(value_details["AssVal"]) - total_item_ass_value) > 1: + frappe.throw( + _( + "Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction." + ) + ) - if abs(flt(value_details['CgstVal']) + flt(value_details['SgstVal']) - total_item_cgst_value - total_item_sgst_value) > 1: - frappe.throw(_('CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction.')) + if ( + abs( + flt(value_details["CgstVal"]) + + flt(value_details["SgstVal"]) + - total_item_cgst_value + - total_item_sgst_value + ) + > 1 + ): + frappe.throw( + _( + "CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction." + ) + ) - if abs(flt(value_details['IgstVal']) - total_item_igst_value) > 1: - frappe.throw(_('IGST value of all items is not equal to total IGST value. Please review taxes for any correction.')) + if abs(flt(value_details["IgstVal"]) - total_item_igst_value) > 1: + frappe.throw( + _( + "IGST value of all items is not equal to total IGST value. Please review taxes for any correction." + ) + ) - if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1: - frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) + if ( + abs( + flt(value_details["TotInvVal"]) + + flt(value_details["Discount"]) + - flt(value_details["OthChrg"]) + - total_item_value + ) + > 1 + ): + frappe.throw( + _( + "Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction." + ) + ) - calculated_invoice_value = \ - flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ - + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ - + flt(value_details['OthChrg']) - flt(value_details['Discount']) + calculated_invoice_value = ( + flt(value_details["AssVal"]) + + flt(value_details["CgstVal"]) + + flt(value_details["SgstVal"]) + + flt(value_details["IgstVal"]) + + flt(value_details["OthChrg"]) + - flt(value_details["Discount"]) + ) + + if abs(flt(value_details["TotInvVal"]) - calculated_invoice_value) > 1: + frappe.throw( + _( + "Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction." + ) + ) - if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: - frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) def make_einvoice(invoice): validate_mandatory_fields(invoice) - schema = read_json('einv_template') + schema = read_json("einv_template") transaction_details = get_transaction_details(invoice) item_list = get_item_list(invoice) @@ -430,13 +567,13 @@ def make_einvoice(invoice): invoice_value_details = get_invoice_value_details(invoice) seller_details = get_party_details(invoice.company_address) - if invoice.gst_category == 'Overseas': + if invoice.gst_category == "Overseas": buyer_details = get_overseas_address_details(invoice.customer_address) else: buyer_details = get_party_details(invoice.customer_address) place_of_supply = get_place_of_supply(invoice, invoice.doctype) if place_of_supply: - place_of_supply = place_of_supply.split('-')[0] + place_of_supply = place_of_supply.split("-")[0] else: place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) @@ -446,7 +583,7 @@ def make_einvoice(invoice): shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: - if invoice.gst_category == 'Overseas': + if invoice.gst_category == "Overseas": shipping_details = get_overseas_address_details(invoice.shipping_address_name) else: shipping_details = get_party_details(invoice.shipping_address_name, skip_gstin_validation=True) @@ -468,11 +605,19 @@ def make_einvoice(invoice): period_details = export_details = frappe._dict({}) einvoice = schema.format( - transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details, - seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, - item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details, - period_details=period_details, prev_doc_details=prev_doc_details, - export_details=export_details, eway_bill_details=eway_bill_details + transaction_details=transaction_details, + doc_details=doc_details, + dispatch_details=dispatch_details, + seller_details=seller_details, + buyer_details=buyer_details, + shipping_details=shipping_details, + item_list=item_list, + invoice_value_details=invoice_value_details, + payment_details=payment_details, + period_details=period_details, + prev_doc_details=prev_doc_details, + export_details=export_details, + eway_bill_details=eway_bill_details, ) try: @@ -489,15 +634,18 @@ def make_einvoice(invoice): return einvoice + def show_link_to_error_log(invoice, einvoice): err_log = log_error(einvoice) - link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log') + link_to_error_log = get_link_to_form("Error Log", err_log.name, "Error Log") frappe.throw( - _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format( - invoice.name, link_to_error_log), - title=_('E Invoice Creation Failed') + _( + "An error occurred while creating e-invoice for {}. Please check {} for more information." + ).format(invoice.name, link_to_error_log), + title=_("E Invoice Creation Failed"), ) + def log_error(data=None): if isinstance(data, six.string_types): data = json.loads(data) @@ -507,16 +655,47 @@ def log_error(data=None): err_msg = str(sys.exc_info()[1]) data = json.dumps(data, indent=4) - message = "\n".join([ - "Error", err_msg, seperator, - "Data:", data, seperator, - "Exception:", err_tb - ]) - return frappe.log_error(title=_('E Invoice Request Failed'), message=message) + message = "\n".join(["Error", err_msg, seperator, "Data:", data, seperator, "Exception:", err_tb]) + return frappe.log_error(title=_("E Invoice Request Failed"), message=message) + def santize_einvoice_fields(einvoice): - int_fields = ["Pin","Distance","CrDay"] - float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",] + int_fields = ["Pin", "Distance", "CrDay"] + float_fields = [ + "Qty", + "FreeQty", + "UnitPrice", + "TotAmt", + "Discount", + "PreTaxVal", + "AssAmt", + "GstRt", + "IgstAmt", + "CgstAmt", + "SgstAmt", + "CesRt", + "CesAmt", + "CesNonAdvlAmt", + "StateCesRt", + "StateCesAmt", + "StateCesNonAdvlAmt", + "OthChrg", + "TotItemVal", + "AssVal", + "CgstVal", + "SgstVal", + "IgstVal", + "CesVal", + "StCesVal", + "Discount", + "OthChrg", + "RndOffAmt", + "TotInvVal", + "TotInvValFc", + "PaidAmt", + "PaymtDue", + "ExpDuty", + ] copy = einvoice.copy() for key, value in copy.items(): if isinstance(value, list): @@ -548,22 +727,31 @@ def santize_einvoice_fields(einvoice): return einvoice + def safe_json_load(json_string): try: return json.loads(json_string) except json.JSONDecodeError as e: # print a snippet of 40 characters around the location where error occured pos = e.pos - start, end = max(0, pos-20), min(len(json_string)-1, pos+20) + start, end = max(0, pos - 20), min(len(json_string) - 1, pos + 20) snippet = json_string[start:end] - frappe.throw(_("Error in input data. Please check for any special characters near following input:
    {}").format(snippet)) + frappe.throw( + _( + "Error in input data. Please check for any special characters near following input:
    {}" + ).format(snippet) + ) + class RequestFailed(Exception): pass + + class CancellationNotAllowed(Exception): pass -class GSPConnector(): + +class GSPConnector: def __init__(self, doctype=None, docname=None): self.doctype = doctype self.docname = docname @@ -572,15 +760,19 @@ class GSPConnector(): self.set_credentials() # authenticate url is same for sandbox & live - self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' - self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test' + self.authenticate_url = "https://gsp.adaequare.com/gsp/authenticate?grant_type=token" + self.base_url = ( + "https://gsp.adaequare.com" + if not self.e_invoice_settings.sandbox_mode + else "https://gsp.adaequare.com/test" + ) - self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' - self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' - self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice' - self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin' - self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB' - self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' + self.cancel_irn_url = self.base_url + "/enriched/ei/api/invoice/cancel" + self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn" + self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" + self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" + self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" + self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" def set_invoice(self): self.invoice = None @@ -588,10 +780,14 @@ class GSPConnector(): self.invoice = frappe.get_cached_doc(self.doctype, self.docname) def set_credentials(self): - self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') + self.e_invoice_settings = frappe.get_cached_doc("E Invoice Settings") if not self.e_invoice_settings.enable: - frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) + frappe.throw( + _("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format( + get_link_to_form("E Invoice Settings", "E Invoice Settings") + ) + ) if self.invoice: gstin = self.get_seller_gstin() @@ -599,14 +795,22 @@ class GSPConnector(): if credentials_for_gstin: self.credentials = credentials_for_gstin[0] else: - frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings')) + frappe.throw( + _( + "Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings" + ) + ) else: - self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + self.credentials = ( + self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + ) def get_seller_gstin(self): - gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin') + gstin = frappe.db.get_value("Address", self.invoice.company_address, "gstin") if not gstin: - frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) + frappe.throw( + _("Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.") + ) return gstin def get_auth_token(self): @@ -616,7 +820,7 @@ class GSPConnector(): return self.e_invoice_settings.auth_token def make_request(self, request_type, url, headers=None, data=None): - if request_type == 'post': + if request_type == "post": res = make_post_request(url, headers=headers, data=data) else: res = make_get_request(url, headers=headers, data=data) @@ -625,36 +829,37 @@ class GSPConnector(): return res def log_request(self, url, headers, data, res): - headers.update({ 'password': self.credentials.password }) - request_log = frappe.get_doc({ - "doctype": "E Invoice Request Log", - "user": frappe.session.user, - "reference_invoice": self.invoice.name if self.invoice else None, - "url": url, - "headers": json.dumps(headers, indent=4) if headers else None, - "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, - "response": json.dumps(res, indent=4) if res else None - }) + headers.update({"password": self.credentials.password}) + request_log = frappe.get_doc( + { + "doctype": "E Invoice Request Log", + "user": frappe.session.user, + "reference_invoice": self.invoice.name if self.invoice else None, + "url": url, + "headers": json.dumps(headers, indent=4) if headers else None, + "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, + "response": json.dumps(res, indent=4) if res else None, + } + ) request_log.save(ignore_permissions=True) frappe.db.commit() def get_client_credentials(self): if self.e_invoice_settings.client_id and self.e_invoice_settings.client_secret: - return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password('client_secret') + return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password("client_secret") return frappe.conf.einvoice_client_id, frappe.conf.einvoice_client_secret def fetch_auth_token(self): client_id, client_secret = self.get_client_credentials() - headers = { - 'gspappid': client_id, - 'gspappsecret': client_secret - } + headers = {"gspappid": client_id, "gspappsecret": client_secret} res = {} try: - res = self.make_request('post', self.authenticate_url, headers) - self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) - self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) + res = self.make_request("post", self.authenticate_url, headers) + self.e_invoice_settings.auth_token = "{} {}".format( + res.get("token_type"), res.get("access_token") + ) + self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get("expires_in")) self.e_invoice_settings.save(ignore_permissions=True) self.e_invoice_settings.reload() @@ -664,22 +869,22 @@ class GSPConnector(): def get_headers(self): return { - 'content-type': 'application/json', - 'user_name': self.credentials.username, - 'password': self.credentials.get_password(), - 'gstin': self.credentials.gstin, - 'authorization': self.get_auth_token(), - 'requestid': str(base64.b64encode(os.urandom(18))), + "content-type": "application/json", + "user_name": self.credentials.username, + "password": self.credentials.get_password(), + "gstin": self.credentials.gstin, + "authorization": self.get_auth_token(), + "requestid": str(base64.b64encode(os.urandom(18))), } def fetch_gstin_details(self, gstin): headers = self.get_headers() try: - params = '?gstin={gstin}'.format(gstin=gstin) - res = self.make_request('get', self.gstin_details_url + params, headers) - if res.get('success'): - return res.get('result') + params = "?gstin={gstin}".format(gstin=gstin) + res = self.make_request("get", self.gstin_details_url + params, headers) + if res.get("success"): + return res.get("result") else: log_error(res) raise RequestFailed @@ -690,10 +895,11 @@ class GSPConnector(): except Exception: log_error() self.raise_error(True) + @staticmethod def get_gstin_details(gstin): - '''fetch and cache GSTIN details''' - if not hasattr(frappe.local, 'gstin_cache'): + """fetch and cache GSTIN details""" + if not hasattr(frappe.local, "gstin_cache"): frappe.local.gstin_cache = {} key = gstin @@ -701,7 +907,7 @@ class GSPConnector(): details = gsp_connector.fetch_gstin_details(gstin) frappe.local.gstin_cache[key] = details - frappe.cache().hset('gstin_cache', key, details) + frappe.cache().hset("gstin_cache", key, details) return details def generate_irn(self): @@ -710,27 +916,29 @@ class GSPConnector(): headers = self.get_headers() einvoice = make_einvoice(self.invoice) data = json.dumps(einvoice, indent=4) - res = self.make_request('post', self.generate_irn_url, headers, data) + res = self.make_request("post", self.generate_irn_url, headers, data) - if res.get('success'): - self.set_einvoice_data(res.get('result')) + if res.get("success"): + self.set_einvoice_data(res.get("result")) - elif '2150' in res.get('message'): + elif "2150" in res.get("message"): # IRN already generated but not updated in invoice # Extract the IRN from the response description and fetch irn details - irn = res.get('result')[0].get('Desc').get('Irn') + irn = res.get("result")[0].get("Desc").get("Irn") irn_details = self.get_irn_details(irn) if irn_details: self.set_einvoice_data(irn_details) else: - raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \ - Contact ERPNext support to resolve the issue.') + raise RequestFailed( + "IRN has already been generated for the invoice but cannot fetch details for the it. \ + Contact ERPNext support to resolve the issue." + ) else: raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.set_failed_status(errors=errors) self.raise_error(errors=errors) @@ -742,7 +950,7 @@ class GSPConnector(): @staticmethod def bulk_generate_irn(invoices): gsp_connector = GSPConnector() - gsp_connector.doctype = 'Sales Invoice' + gsp_connector.doctype = "Sales Invoice" failed = [] @@ -754,10 +962,7 @@ class GSPConnector(): gsp_connector.generate_irn() except Exception as e: - failed.append({ - 'docname': invoice, - 'message': str(e) - }) + failed.append({"docname": invoice, "message": str(e)}) return failed @@ -765,15 +970,15 @@ class GSPConnector(): headers = self.get_headers() try: - params = '?irn={irn}'.format(irn=irn) - res = self.make_request('get', self.irn_details_url + params, headers) - if res.get('success'): - return res.get('result') + params = "?irn={irn}".format(irn=irn) + res = self.make_request("get", self.irn_details_url + params, headers) + if res.get("success"): + return res.get("result") else: raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.raise_error(errors=errors) except Exception: @@ -785,26 +990,30 @@ class GSPConnector(): try: # validate cancellation if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24: - frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + frappe.throw( + _("E-Invoice cannot be cancelled after 24 hours of IRN generation."), + title=_("Not Allowed"), + exc=CancellationNotAllowed, + ) if not irn: - frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed) + frappe.throw( + _("IRN not found. You must generate IRN before cancelling."), + title=_("Not Allowed"), + exc=CancellationNotAllowed, + ) headers = self.get_headers() - data = json.dumps({ - 'Irn': irn, - 'Cnlrsn': reason, - 'Cnlrem': remark - }, indent=4) + data = json.dumps({"Irn": irn, "Cnlrsn": reason, "Cnlrem": remark}, indent=4) - res = self.make_request('post', self.cancel_irn_url, headers, data) - if res.get('success') or '9999' in res.get('message'): + res = self.make_request("post", self.cancel_irn_url, headers, data) + if res.get("success") or "9999" in res.get("message"): self.invoice.irn_cancelled = 1 - self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else "" - self.invoice.einvoice_status = 'Cancelled' + self.invoice.irn_cancel_date = res.get("result")["CancelDate"] if res.get("result") else "" + self.invoice.einvoice_status = "Cancelled" self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('IRN Cancelled - {}').format(remark) + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("IRN Cancelled - {}").format(remark), } self.update_invoice() @@ -812,7 +1021,7 @@ class GSPConnector(): raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.set_failed_status(errors=errors) self.raise_error(errors=errors) @@ -828,7 +1037,7 @@ class GSPConnector(): @staticmethod def bulk_cancel_irn(invoices, reason, remark): gsp_connector = GSPConnector() - gsp_connector.doctype = 'Sales Invoice' + gsp_connector.doctype = "Sales Invoice" failed = [] @@ -841,10 +1050,7 @@ class GSPConnector(): gsp_connector.cancel_irn(irn, reason, remark) except Exception as e: - failed.append({ - 'docname': invoice, - 'message': str(e) - }) + failed.append({"docname": invoice, "message": str(e)}) return failed @@ -853,29 +1059,32 @@ class GSPConnector(): headers = self.get_headers() eway_bill_details = get_eway_bill_details(args) - data = json.dumps({ - 'Irn': args.irn, - 'Distance': cint(eway_bill_details.distance), - 'TransMode': eway_bill_details.mode_of_transport, - 'TransId': eway_bill_details.gstin, - 'TransName': eway_bill_details.transporter, - 'TrnDocDt': eway_bill_details.document_date, - 'TrnDocNo': eway_bill_details.document_name, - 'VehNo': eway_bill_details.vehicle_no, - 'VehType': eway_bill_details.vehicle_type - }, indent=4) + data = json.dumps( + { + "Irn": args.irn, + "Distance": cint(eway_bill_details.distance), + "TransMode": eway_bill_details.mode_of_transport, + "TransId": eway_bill_details.gstin, + "TransName": eway_bill_details.transporter, + "TrnDocDt": eway_bill_details.document_date, + "TrnDocNo": eway_bill_details.document_name, + "VehNo": eway_bill_details.vehicle_no, + "VehType": eway_bill_details.vehicle_type, + }, + indent=4, + ) try: - res = self.make_request('post', self.generate_ewaybill_url, headers, data) - if res.get('success'): - self.invoice.ewaybill = res.get('result').get('EwbNo') - self.invoice.eway_bill_validity = res.get('result').get('EwbValidTill') + res = self.make_request("post", self.generate_ewaybill_url, headers, data) + if res.get("success"): + self.invoice.ewaybill = res.get("result").get("EwbNo") + self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill") self.invoice.eway_bill_cancelled = 0 self.invoice.update(args) self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('E-Way Bill Generated') + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("E-Way Bill Generated"), } self.update_invoice() @@ -883,7 +1092,7 @@ class GSPConnector(): raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.raise_error(errors=errors) except Exception: @@ -892,22 +1101,18 @@ class GSPConnector(): def cancel_eway_bill(self, eway_bill, reason, remark): headers = self.get_headers() - data = json.dumps({ - 'ewbNo': eway_bill, - 'cancelRsnCode': reason, - 'cancelRmrk': remark - }, indent=4) + data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) headers["username"] = headers["user_name"] del headers["user_name"] try: - res = self.make_request('post', self.cancel_ewaybill_url, headers, data) - if res.get('success'): - self.invoice.ewaybill = '' + res = self.make_request("post", self.cancel_ewaybill_url, headers, data) + if res.get("success"): + self.invoice.ewaybill = "" self.invoice.eway_bill_cancelled = 1 self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('E-Way Bill Cancelled - {}').format(remark) + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("E-Way Bill Cancelled - {}").format(remark), } self.update_invoice() @@ -915,7 +1120,7 @@ class GSPConnector(): raise RequestFailed except RequestFailed: - errors = self.sanitize_error_message(res.get('message')) + errors = self.sanitize_error_message(res.get("message")) self.raise_error(errors=errors) except Exception: @@ -923,24 +1128,24 @@ class GSPConnector(): self.raise_error(True) def sanitize_error_message(self, message): - ''' - On validation errors, response message looks something like this: - message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, - 3095 : Supplier GSTIN is inactive' - we search for string between ':' to extract the error messages - errors = [ - ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', - ': Test' - ] - then we trim down the message by looping over errors - ''' + """ + On validation errors, response message looks something like this: + message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, + 3095 : Supplier GSTIN is inactive' + we search for string between ':' to extract the error messages + errors = [ + ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', + ': Test' + ] + then we trim down the message by looping over errors + """ if not message: return [] - errors = re.findall(': [^:]+', message) + errors = re.findall(": [^:]+", message) for idx, e in enumerate(errors): # remove colons - errors[idx] = errors[idx].replace(':', '').strip() + errors[idx] = errors[idx].replace(":", "").strip() # if not last if idx != len(errors) - 1: # remove last 7 chars eg: ', 3095 ' @@ -949,39 +1154,41 @@ class GSPConnector(): return errors def raise_error(self, raise_exception=False, errors=None): - title = _('E Invoice Request Failed') + title = _("E Invoice Request Failed") if errors: frappe.throw(errors, title=title, as_list=1) else: link_to_error_list = 'Error Log' frappe.msgprint( - _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), + _( + "An error occurred while making e-invoicing request. Please check {} for more information." + ).format(link_to_error_list), title=title, raise_exception=raise_exception, - indicator='red' + indicator="red", ) def set_einvoice_data(self, res): - enc_signed_invoice = res.get('SignedInvoice') - dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] + enc_signed_invoice = res.get("SignedInvoice") + dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)["data"] - self.invoice.irn = res.get('Irn') - self.invoice.ewaybill = res.get('EwbNo') - self.invoice.eway_bill_validity = res.get('EwbValidTill') - self.invoice.ack_no = res.get('AckNo') - self.invoice.ack_date = res.get('AckDt') + self.invoice.irn = res.get("Irn") + self.invoice.ewaybill = res.get("EwbNo") + self.invoice.eway_bill_validity = res.get("EwbValidTill") + self.invoice.ack_no = res.get("AckNo") + self.invoice.ack_date = res.get("AckDt") self.invoice.signed_einvoice = dec_signed_invoice - self.invoice.ack_no = res.get('AckNo') - self.invoice.ack_date = res.get('AckDt') - self.invoice.signed_qr_code = res.get('SignedQRCode') - self.invoice.einvoice_status = 'Generated' + self.invoice.ack_no = res.get("AckNo") + self.invoice.ack_date = res.get("AckDt") + self.invoice.signed_qr_code = res.get("SignedQRCode") + self.invoice.einvoice_status = "Generated" self.attach_qrcode_image() self.invoice.flags.updater_reference = { - 'doctype': self.invoice.doctype, - 'docname': self.invoice.name, - 'label': _('IRN Generated') + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("IRN Generated"), } self.update_invoice() @@ -989,19 +1196,22 @@ class GSPConnector(): qrcode = self.invoice.signed_qr_code doctype = self.invoice.doctype docname = self.invoice.name - filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__") + filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__") qr_image = io.BytesIO() - url = qrcreate(qrcode, error='L') + url = qrcreate(qrcode, error="L") url.png(qr_image, scale=2, quiet_zone=1) - _file = frappe.get_doc({ - "doctype": "File", - "file_name": filename, - "attached_to_doctype": doctype, - "attached_to_name": docname, - "attached_to_field": "qrcode_image", - "is_private": 0, - "content": qr_image.getvalue()}) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": filename, + "attached_to_doctype": doctype, + "attached_to_name": docname, + "attached_to_field": "qrcode_image", + "is_private": 0, + "content": qr_image.getvalue(), + } + ) _file.save() frappe.db.commit() self.invoice.qrcode_image = _file.file_url @@ -1013,50 +1223,57 @@ class GSPConnector(): def set_failed_status(self, errors=None): frappe.db.rollback() - self.invoice.einvoice_status = 'Failed' + self.invoice.einvoice_status = "Failed" self.invoice.failure_description = self.get_failure_message(errors) if errors else "" self.update_invoice() frappe.db.commit() def get_failure_message(self, errors): if isinstance(errors, list): - errors = ', '.join(errors) + errors = ", ".join(errors) return errors + def sanitize_for_json(string): """Escape JSON specific characters from a string.""" # json.dumps adds double-quotes to the string. Indexing to remove them. return json.dumps(string)[1:-1] + @frappe.whitelist() def get_einvoice(doctype, docname): invoice = frappe.get_doc(doctype, docname) return make_einvoice(invoice) + @frappe.whitelist() def generate_irn(doctype, docname): gsp_connector = GSPConnector(doctype, docname) gsp_connector.generate_irn() + @frappe.whitelist() def cancel_irn(doctype, docname, irn, reason, remark): gsp_connector = GSPConnector(doctype, docname) gsp_connector.cancel_irn(irn, reason, remark) + @frappe.whitelist() def generate_eway_bill(doctype, docname, **kwargs): gsp_connector = GSPConnector(doctype, docname) gsp_connector.generate_eway_bill(**kwargs) + @frappe.whitelist() def cancel_eway_bill(doctype, docname): # TODO: uncomment when eway_bill api from Adequare is enabled # gsp_connector = GSPConnector(doctype, docname) # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) - frappe.db.set_value(doctype, docname, 'ewaybill', '') - frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) + frappe.db.set_value(doctype, docname, "ewaybill", "") + frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1) + @frappe.whitelist() def generate_einvoices(docnames): @@ -1071,40 +1288,40 @@ def generate_einvoices(docnames): success = len(docnames) - len(failures) frappe.msgprint( - _('{} e-invoices generated successfully').format(success), - title=_('Bulk E-Invoice Generation Complete') + _("{} e-invoices generated successfully").format(success), + title=_("Bulk E-Invoice Generation Complete"), ) else: enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) + def schedule_bulk_generate_irn(docnames): failures = GSPConnector.bulk_generate_irn(docnames) frappe.local.message_log = [] - frappe.publish_realtime("bulk_einvoice_generation_complete", { - "user": frappe.session.user, - "failures": failures, - "invoices": docnames - }) + frappe.publish_realtime( + "bulk_einvoice_generation_complete", + {"user": frappe.session.user, "failures": failures, "invoices": docnames}, + ) + def show_bulk_action_failure_message(failures): for doc in failures: - docname = '{0}'.format(doc.get('docname')) - message = doc.get('message').replace("'", '"') - if message[0] == '[': + docname = '{0}'.format(doc.get("docname")) + message = doc.get("message").replace("'", '"') + if message[0] == "[": errors = json.loads(message) - error_list = ''.join(['
  • {}
  • '.format(err) for err in errors]) - message = '''{} has following errors:
    -
      {}
    '''.format(docname, error_list) + error_list = "".join(["
  • {}
  • ".format(err) for err in errors]) + message = """{} has following errors:
    +
      {}
    """.format( + docname, error_list + ) else: - message = '{} - {}'.format(docname, message) + message = "{} - {}".format(docname, message) + + frappe.msgprint(message, title=_("Bulk E-Invoice Generation Complete"), indicator="red") - frappe.msgprint( - message, - title=_('Bulk E-Invoice Generation Complete'), - indicator='red' - ) @frappe.whitelist() def cancel_irns(docnames, reason, remark): @@ -1119,21 +1336,22 @@ def cancel_irns(docnames, reason, remark): success = len(docnames) - len(failures) frappe.msgprint( - _('{} e-invoices cancelled successfully').format(success), - title=_('Bulk E-Invoice Cancellation Complete') + _("{} e-invoices cancelled successfully").format(success), + title=_("Bulk E-Invoice Cancellation Complete"), ) else: enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark) + def schedule_bulk_cancel_irn(docnames, reason, remark): failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark) frappe.local.message_log = [] - frappe.publish_realtime("bulk_einvoice_cancellation_complete", { - "user": frappe.session.user, - "failures": failures, - "invoices": docnames - }) + frappe.publish_realtime( + "bulk_einvoice_cancellation_complete", + {"user": frappe.session.user, "failures": failures, "invoices": docnames}, + ) + def enqueue_bulk_action(job, **kwargs): check_scheduler_status() @@ -1148,16 +1366,18 @@ def enqueue_bulk_action(job, **kwargs): ) if job == schedule_bulk_generate_irn: - msg = _('E-Invoices will be generated in a background process.') + msg = _("E-Invoices will be generated in a background process.") else: - msg = _('E-Invoices will be cancelled in a background process.') + msg = _("E-Invoices will be cancelled in a background process.") frappe.msgprint(msg, alert=1) + def check_scheduler_status(): if is_scheduler_inactive() and not frappe.flags.in_test: frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) + def job_already_enqueued(job_name): enqueued_jobs = [d.get("job_name") for d in get_info()] if job_name in enqueued_jobs: diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 6f2300b9fb7..8e3d8e3c8a7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -17,44 +17,47 @@ from erpnext.regional.india import states def setup(company=None, patch=True): # Company independent fixtures should be called only once at the first company setup - if patch or frappe.db.count('Company', {'country': 'India'}) <=1: + if patch or frappe.db.count("Company", {"country": "India"}) <= 1: setup_company_independent_fixtures(patch=patch) if not patch: make_fixtures(company) + # TODO: for all countries def setup_company_independent_fixtures(patch=False): make_custom_fields() make_property_setters(patch=patch) add_permissions() add_custom_roles_for_reports() - frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) + frappe.enqueue("erpnext.regional.india.setup.add_hsn_sac_codes", now=frappe.flags.in_test) create_gratuity_rule() add_print_formats() update_accounts_settings_for_taxes() + def add_hsn_sac_codes(): if frappe.flags.in_test and frappe.flags.created_hsn_codes: return # HSN codes - with open(os.path.join(os.path.dirname(__file__), 'hsn_code_data.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "hsn_code_data.json"), "r") as f: hsn_codes = json.loads(f.read()) create_hsn_codes(hsn_codes, code_field="hsn_code") # SAC Codes - with open(os.path.join(os.path.dirname(__file__), 'sac_code_data.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "sac_code_data.json"), "r") as f: sac_codes = json.loads(f.read()) create_hsn_codes(sac_codes, code_field="sac_code") if frappe.flags.in_test: frappe.flags.created_hsn_codes = True + def create_hsn_codes(data, code_field): for d in data: - hsn_code = frappe.new_doc('GST HSN Code') + hsn_code = frappe.new_doc("GST HSN Code") hsn_code.description = d["description"] hsn_code.hsn_code = d[code_field] hsn_code.name = d[code_field] @@ -63,59 +66,69 @@ def create_hsn_codes(data, code_field): except frappe.DuplicateEntryError: pass + def add_custom_roles_for_reports(): - for report_name in ('GST Sales Register', 'GST Purchase Register', - 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'): + for report_name in ( + "GST Sales Register", + "GST Purchase Register", + "GST Itemised Sales Register", + "GST Itemised Purchase Register", + "Eway Bill", + "E-Invoice Summary", + ): - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() - for report_name in ('Professional Tax Deductions', 'Provident Fund Deductions'): + for report_name in ("Professional Tax Deductions", "Provident Fund Deductions"): - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='HR User'), - dict(role='HR Manager'), - dict(role='Employee') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="HR User"), dict(role="HR Manager"), dict(role="Employee")], + ) + ).insert() - for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'): + for report_name in ("HSN-wise-summary of outward supplies", "GSTR-1", "GSTR-2"): + + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + for doctype in ( + "GST HSN Code", + "GST Settings", + "GSTR 3B Report", + "Lower Deduction Certificate", + "E Invoice Settings", + ): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - if doctype == 'GST HSN Code': - for role in ('Item Manager', 'Stock Manager'): + if doctype == "GST HSN Code": + for role in ("Item Manager", "Stock Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") @@ -126,614 +139,1117 @@ def add_print_formats(): frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) + def make_property_setters(patch=False): # GST rules do not allow for an invoice no. bigger than 16 characters - journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC'] - sales_invoice_series = ['SINV-.YY.-', 'SRET-.YY.-', ''] + frappe.get_meta("Sales Invoice").get_options("naming_series").split("\n") - purchase_invoice_series = ['PINV-.YY.-', 'PRET-.YY.-', ''] + frappe.get_meta("Purchase Invoice").get_options("naming_series").split("\n") + journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + [ + "Reversal Of ITC" + ] + sales_invoice_series = ["SINV-.YY.-", "SRET-.YY.-", ""] + frappe.get_meta( + "Sales Invoice" + ).get_options("naming_series").split("\n") + purchase_invoice_series = ["PINV-.YY.-", "PRET-.YY.-", ""] + frappe.get_meta( + "Purchase Invoice" + ).get_options("naming_series").split("\n") if not patch: - make_property_setter('Sales Invoice', 'naming_series', 'options', '\n'.join(sales_invoice_series), '') - make_property_setter('Purchase Invoice', 'naming_series', 'options', '\n'.join(purchase_invoice_series), '') - make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '') + make_property_setter( + "Sales Invoice", "naming_series", "options", "\n".join(sales_invoice_series), "" + ) + make_property_setter( + "Purchase Invoice", "naming_series", "options", "\n".join(purchase_invoice_series), "" + ) + make_property_setter( + "Journal Entry", "voucher_type", "options", "\n".join(journal_entry_types), "" + ) + def make_custom_fields(update=True): custom_fields = get_custom_fields() create_custom_fields(custom_fields, update=update) + def get_custom_fields(): - hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description', - allow_on_submit=1, print_hide=1, fetch_if_empty=1) - nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code', - print_hide=1) - is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', - fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', - print_hide=1) - taxable_value = dict(fieldname='taxable_value', label='Taxable Value', - fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", - print_hide=1) + hsn_sac_field = dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Data", + fetch_from="item_code.gst_hsn_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + fetch_if_empty=1, + ) + nil_rated_exempt = dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + fetch_from="item_code.is_nil_exempt", + insert_after="gst_hsn_code", + print_hide=1, + ) + is_non_gst = dict( + fieldname="is_non_gst", + label="Is Non GST", + fieldtype="Check", + fetch_from="item_code.is_non_gst", + insert_after="is_nil_exempt", + print_hide=1, + ) + taxable_value = dict( + fieldname="taxable_value", + label="Taxable Value", + fieldtype="Currency", + insert_after="base_net_amount", + hidden=1, + options="Company:company:default_currency", + print_hide=1, + ) purchase_invoice_gst_category = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', - insert_after='language', print_hide=1, collapsible=1), - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_section', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - fetch_from='supplier.gst_category', fetch_if_empty=1), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='gst_category', print_hide=1, + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="language", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_section", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders", + fetch_from="supplier.gst_category", + fetch_if_empty=1, + ), + dict( + fieldname="export_type", + label="Export Type", + fieldtype="Select", + insert_after="gst_category", + print_hide=1, depends_on='eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='supplier.export_type', - fetch_if_empty=1), + options="\nWith Payment of Tax\nWithout Payment of Tax", + fetch_from="supplier.export_type", + fetch_if_empty=1, + ), ] sales_invoice_gst_category = [ - dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', - insert_after='language', print_hide=1, collapsible=1), - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_section', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1, length=25), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='gst_category', print_hide=1, + dict( + fieldname="gst_section", + label="GST Details", + fieldtype="Section Break", + insert_after="language", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_section", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + length=25, + ), + dict( + fieldname="export_type", + label="Export Type", + fieldtype="Select", + insert_after="gst_category", + print_hide=1, depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type', - fetch_if_empty=1, length=25), + options="\nWith Payment of Tax\nWithout Payment of Tax", + fetch_from="customer.export_type", + fetch_if_empty=1, + length=25, + ), ] delivery_note_gst_category = [ - dict(fieldname='gst_category', label='GST Category', - fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1, - options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1), + dict( + fieldname="gst_category", + label="GST Category", + fieldtype="Select", + insert_after="gst_vehicle_type", + print_hide=1, + options="\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + fetch_from="customer.gst_category", + fetch_if_empty=1, + ), ] invoice_gst_fields = [ - dict(fieldname='invoice_copy', label='Invoice Copy', length=30, - fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, - options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'), - dict(fieldname='reverse_charge', label='Reverse Charge', length=2, - fieldtype='Select', insert_after='invoice_copy', print_hide=1, - options='Y\nN', default='N'), - dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', length=15, - fieldtype='Data', insert_after='export_type', print_hide=1), - dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'), - dict(fieldname='reason_for_issuing_document', label='Reason For Issuing document', - fieldtype='Select', insert_after='gst_col_break', print_hide=1, - depends_on='eval:doc.is_return==1', length=45, - options='\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others') + dict( + fieldname="invoice_copy", + label="Invoice Copy", + length=30, + fieldtype="Select", + insert_after="export_type", + print_hide=1, + allow_on_submit=1, + options="Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier", + ), + dict( + fieldname="reverse_charge", + label="Reverse Charge", + length=2, + fieldtype="Select", + insert_after="invoice_copy", + print_hide=1, + options="Y\nN", + default="N", + ), + dict( + fieldname="ecommerce_gstin", + label="E-commerce GSTIN", + length=15, + fieldtype="Data", + insert_after="export_type", + print_hide=1, + ), + dict(fieldname="gst_col_break", fieldtype="Column Break", insert_after="ecommerce_gstin"), + dict( + fieldname="reason_for_issuing_document", + label="Reason For Issuing document", + fieldtype="Select", + insert_after="gst_col_break", + print_hide=1, + depends_on="eval:doc.is_return==1", + length=45, + options="\n01-Sales Return\n02-Post Sale Discount\n03-Deficiency in services\n04-Correction in Invoice\n05-Change in POS\n06-Finalization of Provisional assessment\n07-Others", + ), ] purchase_invoice_gst_fields = [ - dict(fieldname='supplier_gstin', label='Supplier GSTIN', - fieldtype='Data', insert_after='supplier_address', - fetch_from='supplier_address.gstin', print_hide=1, read_only=1), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='shipping_address_display', - fetch_from='shipping_address.gstin', print_hide=1, read_only=1), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='shipping_address', - print_hide=1, read_only=1), - ] + dict( + fieldname="supplier_gstin", + label="Supplier GSTIN", + fieldtype="Data", + insert_after="supplier_address", + fetch_from="supplier_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="shipping_address_display", + fetch_from="shipping_address.gstin", + print_hide=1, + read_only=1, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="shipping_address", + print_hide=1, + read_only=1, + ), + ] purchase_invoice_itc_fields = [ - dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', - fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, - options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC', - default="All Other ITC"), - dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax', - fieldtype='Currency', insert_after='eligibility_for_itc', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_central_tax', label='Availed ITC Central Tax', - fieldtype='Currency', insert_after='itc_integrated_tax', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax', - fieldtype='Currency', insert_after='itc_central_tax', - options='Company:company:default_currency', print_hide=1), - dict(fieldname='itc_cess_amount', label='Availed ITC Cess', - fieldtype='Currency', insert_after='itc_state_tax', - options='Company:company:default_currency', print_hide=1), - ] + dict( + fieldname="eligibility_for_itc", + label="Eligibility For ITC", + fieldtype="Select", + insert_after="reason_for_issuing_document", + print_hide=1, + options="Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC", + default="All Other ITC", + ), + dict( + fieldname="itc_integrated_tax", + label="Availed ITC Integrated Tax", + fieldtype="Currency", + insert_after="eligibility_for_itc", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_central_tax", + label="Availed ITC Central Tax", + fieldtype="Currency", + insert_after="itc_integrated_tax", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_state_tax", + label="Availed ITC State/UT Tax", + fieldtype="Currency", + insert_after="itc_central_tax", + options="Company:company:default_currency", + print_hide=1, + ), + dict( + fieldname="itc_cess_amount", + label="Availed ITC Cess", + fieldtype="Currency", + insert_after="itc_state_tax", + options="Company:company:default_currency", + print_hide=1, + ), + ] sales_invoice_gst_fields = [ - dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', read_only=1, - fetch_from='customer_address.gstin', print_hide=1, length=15), - dict(fieldname='customer_gstin', label='Customer GSTIN', - fieldtype='Data', insert_after='shipping_address_name', - fetch_from='shipping_address_name.gstin', print_hide=1, length=15), - dict(fieldname='place_of_supply', label='Place of Supply', - fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=1, length=50), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1, read_only=1, length=15), - ] + dict( + fieldname="billing_address_gstin", + label="Billing Address GSTIN", + fieldtype="Data", + insert_after="customer_address", + read_only=1, + fetch_from="customer_address.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="customer_gstin", + label="Customer GSTIN", + fieldtype="Data", + insert_after="shipping_address_name", + fetch_from="shipping_address_name.gstin", + print_hide=1, + length=15, + ), + dict( + fieldname="place_of_supply", + label="Place of Supply", + fieldtype="Data", + insert_after="customer_gstin", + print_hide=1, + read_only=1, + length=50, + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + insert_after="company_address", + fetch_from="company_address.gstin", + print_hide=1, + read_only=1, + length=15, + ), + ] sales_invoice_shipping_fields = [ - dict(fieldname='port_code', label='Port Code', - fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' ", length=15), - dict(fieldname='shipping_bill_number', label=' Shipping Bill Number', - fieldtype='Data', insert_after='port_code', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' ", length=50), - dict(fieldname='shipping_bill_date', label='Shipping Bill Date', - fieldtype='Date', insert_after='shipping_bill_number', print_hide=1, - depends_on="eval:doc.gst_category=='Overseas' "), - ] + dict( + fieldname="port_code", + label="Port Code", + fieldtype="Data", + insert_after="reason_for_issuing_document", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + length=15, + ), + dict( + fieldname="shipping_bill_number", + label=" Shipping Bill Number", + fieldtype="Data", + insert_after="port_code", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + length=50, + ), + dict( + fieldname="shipping_bill_date", + label="Shipping Bill Date", + fieldtype="Date", + insert_after="shipping_bill_number", + print_hide=1, + depends_on="eval:doc.gst_category=='Overseas' ", + ), + ] journal_entry_fields = [ - dict(fieldname='reversal_type', label='Reversal Type', - fieldtype='Select', insert_after='voucher_type', print_hide=1, + dict( + fieldname="reversal_type", + label="Reversal Type", + fieldtype="Select", + insert_after="voucher_type", + print_hide=1, options="As per rules 42 & 43 of CGST Rules\nOthers", depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_address', label='Company Address', - fieldtype='Link', options='Address', insert_after='reversal_type', - print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"), - dict(fieldname='company_gstin', label='Company GSTIN', - fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1, - fetch_from='company_address.gstin', + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_address", + label="Company Address", + fieldtype="Link", + options="Address", + insert_after="reversal_type", + print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'", - mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'") + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), + dict( + fieldname="company_gstin", + label="Company GSTIN", + fieldtype="Data", + read_only=1, + insert_after="company_address", + print_hide=1, + fetch_from="company_address.gstin", + depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'", + ), ] inter_state_gst_field = [ - dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1), - dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check', - insert_after='is_inter_state', print_hide=1), - dict(fieldname='tax_category_column_break', fieldtype='Column Break', - insert_after='is_reverse_charge'), - dict(fieldname='gst_state', label='Source State', fieldtype='Select', - options='\n'.join(states), insert_after='company') + dict( + fieldname="is_inter_state", + label="Is Inter State", + fieldtype="Check", + insert_after="disabled", + print_hide=1, + ), + dict( + fieldname="is_reverse_charge", + label="Is Reverse Charge", + fieldtype="Check", + insert_after="is_inter_state", + print_hide=1, + ), + dict( + fieldname="tax_category_column_break", + fieldtype="Column Break", + insert_after="is_reverse_charge", + ), + dict( + fieldname="gst_state", + label="Source State", + fieldtype="Select", + options="\n".join(states), + insert_after="company", + ), ] ewaybill_fields = [ { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'insert_after': 'vehicle_no', - 'print_hide': 1 + "fieldname": "distance", + "label": "Distance (in km)", + "fieldtype": "Float", + "insert_after": "vehicle_no", + "print_hide": 1, }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'transporter', - 'fetch_from': 'transporter.gst_transporter_id', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "transporter", + "fetch_from": "transporter.gst_transporter_id", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': '\nRoad\nAir\nRail\nShip', - 'default': 'Road', - 'insert_after': 'transporter_name', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "mode_of_transport", + "label": "Mode of Transport", + "fieldtype": "Select", + "options": "\nRoad\nAir\nRail\nShip", + "default": "Road", + "insert_after": "transporter_name", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'gst_vehicle_type', - 'label': 'GST Vehicle Type', - 'fieldtype': 'Select', - 'options': 'Regular\nOver Dimensional Cargo (ODC)', - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', - 'default': 'Regular', - 'insert_after': 'lr_date', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "gst_vehicle_type", + "label": "GST Vehicle Type", + "fieldtype": "Select", + "options": "Regular\nOver Dimensional Cargo (ODC)", + "depends_on": 'eval:(doc.mode_of_transport === "Road")', + "default": "Regular", + "insert_after": "lr_date", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', - 'allow_on_submit': 1, - 'insert_after': 'customer_name_in_arabic', - 'translatable': 0, - } + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:(doc.docstatus === 1)", + "allow_on_submit": 1, + "insert_after": "customer_name_in_arabic", + "translatable": 0, + }, ] si_ewaybill_fields = [ { - 'fieldname': 'transporter_info', - 'label': 'Transporter Info', - 'fieldtype': 'Section Break', - 'insert_after': 'terms', - 'collapsible': 1, - 'collapsible_depends_on': 'transporter', - 'print_hide': 1 + "fieldname": "transporter_info", + "label": "Transporter Info", + "fieldtype": "Section Break", + "insert_after": "terms", + "collapsible": 1, + "collapsible_depends_on": "transporter", + "print_hide": 1, }, { - 'fieldname': 'transporter', - 'label': 'Transporter', - 'fieldtype': 'Link', - 'insert_after': 'transporter_info', - 'options': 'Supplier', - 'print_hide': 1 + "fieldname": "transporter", + "label": "Transporter", + "fieldtype": "Link", + "insert_after": "transporter_info", + "options": "Supplier", + "print_hide": 1, }, { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'transporter', - 'fetch_from': 'transporter.gst_transporter_id', - 'print_hide': 1, - 'translatable': 0, - 'length': 20 + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "transporter", + "fetch_from": "transporter.gst_transporter_id", + "print_hide": 1, + "translatable": 0, + "length": 20, }, { - 'fieldname': 'driver', - 'label': 'Driver', - 'fieldtype': 'Link', - 'insert_after': 'gst_transporter_id', - 'options': 'Driver', - 'print_hide': 1 + "fieldname": "driver", + "label": "Driver", + "fieldtype": "Link", + "insert_after": "gst_transporter_id", + "options": "Driver", + "print_hide": 1, }, { - 'fieldname': 'lr_no', - 'label': 'Transport Receipt No', - 'fieldtype': 'Data', - 'insert_after': 'driver', - 'print_hide': 1, - 'translatable': 0, - 'length': 30 + "fieldname": "lr_no", + "label": "Transport Receipt No", + "fieldtype": "Data", + "insert_after": "driver", + "print_hide": 1, + "translatable": 0, + "length": 30, }, { - 'fieldname': 'vehicle_no', - 'label': 'Vehicle No', - 'fieldtype': 'Data', - 'insert_after': 'lr_no', - 'print_hide': 1, - 'translatable': 0, - 'length': 10 + "fieldname": "vehicle_no", + "label": "Vehicle No", + "fieldtype": "Data", + "insert_after": "lr_no", + "print_hide": 1, + "translatable": 0, + "length": 10, }, { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'insert_after': 'vehicle_no', - 'print_hide': 1 + "fieldname": "distance", + "label": "Distance (in km)", + "fieldtype": "Float", + "insert_after": "vehicle_no", + "print_hide": 1, + }, + {"fieldname": "transporter_col_break", "fieldtype": "Column Break", "insert_after": "distance"}, + { + "fieldname": "transporter_name", + "label": "Transporter Name", + "fieldtype": "Small Text", + "insert_after": "transporter_col_break", + "fetch_from": "transporter.name", + "read_only": 1, + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'transporter_col_break', - 'fieldtype': 'Column Break', - 'insert_after': 'distance' + "fieldname": "mode_of_transport", + "label": "Mode of Transport", + "fieldtype": "Select", + "options": "\nRoad\nAir\nRail\nShip", + "insert_after": "transporter_name", + "print_hide": 1, + "translatable": 0, + "length": 5, }, { - 'fieldname': 'transporter_name', - 'label': 'Transporter Name', - 'fieldtype': 'Small Text', - 'insert_after': 'transporter_col_break', - 'fetch_from': 'transporter.name', - 'read_only': 1, - 'print_hide': 1, - 'translatable': 0 + "fieldname": "driver_name", + "label": "Driver Name", + "fieldtype": "Small Text", + "insert_after": "mode_of_transport", + "fetch_from": "driver.full_name", + "print_hide": 1, + "translatable": 0, }, { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': '\nRoad\nAir\nRail\nShip', - 'insert_after': 'transporter_name', - 'print_hide': 1, - 'translatable': 0, - 'length': 5 + "fieldname": "lr_date", + "label": "Transport Receipt Date", + "fieldtype": "Date", + "insert_after": "driver_name", + "default": "Today", + "print_hide": 1, }, { - 'fieldname': 'driver_name', - 'label': 'Driver Name', - 'fieldtype': 'Small Text', - 'insert_after': 'mode_of_transport', - 'fetch_from': 'driver.full_name', - 'print_hide': 1, - 'translatable': 0 + "fieldname": "gst_vehicle_type", + "label": "GST Vehicle Type", + "fieldtype": "Select", + "options": "Regular\nOver Dimensional Cargo (ODC)", + "depends_on": 'eval:(doc.mode_of_transport === "Road")', + "default": "Regular", + "insert_after": "lr_date", + "print_hide": 1, + "translatable": 0, + "length": 30, }, { - 'fieldname': 'lr_date', - 'label': 'Transport Receipt Date', - 'fieldtype': 'Date', - 'insert_after': 'driver_name', - 'default': 'Today', - 'print_hide': 1 + "fieldname": "ewaybill", + "label": "E-Way Bill No.", + "fieldtype": "Data", + "depends_on": "eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)", + "allow_on_submit": 1, + "insert_after": "tax_id", + "translatable": 0, + "length": 20, }, - { - 'fieldname': 'gst_vehicle_type', - 'label': 'GST Vehicle Type', - 'fieldtype': 'Select', - 'options': 'Regular\nOver Dimensional Cargo (ODC)', - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', - 'default': 'Regular', - 'insert_after': 'lr_date', - 'print_hide': 1, - 'translatable': 0, - 'length': 30 - }, - { - 'fieldname': 'ewaybill', - 'label': 'E-Way Bill No.', - 'fieldtype': 'Data', - 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', - 'allow_on_submit': 1, - 'insert_after': 'tax_id', - 'translatable': 0, - 'length': 20 - } ] si_einvoice_fields = [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, - depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - - dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval: doc.irn', allow_on_submit=1, insert_after='customer'), - - dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1, - depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'), - - dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, - depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - - dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', - print_hide=1, hidden=1), - - dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', - no_copy=1, print_hide=1), - - dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), - - dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', - no_copy=1, print_hide=1), - - dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', - no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', - options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), - - dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', - hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) + dict( + fieldname="irn", + label="IRN", + fieldtype="Data", + read_only=1, + insert_after="customer", + no_copy=1, + print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0', + ), + dict( + fieldname="irn_cancelled", + label="IRN Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval: doc.irn", + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="eway_bill_validity", + label="E-Way Bill Validity", + fieldtype="Data", + no_copy=1, + print_hide=1, + depends_on="ewaybill", + read_only=1, + allow_on_submit=1, + insert_after="ewaybill", + ), + dict( + fieldname="eway_bill_cancelled", + label="E-Way Bill Cancelled", + fieldtype="Check", + no_copy=1, + print_hide=1, + depends_on="eval:(doc.eway_bill_cancelled === 1)", + read_only=1, + allow_on_submit=1, + insert_after="customer", + ), + dict( + fieldname="einvoice_section", + label="E-Invoice Fields", + fieldtype="Section Break", + insert_after="gst_vehicle_type", + print_hide=1, + hidden=1, + ), + dict( + fieldname="ack_no", + label="Ack. No.", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="einvoice_section", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="ack_date", + label="Ack. Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_no", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="irn_cancel_date", + label="Cancel Date", + fieldtype="Data", + read_only=1, + hidden=1, + insert_after="ack_date", + no_copy=1, + print_hide=1, + ), + dict( + fieldname="signed_einvoice", + label="Signed E-Invoice", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="irn_cancel_date", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="signed_qr_code", + label="Signed QRCode", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="signed_einvoice", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="qrcode_image", + label="QRCode", + fieldtype="Attach Image", + hidden=1, + insert_after="signed_qr_code", + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="einvoice_status", + label="E-Invoice Status", + fieldtype="Select", + insert_after="qrcode_image", + options="\nPending\nGenerated\nCancelled\nFailed", + default=None, + hidden=1, + no_copy=1, + print_hide=1, + read_only=1, + ), + dict( + fieldname="failure_description", + label="E-Invoice Failure Description", + fieldtype="Code", + options="JSON", + hidden=1, + insert_after="einvoice_status", + no_copy=1, + print_hide=1, + read_only=1, + ), ] payment_entry_fields = [ - 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='gst_column_break', fieldtype='Column Break', - insert_after='place_of_supply'), - dict(fieldname='customer_address', label='Customer Address', fieldtype='Link', insert_after='gst_column_break', - 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) + 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="gst_column_break", fieldtype="Column Break", insert_after="place_of_supply"), + dict( + fieldname="customer_address", + label="Customer Address", + fieldtype="Link", + insert_after="gst_column_break", + 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, + ), ] custom_fields = { - 'Address': [ - dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', - insert_after='fax'), - dict(fieldname='gst_state', label='GST State', fieldtype='Select', - options='\n'.join(states), insert_after='gstin'), - dict(fieldname='gst_state_number', label='GST State Number', - fieldtype='Data', insert_after='gst_state', read_only=1), + "Address": [ + dict(fieldname="gstin", label="Party GSTIN", fieldtype="Data", insert_after="fax"), + dict( + fieldname="gst_state", + label="GST State", + fieldtype="Select", + options="\n".join(states), + insert_after="gstin", + ), + dict( + fieldname="gst_state_number", + label="GST State Number", + fieldtype="Data", + insert_after="gst_state", + read_only=1, + ), ], - 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, - 'Purchase Order': purchase_invoice_gst_fields, - 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, - 'POS Invoice': sales_invoice_gst_fields, - 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, - 'Payment Entry': payment_entry_fields, - 'Journal Entry': journal_entry_fields, - 'Sales Order': sales_invoice_gst_fields, - 'Tax Category': inter_state_gst_field, - 'Quotation': sales_invoice_gst_fields, - 'Item': [ - dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Link', options='GST HSN Code', insert_after='item_group'), - dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted', - fieldtype='Check', insert_after='gst_hsn_code'), - dict(fieldname='is_non_gst', label='Is Non GST ', - fieldtype='Check', insert_after='is_nil_exempt') + "Purchase Invoice": purchase_invoice_gst_category + + invoice_gst_fields + + purchase_invoice_itc_fields + + purchase_invoice_gst_fields, + "Purchase Order": purchase_invoice_gst_fields, + "Purchase Receipt": purchase_invoice_gst_fields, + "Sales Invoice": sales_invoice_gst_category + + invoice_gst_fields + + sales_invoice_shipping_fields + + sales_invoice_gst_fields + + si_ewaybill_fields + + si_einvoice_fields, + "POS Invoice": sales_invoice_gst_fields, + "Delivery Note": sales_invoice_gst_fields + + ewaybill_fields + + sales_invoice_shipping_fields + + delivery_note_gst_category, + "Payment Entry": payment_entry_fields, + "Journal Entry": journal_entry_fields, + "Sales Order": sales_invoice_gst_fields, + "Tax Category": inter_state_gst_field, + "Quotation": sales_invoice_gst_fields, + "Item": [ + dict( + fieldname="gst_hsn_code", + label="HSN/SAC", + fieldtype="Link", + options="GST HSN Code", + insert_after="item_group", + ), + dict( + fieldname="is_nil_exempt", + label="Is Nil Rated or Exempted", + fieldtype="Check", + insert_after="gst_hsn_code", + ), + dict( + fieldname="is_non_gst", label="Is Non GST ", fieldtype="Check", insert_after="is_nil_exempt" + ), ], - 'Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], - 'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], - 'Salary Component': [ - dict(fieldname= 'component_type', - label= 'Component Type', - fieldtype= 'Select', - insert_after= 'description', - options= "\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", - depends_on = 'eval:doc.type == "Deduction"' + "Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Supplier Quotation Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Sales Order Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Delivery Note Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Sales Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "POS Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "Purchase Order Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Purchase Receipt Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Purchase Invoice Item": [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], + "Material Request Item": [hsn_sac_field, nil_rated_exempt, is_non_gst], + "Salary Component": [ + dict( + fieldname="component_type", + label="Component Type", + fieldtype="Select", + insert_after="description", + options="\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax", + depends_on='eval:doc.type == "Deduction"', ) ], - 'Employee': [ - dict(fieldname='ifsc_code', - label='IFSC Code', - fieldtype='Data', - insert_after='bank_ac_no', + "Employee": [ + dict( + fieldname="ifsc_code", + label="IFSC Code", + fieldtype="Data", + insert_after="bank_ac_no", print_hide=1, - depends_on='eval:doc.salary_mode == "Bank"' - ), - dict( - fieldname = 'pan_number', - label = 'PAN Number', - fieldtype = 'Data', - insert_after = 'payroll_cost_center', - print_hide = 1 + depends_on='eval:doc.salary_mode == "Bank"', ), dict( - fieldname = 'micr_code', - label = 'MICR Code', - fieldtype = 'Data', - insert_after = 'ifsc_code', - print_hide = 1, - depends_on='eval:doc.salary_mode == "Bank"' + fieldname="pan_number", + label="PAN Number", + fieldtype="Data", + insert_after="payroll_cost_center", + print_hide=1, ), dict( - fieldname = 'provident_fund_account', - label = 'Provident Fund Account', - fieldtype = 'Data', - insert_after = 'pan_number' - ) - + fieldname="micr_code", + label="MICR Code", + fieldtype="Data", + insert_after="ifsc_code", + print_hide=1, + depends_on='eval:doc.salary_mode == "Bank"', + ), + dict( + fieldname="provident_fund_account", + label="Provident Fund Account", + fieldtype="Data", + insert_after="pan_number", + ), ], - 'Company': [ - dict(fieldname='hra_section', label='HRA Settings', - fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), - dict(fieldname='basic_component', label='Basic Component', - fieldtype='Link', options='Salary Component', insert_after='hra_section'), - dict(fieldname='hra_component', label='HRA Component', - fieldtype='Link', options='Salary Component', insert_after='basic_component'), - dict(fieldname='arrear_component', label='Arrear Component', - fieldtype='Link', options='Salary Component', insert_after='hra_component'), - dict(fieldname='non_profit_section', label='Non Profit Settings', - fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), - dict(fieldname='company_80g_number', label='80G Number', - fieldtype='Data', insert_after='non_profit_section'), - dict(fieldname='with_effect_from', label='80G With Effect From', - fieldtype='Date', insert_after='company_80g_number'), - dict(fieldname='pan_details', label='PAN Number', - fieldtype='Data', insert_after='with_effect_from') + "Company": [ + dict( + fieldname="hra_section", + label="HRA Settings", + fieldtype="Section Break", + insert_after="asset_received_but_not_billed", + collapsible=1, + ), + dict( + fieldname="basic_component", + label="Basic Component", + fieldtype="Link", + options="Salary Component", + insert_after="hra_section", + ), + dict( + fieldname="hra_component", + label="HRA Component", + fieldtype="Link", + options="Salary Component", + insert_after="basic_component", + ), + dict( + fieldname="arrear_component", + label="Arrear Component", + fieldtype="Link", + options="Salary Component", + insert_after="hra_component", + ), + dict( + fieldname="non_profit_section", + label="Non Profit Settings", + fieldtype="Section Break", + insert_after="asset_received_but_not_billed", + collapsible=1, + ), + dict( + fieldname="company_80g_number", + label="80G Number", + fieldtype="Data", + insert_after="non_profit_section", + ), + dict( + fieldname="with_effect_from", + label="80G With Effect From", + fieldtype="Date", + insert_after="company_80g_number", + ), + dict( + fieldname="pan_details", label="PAN Number", fieldtype="Data", insert_after="with_effect_from" + ), ], - 'Employee Tax Exemption Declaration':[ - dict(fieldname='hra_section', label='HRA Exemption', - fieldtype='Section Break', insert_after='declarations'), - dict(fieldname='monthly_house_rent', label='Monthly House Rent', - fieldtype='Currency', insert_after='hra_section'), - dict(fieldname='rented_in_metro_city', label='Rented in Metro City', - fieldtype='Check', insert_after='monthly_house_rent', depends_on='monthly_house_rent'), - dict(fieldname='salary_structure_hra', label='HRA as per Salary Structure', - fieldtype='Currency', insert_after='rented_in_metro_city', read_only=1, depends_on='monthly_house_rent'), - dict(fieldname='hra_column_break', fieldtype='Column Break', - insert_after='salary_structure_hra', depends_on='monthly_house_rent'), - dict(fieldname='annual_hra_exemption', label='Annual HRA Exemption', - fieldtype='Currency', insert_after='hra_column_break', read_only=1, depends_on='monthly_house_rent'), - dict(fieldname='monthly_hra_exemption', label='Monthly HRA Exemption', - fieldtype='Currency', insert_after='annual_hra_exemption', read_only=1, depends_on='monthly_house_rent') + "Employee Tax Exemption Declaration": [ + dict( + fieldname="hra_section", + label="HRA Exemption", + fieldtype="Section Break", + insert_after="declarations", + ), + dict( + fieldname="monthly_house_rent", + label="Monthly House Rent", + fieldtype="Currency", + insert_after="hra_section", + ), + dict( + fieldname="rented_in_metro_city", + label="Rented in Metro City", + fieldtype="Check", + insert_after="monthly_house_rent", + depends_on="monthly_house_rent", + ), + dict( + fieldname="salary_structure_hra", + label="HRA as per Salary Structure", + fieldtype="Currency", + insert_after="rented_in_metro_city", + read_only=1, + depends_on="monthly_house_rent", + ), + dict( + fieldname="hra_column_break", + fieldtype="Column Break", + insert_after="salary_structure_hra", + depends_on="monthly_house_rent", + ), + dict( + fieldname="annual_hra_exemption", + label="Annual HRA Exemption", + fieldtype="Currency", + insert_after="hra_column_break", + read_only=1, + depends_on="monthly_house_rent", + ), + dict( + fieldname="monthly_hra_exemption", + label="Monthly HRA Exemption", + fieldtype="Currency", + insert_after="annual_hra_exemption", + read_only=1, + depends_on="monthly_house_rent", + ), ], - 'Employee Tax Exemption Proof Submission': [ - dict(fieldname='hra_section', label='HRA Exemption', - fieldtype='Section Break', insert_after='tax_exemption_proofs'), - dict(fieldname='house_rent_payment_amount', label='House Rent Payment Amount', - fieldtype='Currency', insert_after='hra_section'), - dict(fieldname='rented_in_metro_city', label='Rented in Metro City', - fieldtype='Check', insert_after='house_rent_payment_amount', depends_on='house_rent_payment_amount'), - dict(fieldname='rented_from_date', label='Rented From Date', - fieldtype='Date', insert_after='rented_in_metro_city', depends_on='house_rent_payment_amount'), - dict(fieldname='rented_to_date', label='Rented To Date', - fieldtype='Date', insert_after='rented_from_date', depends_on='house_rent_payment_amount'), - dict(fieldname='hra_column_break', fieldtype='Column Break', - insert_after='rented_to_date', depends_on='house_rent_payment_amount'), - dict(fieldname='monthly_house_rent', label='Monthly House Rent', - fieldtype='Currency', insert_after='hra_column_break', read_only=1, depends_on='house_rent_payment_amount'), - dict(fieldname='monthly_hra_exemption', label='Monthly Eligible Amount', - fieldtype='Currency', insert_after='monthly_house_rent', read_only=1, depends_on='house_rent_payment_amount'), - dict(fieldname='total_eligible_hra_exemption', label='Total Eligible HRA Exemption', - fieldtype='Currency', insert_after='monthly_hra_exemption', read_only=1, depends_on='house_rent_payment_amount') + "Employee Tax Exemption Proof Submission": [ + dict( + fieldname="hra_section", + label="HRA Exemption", + fieldtype="Section Break", + insert_after="tax_exemption_proofs", + ), + dict( + fieldname="house_rent_payment_amount", + label="House Rent Payment Amount", + fieldtype="Currency", + insert_after="hra_section", + ), + dict( + fieldname="rented_in_metro_city", + label="Rented in Metro City", + fieldtype="Check", + insert_after="house_rent_payment_amount", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="rented_from_date", + label="Rented From Date", + fieldtype="Date", + insert_after="rented_in_metro_city", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="rented_to_date", + label="Rented To Date", + fieldtype="Date", + insert_after="rented_from_date", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="hra_column_break", + fieldtype="Column Break", + insert_after="rented_to_date", + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="monthly_house_rent", + label="Monthly House Rent", + fieldtype="Currency", + insert_after="hra_column_break", + read_only=1, + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="monthly_hra_exemption", + label="Monthly Eligible Amount", + fieldtype="Currency", + insert_after="monthly_house_rent", + read_only=1, + depends_on="house_rent_payment_amount", + ), + dict( + fieldname="total_eligible_hra_exemption", + label="Total Eligible HRA Exemption", + fieldtype="Currency", + insert_after="monthly_hra_exemption", + read_only=1, + depends_on="house_rent_payment_amount", + ), ], - 'Supplier': [ + "Supplier": [ { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'insert_after': 'supplier_type', - 'depends_on': 'eval:doc.is_transporter' + "fieldname": "gst_transporter_id", + "label": "GST Transporter ID", + "fieldtype": "Data", + "insert_after": "supplier_type", + "depends_on": "eval:doc.is_transporter", }, { - 'fieldname': 'gst_category', - 'label': 'GST Category', - 'fieldtype': 'Select', - 'insert_after': 'gst_transporter_id', - 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - 'default': 'Unregistered' + "fieldname": "gst_category", + "label": "GST Category", + "fieldtype": "Select", + "insert_after": "gst_transporter_id", + "options": "Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders", + "default": "Unregistered", }, { - 'fieldname': 'export_type', - 'label': 'Export Type', - 'fieldtype': 'Select', - 'insert_after': 'gst_category', - 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)' - } + "fieldname": "export_type", + "label": "Export Type", + "fieldtype": "Select", + "insert_after": "gst_category", + "depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + "options": "\nWith Payment of Tax\nWithout Payment of Tax", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + }, ], - 'Customer': [ + "Customer": [ { - 'fieldname': 'gst_category', - 'label': 'GST Category', - 'fieldtype': 'Select', - 'insert_after': 'customer_type', - 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - 'default': 'Unregistered' + "fieldname": "gst_category", + "label": "GST Category", + "fieldtype": "Select", + "insert_after": "customer_type", + "options": "Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders", + "default": "Unregistered", }, { - 'fieldname': 'export_type', - 'label': 'Export Type', - 'fieldtype': 'Select', - 'insert_after': 'gst_category', - 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - 'options': '\nWith Payment of Tax\nWithout Payment of Tax', - 'mandatory_depends_on': 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)' + "fieldname": "export_type", + "label": "Export Type", + "fieldtype": "Select", + "insert_after": "gst_category", + "depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + "options": "\nWith Payment of Tax\nWithout Payment of Tax", + "mandatory_depends_on": 'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + }, + ], + "Member": [ + { + "fieldname": "pan_number", + "label": "PAN Details", + "fieldtype": "Data", + "insert_after": "email_id", } ], - 'Member': [ + "Donor": [ { - 'fieldname': 'pan_number', - 'label': 'PAN Details', - 'fieldtype': 'Data', - 'insert_after': 'email_id' + "fieldname": "pan_number", + "label": "PAN Details", + "fieldtype": "Data", + "insert_after": "email", } ], - 'Donor': [ + "Finance Book": [ { - 'fieldname': 'pan_number', - 'label': 'PAN Details', - 'fieldtype': 'Data', - 'insert_after': 'email' + "fieldname": "for_income_tax", + "label": "For Income Tax", + "fieldtype": "Check", + "insert_after": "finance_book_name", + "description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.", } ], - 'Finance Book': [ - { - 'fieldname': 'for_income_tax', - 'label': 'For Income Tax', - 'fieldtype': 'Check', - 'insert_after': 'finance_book_name', - 'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.' - } - ] } return custom_fields + def make_fixtures(company=None): docs = [] company = company or frappe.db.get_value("Global Defaults", None, "default_company") @@ -754,33 +1270,47 @@ def make_fixtures(company=None): # create records for Tax Withholding Category set_tax_withholding_category(company) + def update_regional_tax_settings(country, company): # Will only add default GST accounts if present - input_account_names = ['Input Tax CGST', 'Input Tax SGST', 'Input Tax IGST'] - output_account_names = ['Output Tax CGST', 'Output Tax SGST', 'Output Tax IGST'] - rcm_accounts = ['Input Tax CGST RCM', 'Input Tax SGST RCM', 'Input Tax IGST RCM'] - gst_settings = frappe.get_single('GST Settings') + input_account_names = ["Input Tax CGST", "Input Tax SGST", "Input Tax IGST"] + output_account_names = ["Output Tax CGST", "Output Tax SGST", "Output Tax IGST"] + rcm_accounts = ["Input Tax CGST RCM", "Input Tax SGST RCM", "Input Tax IGST RCM"] + gst_settings = frappe.get_single("GST Settings") existing_account_list = [] - for account in gst_settings.get('gst_accounts'): - for key in ['cgst_account', 'sgst_account', 'igst_account']: + for account in gst_settings.get("gst_accounts"): + for key in ["cgst_account", "sgst_account", "igst_account"]: existing_account_list.append(account.get(key)) - gst_accounts = frappe._dict(frappe.get_all("Account", - {'company': company, 'account_name': ('in', input_account_names + - output_account_names + rcm_accounts)}, ['account_name', 'name'], as_list=1)) + gst_accounts = frappe._dict( + frappe.get_all( + "Account", + { + "company": company, + "account_name": ("in", input_account_names + output_account_names + rcm_accounts), + }, + ["account_name", "name"], + as_list=1, + ) + ) - add_accounts_in_gst_settings(company, input_account_names, gst_accounts, - existing_account_list, gst_settings) - add_accounts_in_gst_settings(company, output_account_names, gst_accounts, - existing_account_list, gst_settings) - add_accounts_in_gst_settings(company, rcm_accounts, gst_accounts, - existing_account_list, gst_settings, is_reverse_charge=1) + add_accounts_in_gst_settings( + company, input_account_names, gst_accounts, existing_account_list, gst_settings + ) + add_accounts_in_gst_settings( + company, output_account_names, gst_accounts, existing_account_list, gst_settings + ) + add_accounts_in_gst_settings( + company, rcm_accounts, gst_accounts, existing_account_list, gst_settings, is_reverse_charge=1 + ) gst_settings.save() -def add_accounts_in_gst_settings(company, account_names, gst_accounts, - existing_account_list, gst_settings, is_reverse_charge=0): + +def add_accounts_in_gst_settings( + company, account_names, gst_accounts, existing_account_list, gst_settings, is_reverse_charge=0 +): accounts_not_added = 1 for account in account_names: @@ -793,35 +1323,72 @@ def add_accounts_in_gst_settings(company, account_names, gst_accounts, accounts_not_added = 0 if accounts_not_added: - gst_settings.append('gst_accounts', { - 'company': company, - 'cgst_account': gst_accounts.get(account_names[0]), - 'sgst_account': gst_accounts.get(account_names[1]), - 'igst_account': gst_accounts.get(account_names[2]), - 'is_reverse_charge_account': is_reverse_charge - }) + gst_settings.append( + "gst_accounts", + { + "company": company, + "cgst_account": gst_accounts.get(account_names[0]), + "sgst_account": gst_accounts.get(account_names[1]), + "igst_account": gst_accounts.get(account_names[2]), + "is_reverse_charge_account": is_reverse_charge, + }, + ) + def set_salary_components(docs): - docs.extend([ - {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', - 'description': 'Professional Tax', 'type': 'Deduction', 'exempted_from_income_tax': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', - 'description': 'Provident fund', 'type': 'Deduction', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', - 'description': 'House Rent Allowance', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Basic', - 'description': 'Basic', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Arrear', - 'description': 'Arrear', 'type': 'Earning', 'is_tax_applicable': 1}, - {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', - 'description': 'Leave Encashment', 'type': 'Earning', 'is_tax_applicable': 1} - ]) + docs.extend( + [ + { + "doctype": "Salary Component", + "salary_component": "Professional Tax", + "description": "Professional Tax", + "type": "Deduction", + "exempted_from_income_tax": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Provident Fund", + "description": "Provident fund", + "type": "Deduction", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "House Rent Allowance", + "description": "House Rent Allowance", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Basic", + "description": "Basic", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Arrear", + "description": "Arrear", + "type": "Earning", + "is_tax_applicable": 1, + }, + { + "doctype": "Salary Component", + "salary_component": "Leave Encashment", + "description": "Leave Encashment", + "type": "Earning", + "is_tax_applicable": 1, + }, + ] + ) + def set_tax_withholding_category(company): accounts = [] fiscal_year_details = None abbr = frappe.get_value("Company", company, "abbr") - tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') + tds_account = frappe.get_value("Account", "TDS Payable - {0}".format(abbr), "name") if company and tds_account: accounts = [dict(company=company, account=tds_account)] @@ -848,10 +1415,13 @@ def set_tax_withholding_category(company): if fiscal_year_details: # if fiscal year don't match with any of the already entered data, append rate row - fy_exist = [k for k in doc.get('rates') if k.get('from_date') <= fiscal_year_details[1] \ - and k.get('to_date') >= fiscal_year_details[2]] + fy_exist = [ + k + for k in doc.get("rates") + if k.get("from_date") <= fiscal_year_details[1] and k.get("to_date") >= fiscal_year_details[2] + ] if not fy_exist: - doc.append("rates", d.get('rates')[0]) + doc.append("rates", d.get("rates")[0]) doc.flags.ignore_permissions = True doc.flags.ignore_validate = True @@ -859,164 +1429,451 @@ def set_tax_withholding_category(company): doc.flags.ignore_links = True doc.save() + def set_tds_account(docs, company): - parent_account = frappe.db.get_value("Account", filters = {"account_name": "Duties and Taxes", "company": company}) + parent_account = frappe.db.get_value( + "Account", filters={"account_name": "Duties and Taxes", "company": company} + ) if parent_account: - docs.extend([ - { - "doctype": "Account", - "account_name": "TDS Payable", - "account_type": "Tax", - "parent_account": parent_account, - "company": company - } - ]) + docs.extend( + [ + { + "doctype": "Account", + "account_name": "TDS Payable", + "account_type": "Tax", + "parent_account": parent_account, + "company": company, + } + ] + ) + def get_tds_details(accounts, fiscal_year_details): # bootstrap default tax withholding sections return [ - dict(name="TDS - 194C - Company", + dict( + name="TDS - 194C - Company", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194C - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194C - Individual", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194C - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194C - No PAN / Invalid PAN", category_name="Payment to Contractors (Single / Aggregate)", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 100000}]), - dict(name="TDS - 194D - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 30000, + "cumulative_threshold": 100000, + } + ], + ), + dict( + name="TDS - 194D - Company", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - Company Assessee", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - Company Assessee", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - Individual", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194D - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194D - No PAN / Invalid PAN", category_name="Insurance Commission", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - Company", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - Individual", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 1, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194DA - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 1, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194DA - No PAN / Invalid PAN", category_name="Non-exempt payments made under a life insurance policy", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 100000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 100000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - Company", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - Individual", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194H - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194H - No PAN / Invalid PAN", category_name="Commission / Brokerage", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 15000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - Company", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - Individual", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent - No PAN / Invalid PAN", category_name="Rent", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - Company", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - Individual", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 2, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 2, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194I - Rent/Machinery - No PAN / Invalid PAN", category_name="Rent-Plant / Machinery", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 180000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 180000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - Company", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - Individual", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Professional Fees - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Professional Fees - No PAN / Invalid PAN", category_name="Professional Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 30000, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 30000, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - Company", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - Individual", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194J - Director Fees - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194J - Director Fees - No PAN / Invalid PAN", category_name="Director Fees", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 0, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - Company", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 0, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - Company", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - Individual", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - Individual", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 10, "single_threshold": 2500, "cumulative_threshold": 0}]), - dict(name="TDS - 194 - Dividends - No PAN / Invalid PAN", + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), + dict( + name="TDS - 194 - Dividends - No PAN / Invalid PAN", category_name="Dividends", - doctype="Tax Withholding Category", accounts=accounts, - rates=[{"from_date": fiscal_year_details[1], "to_date": fiscal_year_details[2], - "tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}]) + doctype="Tax Withholding Category", + accounts=accounts, + rates=[ + { + "from_date": fiscal_year_details[1], + "to_date": fiscal_year_details[2], + "tax_withholding_rate": 20, + "single_threshold": 2500, + "cumulative_threshold": 0, + } + ], + ), ] + def create_gratuity_rule(): # Standard Indain Gratuity Rule if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"): @@ -1026,16 +1883,16 @@ def create_gratuity_rule(): rule.work_experience_calculation_method = "Round Off Work Experience" rule.minimum_year_for_gratuity = 5 - fraction = 15/26 - rule.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":0, - "fraction_of_applicable_earnings": fraction - }) + fraction = 15 / 26 + rule.append( + "gratuity_rule_slabs", + {"from_year": 0, "to_year": 0, "fraction_of_applicable_earnings": fraction}, + ) rule.flags.ignore_mandatory = True rule.save() + def update_accounts_settings_for_taxes(): - if frappe.db.count('Company') == 1: - frappe.db.set_value('Accounts Settings', None, "add_taxes_from_item_tax_template", 0) + if frappe.db.count("Company") == 1: + frappe.db.set_value("Accounts Settings", None, "add_taxes_from_item_tax_template", 0) diff --git a/erpnext/regional/india/test_utils.py b/erpnext/regional/india/test_utils.py index 61a0e97fe3e..5c248307ec0 100644 --- a/erpnext/regional/india/test_utils.py +++ b/erpnext/regional/india/test_utils.py @@ -1,4 +1,3 @@ - import unittest from unittest.mock import patch @@ -13,14 +12,12 @@ class TestIndiaUtils(unittest.TestCase): mock_get_cached.return_value = "India" # mock country posting_date = "2021-05-01" - invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05", - "SI.2020.0001", "PI2021 - 001"] + invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05", "SI.2020.0001", "PI2021 - 001"] for name in invalid_names: doc = frappe._dict(name=name, posting_date=posting_date) self.assertRaises(frappe.ValidationError, validate_document_name, doc) - valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001", - "2020-PI-0001", "PI2020-0001"] + valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001", "2020-PI-0001", "PI2020-0001"] for name in valid_names: doc = frappe._dict(name=name, posting_date=posting_date) try: diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 12ad9f1c937..f6f2f8d4934 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,4 +1,3 @@ - import json import re @@ -14,43 +13,51 @@ from erpnext.hr.utils import get_salary_assignment from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.india import number_state_mapping, state_numbers, states -GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - / -GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$") +GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") # alphanumeric and - / +GSTIN_FORMAT = re.compile( + "^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$" +) GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}") PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}") def validate_gstin_for_india(doc, method): - if hasattr(doc, 'gst_state'): + if hasattr(doc, "gst_state"): set_gst_state_and_state_number(doc) - if not hasattr(doc, 'gstin') or not doc.gstin: + if not hasattr(doc, "gstin") or not doc.gstin: return gst_category = [] - if hasattr(doc, 'gst_category'): + if hasattr(doc, "gst_category"): if len(doc.links): link_doctype = doc.links[0].get("link_doctype") link_name = doc.links[0].get("link_name") if link_doctype in ["Customer", "Supplier"]: - gst_category = frappe.db.get_value(link_doctype, {'name': link_name}, ['gst_category']) + gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"]) doc.gstin = doc.gstin.upper().strip() - if not doc.gstin or doc.gstin == 'NA': + if not doc.gstin or doc.gstin == "NA": return if len(doc.gstin) != 15: frappe.throw(_("A GSTIN must have 15 characters."), title=_("Invalid GSTIN")) - if gst_category and gst_category == 'UIN Holders': + if gst_category and gst_category == "UIN Holders": if not GSTIN_UIN_FORMAT.match(doc.gstin): - frappe.throw(_("The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"), - title=_("Invalid GSTIN")) + frappe.throw( + _( + "The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers" + ), + title=_("Invalid GSTIN"), + ) else: if not GSTIN_FORMAT.match(doc.gstin): - frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN")) + frappe.throw( + _("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN") + ) validate_gstin_check_digit(doc.gstin) @@ -58,46 +65,68 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("Please enter GST state"), title=_("Invalid State")) if doc.gst_state_number != doc.gstin[:2]: - frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.") - .format(doc.gst_state_number), title=_("Invalid GSTIN")) + frappe.throw( + _("First 2 digits of GSTIN should match with State number {0}.").format(doc.gst_state_number), + title=_("Invalid GSTIN"), + ) + def validate_pan_for_india(doc, method): - if doc.get('country') != 'India' or not doc.pan: + if doc.get("country") != "India" or not doc.pan: return if not PAN_NUMBER_FORMAT.match(doc.pan): frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN.")) + def validate_tax_category(doc, method): - if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state, - 'is_reverse_charge': doc.is_reverse_charge}): + if doc.get("gst_state") and frappe.db.get_value( + "Tax Category", + { + "gst_state": doc.gst_state, + "is_inter_state": doc.is_inter_state, + "is_reverse_charge": doc.is_reverse_charge, + }, + ): if doc.is_inter_state: - frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state)) + frappe.throw( + _("Inter State tax category for GST State {0} already exists").format(doc.gst_state) + ) else: - frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state)) + frappe.throw( + _("Intra State tax category for GST State {0} already exists").format(doc.gst_state) + ) + def update_gst_category(doc, method): for link in doc.links: - if link.link_doctype in ['Customer', 'Supplier']: + if link.link_doctype in ["Customer", "Supplier"]: meta = frappe.get_meta(link.link_doctype) - if doc.get('gstin') and meta.has_field('gst_category'): - frappe.db.set_value(link.link_doctype, {'name': link.link_name, 'gst_category': 'Unregistered'}, 'gst_category', 'Registered Regular') + if doc.get("gstin") and meta.has_field("gst_category"): + frappe.db.set_value( + link.link_doctype, + {"name": link.link_name, "gst_category": "Unregistered"}, + "gst_category", + "Registered Regular", + ) + def set_gst_state_and_state_number(doc): if not doc.gst_state and doc.state: state = doc.state.lower() - states_lowercase = {s.lower():s for s in states} + states_lowercase = {s.lower(): s for s in states} if state in states_lowercase: doc.gst_state = states_lowercase[state] else: return doc.gst_state_number = state_numbers.get(doc.gst_state) -def validate_gstin_check_digit(gstin, label='GSTIN'): - ''' Function to validate the check digit of the GSTIN.''' + +def validate_gstin_check_digit(gstin, label="GSTIN"): + """Function to validate the check digit of the GSTIN.""" factor = 1 total = 0 - code_point_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + code_point_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" mod = len(code_point_chars) input_chars = gstin[:-1] for char in input_chars: @@ -106,24 +135,30 @@ def validate_gstin_check_digit(gstin, label='GSTIN'): total += digit factor = 2 if factor == 1 else 1 if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: - frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) + frappe.throw( + _( + """Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""" + ).format(label) + ) + def get_itemised_tax_breakup_header(item_doctype, tax_accounts): - hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup') - if frappe.get_meta(item_doctype).has_field('gst_hsn_code') and hsn_wise_in_gst_settings: + hsn_wise_in_gst_settings = frappe.db.get_single_value("GST Settings", "hsn_wise_tax_breakup") + if frappe.get_meta(item_doctype).has_field("gst_hsn_code") and hsn_wise_in_gst_settings: return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts else: return [_("Item"), _("Taxable Amount")] + tax_accounts + def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise) itemised_taxable_amount = get_itemised_taxable_amount(doc.items) - if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'): + if not frappe.get_meta(doc.doctype + " Item").has_field("gst_hsn_code"): return itemised_tax, itemised_taxable_amount - hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup') + hsn_wise_in_gst_settings = frappe.db.get_single_value("GST Settings", "hsn_wise_tax_breakup") tax_breakup_hsn_wise = hsn_wise or hsn_wise_in_gst_settings if tax_breakup_hsn_wise: @@ -138,7 +173,7 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): for tax_desc, tax_detail in taxes.items(): key = tax_desc if account_wise: - key = tax_detail.get('tax_account') + key = tax_detail.get("tax_account") hsn_tax[item_or_hsn].setdefault(key, {"tax_rate": 0, "tax_amount": 0}) hsn_tax[item_or_hsn][key]["tax_rate"] = tax_detail.get("tax_rate") hsn_tax[item_or_hsn][key]["tax_amount"] += tax_detail.get("tax_amount") @@ -152,9 +187,11 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): return hsn_tax, hsn_taxable_amount + def set_place_of_supply(doc, method=None): doc.place_of_supply = get_place_of_supply(doc, doc.doctype) + def validate_document_name(doc, method=None): """Validate GST invoice number requirements.""" @@ -165,18 +202,29 @@ def validate_document_name(doc, method=None): return if len(doc.name) > 16: - frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series.")) + frappe.throw( + _( + "Maximum length of document number should be 16 characters as per GST rules. Please change the naming series." + ) + ) if not GST_INVOICE_NUMBER_FORMAT.match(doc.name): - frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series.")) + frappe.throw( + _( + "Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series." + ) + ) + # don't remove this function it is used in tests def test_method(): - '''test function''' - return 'overridden' + """test function""" + return "overridden" + def get_place_of_supply(party_details, doctype): - if not frappe.get_meta('Address').has_field('gst_state'): return + if not frappe.get_meta("Address").has_field("gst_state"): + return if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): address_name = party_details.customer_address or party_details.shipping_address_name @@ -184,11 +232,14 @@ def get_place_of_supply(party_details, doctype): address_name = party_details.shipping_address or party_details.supplier_address if address_name: - address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number", "gstin"], as_dict=1) + address = frappe.db.get_value( + "Address", address_name, ["gst_state", "gst_state_number", "gstin"], as_dict=1 + ) if address and address.gst_state and address.gst_state_number: party_details.gstin = address.gstin return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) + @frappe.whitelist() def get_regional_address_details(party_details, doctype, company): if isinstance(party_details, string_types): @@ -200,28 +251,40 @@ def get_regional_address_details(party_details, doctype, company): party_details.place_of_supply = get_place_of_supply(party_details, doctype) if is_internal_transfer(party_details, doctype): - party_details.taxes_and_charges = '' + party_details.taxes_and_charges = "" party_details.taxes = [] return party_details if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): master_doctype = "Sales Taxes and Charges Template" - tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category( + master_doctype, company, party_details + ) elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): master_doctype = "Purchase Taxes and Charges Template" - tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details) + tax_template_by_category = get_tax_template_based_on_category( + master_doctype, company, party_details + ) if tax_template_by_category: - party_details['taxes_and_charges'] = tax_template_by_category + party_details["taxes_and_charges"] = tax_template_by_category return party_details - if not party_details.place_of_supply: return party_details - if not party_details.company_gstin: return party_details + if not party_details.place_of_supply: + return party_details + if not party_details.company_gstin: + return party_details - if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin - and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", - "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): + if ( + doctype in ("Sales Invoice", "Delivery Note", "Sales Order") + and party_details.company_gstin + and party_details.company_gstin[:2] != party_details.place_of_supply[:2] + ) or ( + doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt") + and party_details.supplier_gstin + and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2] + ): default_tax = get_tax_template(master_doctype, company, 1, party_details.company_gstin[:2]) else: default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) @@ -234,11 +297,19 @@ def get_regional_address_details(party_details, doctype, company): return party_details + def update_party_details(party_details, doctype): - for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']: + for address_field in [ + "shipping_address", + "company_address", + "supplier_address", + "shipping_address_name", + "customer_address", + ]: if party_details.get(address_field): party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field))) + def is_internal_transfer(party_details, doctype): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation"): destination_gstin = party_details.company_gstin @@ -253,66 +324,93 @@ def is_internal_transfer(party_details, doctype): else: False + def get_tax_template_based_on_category(master_doctype, company, party_details): - if not party_details.get('tax_category'): + if not party_details.get("tax_category"): return - default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, - 'name') + default_tax = frappe.db.get_value( + master_doctype, {"company": company, "tax_category": party_details.get("tax_category")}, "name" + ) return default_tax + def get_tax_template(master_doctype, company, is_inter_state, state_code): - tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], - filters = {'is_inter_state': is_inter_state, 'is_reverse_charge': 0}) + tax_categories = frappe.get_all( + "Tax Category", + fields=["name", "is_inter_state", "gst_state"], + filters={"is_inter_state": is_inter_state, "is_reverse_charge": 0}, + ) - default_tax = '' + default_tax = "" for tax_category in tax_categories: - if tax_category.gst_state == number_state_mapping[state_code] or \ - (not default_tax and not tax_category.gst_state): - default_tax = frappe.db.get_value(master_doctype, - {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') + if tax_category.gst_state == number_state_mapping[state_code] or ( + not default_tax and not tax_category.gst_state + ): + default_tax = frappe.db.get_value( + master_doctype, {"company": company, "disabled": 0, "tax_category": tax_category.name}, "name" + ) return default_tax + def calculate_annual_eligible_hra_exemption(doc): - basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"]) + basic_component, hra_component = frappe.db.get_value( + "Company", doc.company, ["basic_component", "hra_component"] + ) if not (basic_component and hra_component): frappe.throw(_("Please mention Basic and HRA component in Company")) annual_exemption, monthly_exemption, hra_amount = 0, 0, 0 if hra_component and basic_component: assignment = get_salary_assignment(doc.employee, nowdate()) if assignment: - hra_component_exists = frappe.db.exists("Salary Detail", { - "parent": assignment.salary_structure, - "salary_component": hra_component, - "parentfield": "earnings", - "parenttype": "Salary Structure" - }) + hra_component_exists = frappe.db.exists( + "Salary Detail", + { + "parent": assignment.salary_structure, + "salary_component": hra_component, + "parentfield": "earnings", + "parenttype": "Salary Structure", + }, + ) if hra_component_exists: - basic_amount, hra_amount = get_component_amt_from_salary_slip(doc.employee, - assignment.salary_structure, basic_component, hra_component) + basic_amount, hra_amount = get_component_amt_from_salary_slip( + doc.employee, assignment.salary_structure, basic_component, hra_component + ) if hra_amount: if doc.monthly_house_rent: - annual_exemption = calculate_hra_exemption(assignment.salary_structure, - basic_amount, hra_amount, doc.monthly_house_rent, doc.rented_in_metro_city) + annual_exemption = calculate_hra_exemption( + assignment.salary_structure, + basic_amount, + hra_amount, + doc.monthly_house_rent, + doc.rented_in_metro_city, + ) if annual_exemption > 0: monthly_exemption = annual_exemption / 12 else: annual_exemption = 0 elif doc.docstatus == 1: - frappe.throw(_("Salary Structure must be submitted before submission of Tax Ememption Declaration")) + frappe.throw( + _("Salary Structure must be submitted before submission of Tax Ememption Declaration") + ) + + return frappe._dict( + { + "hra_amount": hra_amount, + "annual_exemption": annual_exemption, + "monthly_exemption": monthly_exemption, + } + ) - return frappe._dict({ - "hra_amount": hra_amount, - "annual_exemption": annual_exemption, - "monthly_exemption": monthly_exemption - }) def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): - salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1, ignore_permissions=True) + salary_slip = make_salary_slip( + salary_structure, employee=employee, for_preview=1, ignore_permissions=True + ) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: @@ -323,7 +421,10 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone return basic_amt, hra_amt return basic_amt, hra_amt -def calculate_hra_exemption(salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city): + +def calculate_hra_exemption( + salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city +): # TODO make this configurable exemptions = [] frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency") @@ -340,6 +441,7 @@ def calculate_hra_exemption(salary_structure, basic, monthly_hra, monthly_house_ # return minimum of 3 cases return min(exemptions) + def get_annual_component_pay(frequency, amount): if frequency == "Daily": return amount * 365 @@ -352,6 +454,7 @@ def get_annual_component_pay(frequency, amount): elif frequency == "Bimonthly": return amount * 6 + def validate_house_rent_dates(doc): if not doc.rented_to_date or not doc.rented_from_date: frappe.throw(_("House rented dates required for exemption calculation")) @@ -359,30 +462,34 @@ def validate_house_rent_dates(doc): if date_diff(doc.rented_to_date, doc.rented_from_date) < 14: frappe.throw(_("House rented dates should be atleast 15 days apart")) - proofs = frappe.db.sql(""" + proofs = frappe.db.sql( + """ select name from `tabEmployee Tax Exemption Proof Submission` where docstatus=1 and employee=%(employee)s and payroll_period=%(payroll_period)s and (rented_from_date between %(from_date)s and %(to_date)s or rented_to_date between %(from_date)s and %(to_date)s) - """, { - "employee": doc.employee, - "payroll_period": doc.payroll_period, - "from_date": doc.rented_from_date, - "to_date": doc.rented_to_date - }) + """, + { + "employee": doc.employee, + "payroll_period": doc.payroll_period, + "from_date": doc.rented_from_date, + "to_date": doc.rented_to_date, + }, + ) if proofs: frappe.throw(_("House rent paid days overlapping with {0}").format(proofs[0][0])) + def calculate_hra_exemption_for_period(doc): monthly_rent, eligible_hra = 0, 0 if doc.house_rent_payment_amount: validate_house_rent_dates(doc) # TODO receive rented months or validate dates are start and end of months? # Calc monthly rent, round to nearest .5 - factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1)/30 - factor = round(factor * 2)/2 + factor = flt(date_diff(doc.rented_to_date, doc.rented_from_date) + 1) / 30 + factor = round(factor * 2) / 2 monthly_rent = doc.house_rent_payment_amount / factor # update field used by calculate_annual_eligible_hra_exemption doc.monthly_house_rent = monthly_rent @@ -395,6 +502,7 @@ def calculate_hra_exemption_for_period(doc): exemptions["total_eligible_hra_exemption"] = eligible_hra return exemptions + def get_ewb_data(dt, dn): ewaybills = [] @@ -403,32 +511,38 @@ def get_ewb_data(dt, dn): validate_doc(doc) - data = frappe._dict({ - "transporterId": "", - "TotNonAdvolVal": 0, - }) + data = frappe._dict( + { + "transporterId": "", + "TotNonAdvolVal": 0, + } + ) data.userGstin = data.fromGstin = doc.company_gstin - data.supplyType = 'O' + data.supplyType = "O" - if dt == 'Delivery Note': + if dt == "Delivery Note": data.subSupplyType = 1 - elif doc.gst_category in ['Registered Regular', 'SEZ']: + elif doc.gst_category in ["Registered Regular", "SEZ"]: data.subSupplyType = 1 - elif doc.gst_category in ['Overseas', 'Deemed Export']: + elif doc.gst_category in ["Overseas", "Deemed Export"]: data.subSupplyType = 3 else: - frappe.throw(_('Unsupported GST Category for E-Way Bill JSON generation')) + frappe.throw(_("Unsupported GST Category for E-Way Bill JSON generation")) - data.docType = 'INV' - data.docDate = frappe.utils.formatdate(doc.posting_date, 'dd/mm/yyyy') + data.docType = "INV" + data.docDate = frappe.utils.formatdate(doc.posting_date, "dd/mm/yyyy") - company_address = frappe.get_doc('Address', doc.company_address) - billing_address = frappe.get_doc('Address', doc.customer_address) + company_address = frappe.get_doc("Address", doc.company_address) + billing_address = frappe.get_doc("Address", doc.customer_address) - #added dispatch address - dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address - shipping_address = frappe.get_doc('Address', doc.shipping_address_name) + # added dispatch address + dispatch_address = ( + frappe.get_doc("Address", doc.dispatch_address_name) + if doc.dispatch_address_name + else company_address + ) + shipping_address = frappe.get_doc("Address", doc.shipping_address_name) data = get_address_details(data, doc, company_address, billing_address, dispatch_address) @@ -437,75 +551,78 @@ def get_ewb_data(dt, dn): data = get_item_list(data, doc, hsn_wise=True) - disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') + disable_rounded = frappe.db.get_single_value("Global Defaults", "disable_rounded_total") data.totInvValue = doc.grand_total if disable_rounded else doc.rounded_total data = get_transport_details(data, doc) fields = { "/. -": { - 'docNo': doc.name, - 'fromTrdName': doc.company, - 'toTrdName': doc.customer_name, - 'transDocNo': doc.lr_no, + "docNo": doc.name, + "fromTrdName": doc.company, + "toTrdName": doc.customer_name, + "transDocNo": doc.lr_no, }, "@#/,&. -": { - 'fromAddr1': company_address.address_line1, - 'fromAddr2': company_address.address_line2, - 'fromPlace': company_address.city, - 'toAddr1': shipping_address.address_line1, - 'toAddr2': shipping_address.address_line2, - 'toPlace': shipping_address.city, - 'transporterName': doc.transporter_name - } + "fromAddr1": company_address.address_line1, + "fromAddr2": company_address.address_line2, + "fromPlace": company_address.city, + "toAddr1": shipping_address.address_line1, + "toAddr2": shipping_address.address_line2, + "toPlace": shipping_address.city, + "transporterName": doc.transporter_name, + }, } for allowed_chars, field_map in fields.items(): for key, value in field_map.items(): if not value: - data[key] = '' + data[key] = "" else: - data[key] = re.sub(r'[^\w' + allowed_chars + ']', '', value) + data[key] = re.sub(r"[^\w" + allowed_chars + "]", "", value) ewaybills.append(data) - data = { - 'version': '1.0.0421', - 'billLists': ewaybills - } + data = {"version": "1.0.0421", "billLists": ewaybills} return data + @frappe.whitelist() def generate_ewb_json(dt, dn): dn = json.loads(dn) return get_ewb_data(dt, dn) + @frappe.whitelist() def download_ewb_json(): data = json.loads(frappe.local.form_dict.data) frappe.local.response.filecontent = json.dumps(data, indent=4, sort_keys=True) - frappe.local.response.type = 'download' + frappe.local.response.type = "download" - filename_prefix = 'Bulk' + filename_prefix = "Bulk" docname = frappe.local.form_dict.docname if docname: - if docname.startswith('['): + if docname.startswith("["): docname = json.loads(docname) if len(docname) == 1: docname = docname[0] if not isinstance(docname, list): # removes characters not allowed in a filename (https://stackoverflow.com/a/38766141/4767738) - filename_prefix = re.sub(r'[^\w_.)( -]', '', docname) + filename_prefix = re.sub(r"[^\w_.)( -]", "", docname) + + frappe.local.response.filename = "{0}_e-WayBill_Data_{1}.json".format( + filename_prefix, frappe.utils.random_string(5) + ) - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(filename_prefix, frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): - company_gstins =[] + company_gstins = [] if company: - company_gstins = frappe.db.sql("""select + company_gstins = frappe.db.sql( + """select distinct `tabAddress`.gstin from `tabAddress`, `tabDynamic Link` @@ -513,56 +630,66 @@ def get_gstins_for_company(company): `tabDynamic Link`.parent = `tabAddress`.name and `tabDynamic Link`.parenttype = 'Address' and `tabDynamic Link`.link_doctype = 'Company' and - `tabDynamic Link`.link_name = %(company)s""", {"company": company}) + `tabDynamic Link`.link_name = %(company)s""", + {"company": company}, + ) return company_gstins + def get_address_details(data, doc, company_address, billing_address, dispatch_address): - data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') - data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address') - data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address') + data.fromPincode = validate_pincode(company_address.pincode, "Company Address") + data.fromStateCode = validate_state_code(company_address.gst_state_number, "Company Address") + data.actualFromStateCode = validate_state_code( + dispatch_address.gst_state_number, "Dispatch Address" + ) if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: - data.toGstin = 'URP' + data.toGstin = "URP" set_gst_state_and_state_number(billing_address) else: data.toGstin = doc.billing_address_gstin - data.toPincode = validate_pincode(billing_address.pincode, 'Customer Address') - data.toStateCode = validate_state_code(billing_address.gst_state_number, 'Customer Address') + data.toPincode = validate_pincode(billing_address.pincode, "Customer Address") + data.toStateCode = validate_state_code(billing_address.gst_state_number, "Customer Address") if doc.customer_address != doc.shipping_address_name: data.transType = 2 - shipping_address = frappe.get_doc('Address', doc.shipping_address_name) + shipping_address = frappe.get_doc("Address", doc.shipping_address_name) set_gst_state_and_state_number(shipping_address) - data.toPincode = validate_pincode(shipping_address.pincode, 'Shipping Address') - data.actualToStateCode = validate_state_code(shipping_address.gst_state_number, 'Shipping Address') + data.toPincode = validate_pincode(shipping_address.pincode, "Shipping Address") + data.actualToStateCode = validate_state_code( + shipping_address.gst_state_number, "Shipping Address" + ) else: data.transType = 1 data.actualToStateCode = data.toStateCode shipping_address = billing_address - if doc.gst_category == 'SEZ': + if doc.gst_category == "SEZ": data.toStateCode = 99 return data + def get_item_list(data, doc, hsn_wise=False): - for attr in ['cgstValue', 'sgstValue', 'igstValue', 'cessValue', 'OthValue']: + for attr in ["cgstValue", "sgstValue", "igstValue", "cessValue", "OthValue"]: data[attr] = 0 gst_accounts = get_gst_accounts(doc.company, account_wise=True) tax_map = { - 'sgst_account': ['sgstRate', 'sgstValue'], - 'cgst_account': ['cgstRate', 'cgstValue'], - 'igst_account': ['igstRate', 'igstValue'], - 'cess_account': ['cessRate', 'cessValue'] + "sgst_account": ["sgstRate", "sgstValue"], + "cgst_account": ["cgstRate", "cgstValue"], + "igst_account": ["igstRate", "igstValue"], + "cess_account": ["cessRate", "cessValue"], } - 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) + 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 item_or_hsn, taxable_amount in hsn_taxable_amount.items(): item_data = frappe._dict() if not item_or_hsn: - frappe.throw(_('GST HSN Code does not exist for one or more items')) + frappe.throw(_("GST HSN Code does not exist for one or more items")) item_data.hsnCode = int(item_or_hsn) if hsn_wise else item_or_hsn item_data.taxableAmount = taxable_amount item_data.qtyUnit = "" @@ -570,87 +697,89 @@ def get_item_list(data, doc, hsn_wise=False): item_data[attr] = 0 for account, tax_detail in hsn_wise_charges.get(item_or_hsn, {}).items(): - account_type = gst_accounts.get(account, '') + account_type = gst_accounts.get(account, "") for tax_acc, attrs in tax_map.items(): if account_type == tax_acc: - item_data[attrs[0]] = tax_detail.get('tax_rate') - data[attrs[1]] += tax_detail.get('tax_amount') + item_data[attrs[0]] = tax_detail.get("tax_rate") + data[attrs[1]] += tax_detail.get("tax_amount") break else: - data.OthValue += tax_detail.get('tax_amount') + data.OthValue += tax_detail.get("tax_amount") data.itemList.append(item_data) # Tax amounts rounded to 2 decimals to avoid exceeding max character limit - for attr in ['sgstValue', 'cgstValue', 'igstValue', 'cessValue']: + for attr in ["sgstValue", "cgstValue", "igstValue", "cessValue"]: data[attr] = flt(data[attr], 2) return data + def validate_doc(doc): if doc.docstatus != 1: - frappe.throw(_('E-Way Bill JSON can only be generated from submitted document')) + frappe.throw(_("E-Way Bill JSON can only be generated from submitted document")) if doc.is_return: - frappe.throw(_('E-Way Bill JSON cannot be generated for Sales Return as of now')) + frappe.throw(_("E-Way Bill JSON cannot be generated for Sales Return as of now")) if doc.ewaybill: - frappe.throw(_('e-Way Bill already exists for this document')) + frappe.throw(_("e-Way Bill already exists for this document")) - reqd_fields = ['company_gstin', 'company_address', 'customer_address', - 'shipping_address_name', 'mode_of_transport', 'distance'] + reqd_fields = [ + "company_gstin", + "company_address", + "customer_address", + "shipping_address_name", + "mode_of_transport", + "distance", + ] for fieldname in reqd_fields: if not doc.get(fieldname): - frappe.throw(_('{} is required to generate E-Way Bill JSON').format( - doc.meta.get_label(fieldname) - )) + frappe.throw( + _("{} is required to generate E-Way Bill JSON").format(doc.meta.get_label(fieldname)) + ) if len(doc.company_gstin) < 15: - frappe.throw(_('You must be a registered supplier to generate e-Way Bill')) + frappe.throw(_("You must be a registered supplier to generate e-Way Bill")) + def get_transport_details(data, doc): if doc.distance > 4000: - frappe.throw(_('Distance cannot be greater than 4000 kms')) + frappe.throw(_("Distance cannot be greater than 4000 kms")) data.transDistance = int(round(doc.distance)) - transport_modes = { - 'Road': 1, - 'Rail': 2, - 'Air': 3, - 'Ship': 4 - } + transport_modes = {"Road": 1, "Rail": 2, "Air": 3, "Ship": 4} - vehicle_types = { - 'Regular': 'R', - 'Over Dimensional Cargo (ODC)': 'O' - } + vehicle_types = {"Regular": "R", "Over Dimensional Cargo (ODC)": "O"} data.transMode = transport_modes.get(doc.mode_of_transport) - if doc.mode_of_transport == 'Road': + if doc.mode_of_transport == "Road": if not doc.gst_transporter_id and not doc.vehicle_no: - frappe.throw(_('Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road')) + frappe.throw( + _("Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road") + ) if doc.vehicle_no: - data.vehicleNo = doc.vehicle_no.replace(' ', '') + data.vehicleNo = doc.vehicle_no.replace(" ", "") if not doc.gst_vehicle_type: - frappe.throw(_('Vehicle Type is required if Mode of Transport is Road')) + frappe.throw(_("Vehicle Type is required if Mode of Transport is Road")) else: data.vehicleType = vehicle_types.get(doc.gst_vehicle_type) else: if not doc.lr_no or not doc.lr_date: - frappe.throw(_('Transport Receipt No and Date are mandatory for your chosen Mode of Transport')) + frappe.throw(_("Transport Receipt No and Date are mandatory for your chosen Mode of Transport")) if doc.lr_no: data.transDocNo = doc.lr_no if doc.lr_date: - data.transDocDate = frappe.utils.formatdate(doc.lr_date, 'dd/mm/yyyy') + data.transDocDate = frappe.utils.formatdate(doc.lr_date, "dd/mm/yyyy") if doc.gst_transporter_id: if doc.gst_transporter_id[0:2] != "88": - validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') + validate_gstin_check_digit(doc.gst_transporter_id, label="GST Transporter ID") data.transporterId = doc.gst_transporter_id return data @@ -663,12 +792,13 @@ def validate_pincode(pincode, address): if not pincode: frappe.throw(_(pin_not_found.format(address))) - pincode = pincode.replace(' ', '') + pincode = pincode.replace(" ", "") if not pincode.isdigit() or len(pincode) != 6: frappe.throw(_(incorrect_pin.format(address))) else: return int(pincode) + def validate_state_code(state_code, address): no_state_code = "GST State Code not found for {0}. Please set GST State in {0}" if not state_code: @@ -676,21 +806,26 @@ def validate_state_code(state_code, address): else: return int(state_code) + @frappe.whitelist() -def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0): - filters={"parent": "GST Settings"} +def get_gst_accounts( + company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0 +): + filters = {"parent": "GST Settings"} if company: - filters.update({'company': company}) + filters.update({"company": company}) if only_reverse_charge: - filters.update({'is_reverse_charge_account': 1}) + filters.update({"is_reverse_charge_account": 1}) elif only_non_reverse_charge: - filters.update({'is_reverse_charge_account': 0}) + filters.update({"is_reverse_charge_account": 0}) gst_accounts = frappe._dict() - gst_settings_accounts = frappe.get_all("GST Account", + gst_settings_accounts = frappe.get_all( + "GST Account", filters=filters, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) + fields=["cgst_account", "sgst_account", "igst_account", "cess_account"], + ) if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -704,32 +839,39 @@ def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, on return gst_accounts -def validate_reverse_charge_transaction(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def validate_reverse_charge_transaction(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return base_gst_tax = 0 base_reverse_charge_booked = 0 - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1) - reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ - + gst_accounts.get('igst_account') + reverse_charge_accounts = ( + gst_accounts.get("cgst_account") + + gst_accounts.get("sgst_account") + + gst_accounts.get("igst_account") + ) gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1) - non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ - + gst_accounts.get('igst_account') + non_reverse_charge_accounts = ( + gst_accounts.get("cgst_account") + + gst_accounts.get("sgst_account") + + gst_accounts.get("igst_account") + ) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.account_head in non_reverse_charge_accounts: - if tax.add_deduct_tax == 'Add': + if tax.add_deduct_tax == "Add": base_gst_tax += tax.base_tax_amount_after_discount_amount else: base_gst_tax += tax.base_tax_amount_after_discount_amount elif tax.account_head in reverse_charge_accounts: - if tax.add_deduct_tax == 'Add': + if tax.add_deduct_tax == "Add": base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount else: base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount @@ -737,57 +879,65 @@ def validate_reverse_charge_transaction(doc, method): if base_gst_tax != base_reverse_charge_booked: msg = _("Booked reverse charge is not equal to applied tax amount") msg += "
    " - msg += _("Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice").format( - gst_document_link='GST Documentation') + msg += _( + "Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice" + ).format( + gst_document_link='GST Documentation' + ) frappe.throw(msg) -def update_itc_availed_fields(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def update_itc_availed_fields(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return # Initialize values doc.itc_integrated_tax = doc.itc_state_tax = doc.itc_central_tax = doc.itc_cess_amount = 0 gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1) - for tax in doc.get('taxes'): - if tax.account_head in gst_accounts.get('igst_account', []): + for tax in doc.get("taxes"): + if tax.account_head in gst_accounts.get("igst_account", []): doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('sgst_account', []): + if tax.account_head in gst_accounts.get("sgst_account", []): doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('cgst_account', []): + if tax.account_head in gst_accounts.get("cgst_account", []): doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount) - if tax.account_head in gst_accounts.get('cess_account', []): + if tax.account_head in gst_accounts.get("cess_account", []): doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount) + def update_place_of_supply(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': + country = frappe.get_cached_value("Company", doc.company, "country") + if country != "India": return - address = frappe.db.get_value("Address", doc.get('customer_address'), ["gst_state", "gst_state_number"], as_dict=1) + address = frappe.db.get_value( + "Address", doc.get("customer_address"), ["gst_state", "gst_state_number"], as_dict=1 + ) if address and address.gst_state and address.gst_state_number: doc.place_of_supply = cstr(address.gst_state_number) + "-" + cstr(address.gst_state) + @frappe.whitelist() def get_regional_round_off_accounts(company, account_list): - country = frappe.get_cached_value('Company', company, 'country') + country = frappe.get_cached_value("Company", company, "country") - if country != 'India': + if country != "India": return if isinstance(account_list, string_types): account_list = json.loads(account_list) - if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'): + if not frappe.db.get_single_value("GST Settings", "round_off_gst_values"): return gst_accounts = get_gst_accounts(company) gst_account_list = [] - for account in ['cgst_account', 'sgst_account', 'igst_account']: + for account in ["cgst_account", "sgst_account", "igst_account"]: if account in gst_accounts: gst_account_list += gst_accounts.get(account) @@ -795,94 +945,113 @@ def get_regional_round_off_accounts(company, account_list): return account_list -def update_taxable_values(doc, method): - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'India': +def update_taxable_values(doc, method): + country = frappe.get_cached_value("Company", doc.company, "country") + + if country != "India": return gst_accounts = get_gst_accounts(doc.company) # Only considering sgst account to avoid inflating taxable value - gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \ - + gst_accounts.get('igst_account', []) + gst_account_list = ( + gst_accounts.get("sgst_account", []) + + gst_accounts.get("sgst_account", []) + + gst_accounts.get("igst_account", []) + ) additional_taxes = 0 total_charges = 0 item_count = 0 considered_rows = [] - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): prev_row_id = cint(tax.row_id) - 1 if tax.account_head in gst_account_list and prev_row_id not in considered_rows: - if tax.charge_type == 'On Previous Row Amount': - additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount + if tax.charge_type == "On Previous Row Amount": + additional_taxes += doc.get("taxes")[prev_row_id].tax_amount_after_discount_amount considered_rows.append(prev_row_id) - if tax.charge_type == 'On Previous Row Total': - additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total + if tax.charge_type == "On Previous Row Total": + additional_taxes += doc.get("taxes")[prev_row_id].base_total - doc.base_net_total considered_rows.append(prev_row_id) - for item in doc.get('items'): + for item in doc.get("items"): proportionate_value = item.base_net_amount if doc.base_net_total else item.qty total_value = doc.base_net_total if doc.base_net_total else doc.total_qty - applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), - item.precision('taxable_value'))) + applicable_charges = flt( + flt( + proportionate_value * (flt(additional_taxes) / flt(total_value)), + item.precision("taxable_value"), + ) + ) item.taxable_value = applicable_charges + proportionate_value total_charges += applicable_charges item_count += 1 if total_charges != additional_taxes: diff = additional_taxes - total_charges - doc.get('items')[item_count - 1].taxable_value += diff + doc.get("items")[item_count - 1].taxable_value += diff + def get_depreciation_amount(asset, depreciable_value, row): 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(asset.gross_purchase_amount) - - flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations) + 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: - depreciation_amount = (flt(row.value_after_depreciation) - - flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) + depreciation_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) else: rate_of_depreciation = row.rate_of_depreciation # if its the first depreciation if depreciable_value == asset.gross_purchase_amount: - if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'): + if row.finance_book and frappe.db.get_value("Finance Book", row.finance_book, "for_income_tax"): # as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2 diff = date_diff(row.depreciation_start_date, asset.available_for_use_date) if diff <= 180: rate_of_depreciation = rate_of_depreciation / 2 frappe.msgprint( - _('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.')) + _( + "As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%." + ) + ) depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100)) return depreciation_amount + def set_item_tax_from_hsn_code(item): if not item.taxes and item.gst_hsn_code: hsn_doc = frappe.get_doc("GST HSN Code", item.gst_hsn_code) for tax in hsn_doc.taxes: - item.append('taxes', { - 'item_tax_template': tax.item_tax_template, - 'tax_category': tax.tax_category, - 'valid_from': tax.valid_from - }) + item.append( + "taxes", + { + "item_tax_template": tax.item_tax_template, + "tax_category": tax.tax_category, + "valid_from": tax.valid_from, + }, + ) + def delete_gst_settings_for_company(doc, method): - if doc.country != 'India': + if doc.country != "India": return gst_settings = frappe.get_doc("GST Settings") records_to_delete = [] - for d in reversed(gst_settings.get('gst_accounts')): + for d in reversed(gst_settings.get("gst_accounts")): if d.company == doc.name: records_to_delete.append(d) @@ -890,4 +1059,3 @@ def delete_gst_settings_for_company(doc, method): gst_settings.remove(d) gst_settings.save() - diff --git a/erpnext/regional/italy/__init__.py b/erpnext/regional/italy/__init__.py index 4932f660ca5..833dcfa13aa 100644 --- a/erpnext/regional/italy/__init__.py +++ b/erpnext/regional/italy/__init__.py @@ -1,79 +1,174 @@ # coding=utf-8 fiscal_regimes = [ - "RF01-Ordinario", - "RF02-Contribuenti minimi (art.1, c.96-117, L. 244/07)", - "RF04-Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)", - "RF05-Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)", - "RF06-Commercio fiammiferi (art.74, c.1, DPR 633/72)", - "RF07-Editoria (art.74, c.1, DPR 633/72)", - "RF08-Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)", - "RF09-Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)", - "RF10-Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)", - "RF11-Agenzie viaggi e turismo (art.74-ter, DPR 633/72)", - "RF12-Agriturismo (art.5, c.2, L. 413/91)", - "RF13-Vendite a domicilio (art.25-bis, c.6, DPR 600/73)", - "RF14-Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)", - "RF15-Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)", - "RF16-IVA per cassa P.A. (art.6, c.5, DPR 633/72)", - "RF17-IVA per cassa (art. 32-bis, DL 83/2012)", - "RF18-Altro", - "RF19-Regime forfettario (art.1, c.54-89, L. 190/2014)" + "RF01-Ordinario", + "RF02-Contribuenti minimi (art.1, c.96-117, L. 244/07)", + "RF04-Agricoltura e attività connesse e pesca (artt.34 e 34-bis, DPR 633/72)", + "RF05-Vendita sali e tabacchi (art.74, c.1, DPR. 633/72)", + "RF06-Commercio fiammiferi (art.74, c.1, DPR 633/72)", + "RF07-Editoria (art.74, c.1, DPR 633/72)", + "RF08-Gestione servizi telefonia pubblica (art.74, c.1, DPR 633/72)", + "RF09-Rivendita documenti di trasporto pubblico e di sosta (art.74, c.1, DPR 633/72)", + "RF10-Intrattenimenti, giochi e altre attività di cui alla tariffa allegata al DPR 640/72 (art.74, c.6, DPR 633/72)", + "RF11-Agenzie viaggi e turismo (art.74-ter, DPR 633/72)", + "RF12-Agriturismo (art.5, c.2, L. 413/91)", + "RF13-Vendite a domicilio (art.25-bis, c.6, DPR 600/73)", + "RF14-Rivendita beni usati, oggetti d’arte, d’antiquariato o da collezione (art.36, DL 41/95)", + "RF15-Agenzie di vendite all’asta di oggetti d’arte, antiquariato o da collezione (art.40-bis, DL 41/95)", + "RF16-IVA per cassa P.A. (art.6, c.5, DPR 633/72)", + "RF17-IVA per cassa (art. 32-bis, DL 83/2012)", + "RF18-Altro", + "RF19-Regime forfettario (art.1, c.54-89, L. 190/2014)", ] tax_exemption_reasons = [ - "N1-Escluse ex art. 15", - "N2-Non Soggette", - "N3-Non Imponibili", - "N4-Esenti", - "N5-Regime del margine / IVA non esposta in fattura", - "N6-Inversione Contabile", - "N7-IVA assolta in altro stato UE" + "N1-Escluse ex art. 15", + "N2-Non Soggette", + "N3-Non Imponibili", + "N4-Esenti", + "N5-Regime del margine / IVA non esposta in fattura", + "N6-Inversione Contabile", + "N7-IVA assolta in altro stato UE", ] mode_of_payment_codes = [ - "MP01-Contanti", - "MP02-Assegno", - "MP03-Assegno circolare", - "MP04-Contanti presso Tesoreria", - "MP05-Bonifico", - "MP06-Vaglia cambiario", - "MP07-Bollettino bancario", - "MP08-Carta di pagamento", - "MP09-RID", - "MP10-RID utenze", - "MP11-RID veloce", - "MP12-RIBA", - "MP13-MAV", - "MP14-Quietanza erario", - "MP15-Giroconto su conti di contabilità speciale", - "MP16-Domiciliazione bancaria", - "MP17-Domiciliazione postale", - "MP18-Bollettino di c/c postale", - "MP19-SEPA Direct Debit", - "MP20-SEPA Direct Debit CORE", - "MP21-SEPA Direct Debit B2B", - "MP22-Trattenuta su somme già riscosse" + "MP01-Contanti", + "MP02-Assegno", + "MP03-Assegno circolare", + "MP04-Contanti presso Tesoreria", + "MP05-Bonifico", + "MP06-Vaglia cambiario", + "MP07-Bollettino bancario", + "MP08-Carta di pagamento", + "MP09-RID", + "MP10-RID utenze", + "MP11-RID veloce", + "MP12-RIBA", + "MP13-MAV", + "MP14-Quietanza erario", + "MP15-Giroconto su conti di contabilità speciale", + "MP16-Domiciliazione bancaria", + "MP17-Domiciliazione postale", + "MP18-Bollettino di c/c postale", + "MP19-SEPA Direct Debit", + "MP20-SEPA Direct Debit CORE", + "MP21-SEPA Direct Debit B2B", + "MP22-Trattenuta su somme già riscosse", ] -vat_collectability_options = [ - "I-Immediata", - "D-Differita", - "S-Scissione dei Pagamenti" -] +vat_collectability_options = ["I-Immediata", "D-Differita", "S-Scissione dei Pagamenti"] -state_codes = {'Siracusa': 'SR', 'Bologna': 'BO', 'Grosseto': 'GR', 'Caserta': 'CE', 'Alessandria': 'AL', 'Ancona': 'AN', 'Pavia': 'PV', - 'Benevento or Beneventum': 'BN', 'Modena': 'MO', 'Lodi': 'LO', 'Novara': 'NO', 'Avellino': 'AV', 'Verona': 'VR', 'Forli-Cesena': 'FC', - 'Caltanissetta': 'CL', 'Brescia': 'BS', 'Rieti': 'RI', 'Treviso': 'TV', 'Ogliastra': 'OG', 'Olbia-Tempio': 'OT', 'Bergamo': 'BG', - 'Napoli': 'NA', 'Campobasso': 'CB', 'Fermo': 'FM', 'Roma': 'RM', 'Lucca': 'LU', 'Rovigo': 'RO', 'Piacenza': 'PC', 'Monza and Brianza': 'MB', - 'La Spezia': 'SP', 'Pescara': 'PE', 'Vercelli': 'VC', 'Enna': 'EN', 'Nuoro': 'NU', 'Medio Campidano': 'MD', 'Trieste': 'TS', 'Aosta': 'AO', - 'Firenze': 'FI', 'Trapani': 'TP', 'Messina': 'ME', 'Teramo': 'TE', 'Udine': 'UD', 'Verbano-Cusio-Ossola': 'VB', 'Padua': 'PD', - 'Reggio Emilia': 'RE', 'Frosinone': 'FR', 'Taranto': 'TA', 'Catanzaro': 'CZ', 'Belluno': 'BL', 'Pordenone': 'PN', 'Viterbo': 'VT', - 'Gorizia': 'GO', 'Vatican City': 'SCV', 'Ferrara': 'FE', 'Chieti': 'CH', 'Crotone': 'KR', 'Foggia': 'FG', 'Perugia': 'PG', 'Bari': 'BA', - 'Massa-Carrara': 'MS', 'Pisa': 'PI', 'Latina': 'LT', 'Salerno': 'SA', 'Turin': 'TO', 'Lecco': 'LC', 'Lecce': 'LE', 'Pistoia': 'PT', 'Como': 'CO', - 'Barletta-Andria-Trani': 'BT', 'Mantua': 'MN', 'Ragusa': 'RG', 'Macerata': 'MC', 'Imperia': 'IM', 'Palermo': 'PA', 'Matera': 'MT', "L'Aquila": 'AQ', - 'Milano': 'MI', 'Catania': 'CT', 'Pesaro e Urbino': 'PU', 'Potenza': 'PZ', 'Republic of San Marino': 'RSM', 'Genoa': 'GE', 'Brindisi': 'BR', - 'Cagliari': 'CA', 'Siena': 'SI', 'Vibo Valentia': 'VV', 'Reggio Calabria': 'RC', 'Ascoli Piceno': 'AP', 'Carbonia-Iglesias': 'CI', 'Oristano': 'OR', - 'Asti': 'AT', 'Ravenna': 'RA', 'Vicenza': 'VI', 'Savona': 'SV', 'Biella': 'BI', 'Rimini': 'RN', 'Agrigento': 'AG', 'Prato': 'PO', 'Cuneo': 'CN', - 'Cosenza': 'CS', 'Livorno or Leghorn': 'LI', 'Sondrio': 'SO', 'Cremona': 'CR', 'Isernia': 'IS', 'Trento': 'TN', 'Terni': 'TR', 'Bolzano/Bozen': 'BZ', - 'Parma': 'PR', 'Varese': 'VA', 'Venezia': 'VE', 'Sassari': 'SS', 'Arezzo': 'AR'} +state_codes = { + "Siracusa": "SR", + "Bologna": "BO", + "Grosseto": "GR", + "Caserta": "CE", + "Alessandria": "AL", + "Ancona": "AN", + "Pavia": "PV", + "Benevento or Beneventum": "BN", + "Modena": "MO", + "Lodi": "LO", + "Novara": "NO", + "Avellino": "AV", + "Verona": "VR", + "Forli-Cesena": "FC", + "Caltanissetta": "CL", + "Brescia": "BS", + "Rieti": "RI", + "Treviso": "TV", + "Ogliastra": "OG", + "Olbia-Tempio": "OT", + "Bergamo": "BG", + "Napoli": "NA", + "Campobasso": "CB", + "Fermo": "FM", + "Roma": "RM", + "Lucca": "LU", + "Rovigo": "RO", + "Piacenza": "PC", + "Monza and Brianza": "MB", + "La Spezia": "SP", + "Pescara": "PE", + "Vercelli": "VC", + "Enna": "EN", + "Nuoro": "NU", + "Medio Campidano": "MD", + "Trieste": "TS", + "Aosta": "AO", + "Firenze": "FI", + "Trapani": "TP", + "Messina": "ME", + "Teramo": "TE", + "Udine": "UD", + "Verbano-Cusio-Ossola": "VB", + "Padua": "PD", + "Reggio Emilia": "RE", + "Frosinone": "FR", + "Taranto": "TA", + "Catanzaro": "CZ", + "Belluno": "BL", + "Pordenone": "PN", + "Viterbo": "VT", + "Gorizia": "GO", + "Vatican City": "SCV", + "Ferrara": "FE", + "Chieti": "CH", + "Crotone": "KR", + "Foggia": "FG", + "Perugia": "PG", + "Bari": "BA", + "Massa-Carrara": "MS", + "Pisa": "PI", + "Latina": "LT", + "Salerno": "SA", + "Turin": "TO", + "Lecco": "LC", + "Lecce": "LE", + "Pistoia": "PT", + "Como": "CO", + "Barletta-Andria-Trani": "BT", + "Mantua": "MN", + "Ragusa": "RG", + "Macerata": "MC", + "Imperia": "IM", + "Palermo": "PA", + "Matera": "MT", + "L'Aquila": "AQ", + "Milano": "MI", + "Catania": "CT", + "Pesaro e Urbino": "PU", + "Potenza": "PZ", + "Republic of San Marino": "RSM", + "Genoa": "GE", + "Brindisi": "BR", + "Cagliari": "CA", + "Siena": "SI", + "Vibo Valentia": "VV", + "Reggio Calabria": "RC", + "Ascoli Piceno": "AP", + "Carbonia-Iglesias": "CI", + "Oristano": "OR", + "Asti": "AT", + "Ravenna": "RA", + "Vicenza": "VI", + "Savona": "SV", + "Biella": "BI", + "Rimini": "RN", + "Agrigento": "AG", + "Prato": "PO", + "Cuneo": "CN", + "Cosenza": "CS", + "Livorno or Leghorn": "LI", + "Sondrio": "SO", + "Cremona": "CR", + "Isernia": "IS", + "Trento": "TN", + "Terni": "TR", + "Bolzano/Bozen": "BZ", + "Parma": "PR", + "Varese": "VA", + "Venezia": "VE", + "Sassari": "SS", + "Arezzo": "AR", +} diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 9453a2340ad..23406ea85a6 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -21,206 +21,476 @@ def setup(company=None, patch=True): setup_report() add_permissions() + def make_custom_fields(update=True): invoice_item_fields = [ - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Float', insert_after='description', - print_hide=1, hidden=1, read_only=1), - dict(fieldname='tax_amount', label='Tax Amount', - fieldtype='Currency', insert_after='tax_rate', - print_hide=1, hidden=1, read_only=1, options="currency"), - dict(fieldname='total_amount', label='Total Amount', - fieldtype='Currency', insert_after='tax_amount', - print_hide=1, hidden=1, read_only=1, options="currency") + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Float", + insert_after="description", + print_hide=1, + hidden=1, + read_only=1, + ), + dict( + fieldname="tax_amount", + label="Tax Amount", + fieldtype="Currency", + insert_after="tax_rate", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), + dict( + fieldname="total_amount", + label="Total Amount", + fieldtype="Currency", + insert_after="tax_amount", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), ] customer_po_fields = [ - dict(fieldname='customer_po_details', label='Customer PO', - fieldtype='Section Break', insert_after='image'), - dict(fieldname='customer_po_no', label='Customer PO No', - fieldtype='Data', insert_after='customer_po_details', - fetch_from = 'sales_order.po_no', - print_hide=1, allow_on_submit=1, fetch_if_empty= 1, read_only=1, no_copy=1), - dict(fieldname='customer_po_clm_brk', label='', - fieldtype='Column Break', insert_after='customer_po_no', - print_hide=1, read_only=1), - dict(fieldname='customer_po_date', label='Customer PO Date', - fieldtype='Date', insert_after='customer_po_clm_brk', - fetch_from = 'sales_order.po_date', - print_hide=1, allow_on_submit=1, fetch_if_empty= 1, read_only=1, no_copy=1) + dict( + fieldname="customer_po_details", + label="Customer PO", + fieldtype="Section Break", + insert_after="image", + ), + dict( + fieldname="customer_po_no", + label="Customer PO No", + fieldtype="Data", + insert_after="customer_po_details", + fetch_from="sales_order.po_no", + print_hide=1, + allow_on_submit=1, + fetch_if_empty=1, + read_only=1, + no_copy=1, + ), + dict( + fieldname="customer_po_clm_brk", + label="", + fieldtype="Column Break", + insert_after="customer_po_no", + print_hide=1, + read_only=1, + ), + dict( + fieldname="customer_po_date", + label="Customer PO Date", + fieldtype="Date", + insert_after="customer_po_clm_brk", + fetch_from="sales_order.po_date", + print_hide=1, + allow_on_submit=1, + fetch_if_empty=1, + read_only=1, + no_copy=1, + ), ] custom_fields = { - 'Company': [ - dict(fieldname='sb_e_invoicing', label='E-Invoicing', - fieldtype='Section Break', insert_after='date_of_establishment', print_hide=1), - dict(fieldname='fiscal_regime', label='Fiscal Regime', - fieldtype='Select', insert_after='sb_e_invoicing', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), fiscal_regimes))), - dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='fiscal_regime', print_hide=1, - description=_("Applicable if the company is an Individual or a Proprietorship")), - dict(fieldname='vat_collectability', label='VAT Collectability', - fieldtype='Select', insert_after='fiscal_code', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options))), - dict(fieldname='cb_e_invoicing1', fieldtype='Column Break', insert_after='vat_collectability', print_hide=1), - dict(fieldname='registrar_office_province', label='Province of the Registrar Office', - fieldtype='Data', insert_after='cb_e_invoicing1', print_hide=1, length=2), - dict(fieldname='registration_number', label='Registration Number', - fieldtype='Data', insert_after='registrar_office_province', print_hide=1, length=20), - dict(fieldname='share_capital_amount', label='Share Capital', - fieldtype='Currency', insert_after='registration_number', print_hide=1, - description=_('Applicable if the company is SpA, SApA or SRL')), - dict(fieldname='no_of_members', label='No of Members', - fieldtype='Select', insert_after='share_capital_amount', print_hide=1, - options="\nSU-Socio Unico\nSM-Piu Soci", description=_("Applicable if the company is a limited liability company")), - dict(fieldname='liquidation_state', label='Liquidation State', - fieldtype='Select', insert_after='no_of_members', print_hide=1, - options="\nLS-In Liquidazione\nLN-Non in Liquidazione") + "Company": [ + dict( + fieldname="sb_e_invoicing", + label="E-Invoicing", + fieldtype="Section Break", + insert_after="date_of_establishment", + print_hide=1, + ), + dict( + fieldname="fiscal_regime", + label="Fiscal Regime", + fieldtype="Select", + insert_after="sb_e_invoicing", + print_hide=1, + options="\n".join(map(lambda x: frappe.safe_decode(x, encoding="utf-8"), fiscal_regimes)), + ), + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="fiscal_regime", + print_hide=1, + description=_("Applicable if the company is an Individual or a Proprietorship"), + ), + dict( + fieldname="vat_collectability", + label="VAT Collectability", + fieldtype="Select", + insert_after="fiscal_code", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), vat_collectability_options) + ), + ), + dict( + fieldname="cb_e_invoicing1", + fieldtype="Column Break", + insert_after="vat_collectability", + print_hide=1, + ), + dict( + fieldname="registrar_office_province", + label="Province of the Registrar Office", + fieldtype="Data", + insert_after="cb_e_invoicing1", + print_hide=1, + length=2, + ), + dict( + fieldname="registration_number", + label="Registration Number", + fieldtype="Data", + insert_after="registrar_office_province", + print_hide=1, + length=20, + ), + dict( + fieldname="share_capital_amount", + label="Share Capital", + fieldtype="Currency", + insert_after="registration_number", + print_hide=1, + description=_("Applicable if the company is SpA, SApA or SRL"), + ), + dict( + fieldname="no_of_members", + label="No of Members", + fieldtype="Select", + insert_after="share_capital_amount", + print_hide=1, + options="\nSU-Socio Unico\nSM-Piu Soci", + description=_("Applicable if the company is a limited liability company"), + ), + dict( + fieldname="liquidation_state", + label="Liquidation State", + fieldtype="Select", + insert_after="no_of_members", + print_hide=1, + options="\nLS-In Liquidazione\nLN-Non in Liquidazione", + ), ], - 'Sales Taxes and Charges': [ - dict(fieldname='tax_exemption_reason', label='Tax Exemption Reason', - fieldtype='Select', insert_after='included_in_print_rate', print_hide=1, + "Sales Taxes and Charges": [ + dict( + fieldname="tax_exemption_reason", + label="Tax Exemption Reason", + fieldtype="Select", + insert_after="included_in_print_rate", + print_hide=1, depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0', - options="\n" + "\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), tax_exemption_reasons))), - dict(fieldname='tax_exemption_law', label='Tax Exempt Under', - fieldtype='Text', insert_after='tax_exemption_reason', print_hide=1, - depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0') + options="\n" + + "\n".join(map(lambda x: frappe.safe_decode(x, encoding="utf-8"), tax_exemption_reasons)), + ), + dict( + fieldname="tax_exemption_law", + label="Tax Exempt Under", + fieldtype="Text", + insert_after="tax_exemption_reason", + print_hide=1, + depends_on='eval:doc.charge_type!="Actual" && doc.rate==0.0', + ), ], - 'Customer': [ - dict(fieldname='fiscal_code', label='Fiscal Code', fieldtype='Data', insert_after='tax_id', print_hide=1), - dict(fieldname='recipient_code', label='Recipient Code', - fieldtype='Data', insert_after='fiscal_code', print_hide=1, default="0000000"), - dict(fieldname='pec', label='Recipient PEC', - fieldtype='Data', insert_after='fiscal_code', print_hide=1), - dict(fieldname='is_public_administration', label='Is Public Administration', - fieldtype='Check', insert_after='is_internal_customer', print_hide=1, + "Customer": [ + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="tax_id", + print_hide=1, + ), + dict( + fieldname="recipient_code", + label="Recipient Code", + fieldtype="Data", + insert_after="fiscal_code", + print_hide=1, + default="0000000", + ), + dict( + fieldname="pec", + label="Recipient PEC", + fieldtype="Data", + insert_after="fiscal_code", + print_hide=1, + ), + dict( + fieldname="is_public_administration", + label="Is Public Administration", + fieldtype="Check", + insert_after="is_internal_customer", + print_hide=1, description=_("Set this if the customer is a Public Administration company."), - depends_on='eval:doc.customer_type=="Company"'), - dict(fieldname='first_name', label='First Name', fieldtype='Data', - insert_after='salutation', print_hide=1, depends_on='eval:doc.customer_type!="Company"'), - dict(fieldname='last_name', label='Last Name', fieldtype='Data', - insert_after='first_name', print_hide=1, depends_on='eval:doc.customer_type!="Company"') + depends_on='eval:doc.customer_type=="Company"', + ), + dict( + fieldname="first_name", + label="First Name", + fieldtype="Data", + insert_after="salutation", + print_hide=1, + depends_on='eval:doc.customer_type!="Company"', + ), + dict( + fieldname="last_name", + label="Last Name", + fieldtype="Data", + insert_after="first_name", + print_hide=1, + depends_on='eval:doc.customer_type!="Company"', + ), ], - 'Mode of Payment': [ - dict(fieldname='mode_of_payment_code', label='Code', - fieldtype='Select', insert_after='included_in_print_rate', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes))) + "Mode of Payment": [ + dict( + fieldname="mode_of_payment_code", + label="Code", + fieldtype="Select", + insert_after="included_in_print_rate", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), mode_of_payment_codes) + ), + ) ], - 'Payment Schedule': [ - dict(fieldname='mode_of_payment_code', label='Code', - fieldtype='Select', insert_after='mode_of_payment', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), mode_of_payment_codes)), - fetch_from="mode_of_payment.mode_of_payment_code", read_only=1), - dict(fieldname='bank_account', label='Bank Account', - fieldtype='Link', insert_after='mode_of_payment_code', print_hide=1, - options="Bank Account"), - dict(fieldname='bank_account_name', label='Bank Name', - fieldtype='Data', insert_after='bank_account', print_hide=1, - fetch_from="bank_account.bank", read_only=1), - dict(fieldname='bank_account_no', label='Bank Account No', - fieldtype='Data', insert_after='bank_account_name', print_hide=1, - fetch_from="bank_account.bank_account_no", read_only=1), - dict(fieldname='bank_account_iban', label='IBAN', - fieldtype='Data', insert_after='bank_account_name', print_hide=1, - fetch_from="bank_account.iban", read_only=1), - dict(fieldname='bank_account_swift_number', label='Swift Code (BIC)', - fieldtype='Data', insert_after='bank_account_iban', print_hide=1, - fetch_from="bank_account.swift_number", read_only=1), + "Payment Schedule": [ + dict( + fieldname="mode_of_payment_code", + label="Code", + fieldtype="Select", + insert_after="mode_of_payment", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), mode_of_payment_codes) + ), + fetch_from="mode_of_payment.mode_of_payment_code", + read_only=1, + ), + dict( + fieldname="bank_account", + label="Bank Account", + fieldtype="Link", + insert_after="mode_of_payment_code", + print_hide=1, + options="Bank Account", + ), + dict( + fieldname="bank_account_name", + label="Bank Name", + fieldtype="Data", + insert_after="bank_account", + print_hide=1, + fetch_from="bank_account.bank", + read_only=1, + ), + dict( + fieldname="bank_account_no", + label="Bank Account No", + fieldtype="Data", + insert_after="bank_account_name", + print_hide=1, + fetch_from="bank_account.bank_account_no", + read_only=1, + ), + dict( + fieldname="bank_account_iban", + label="IBAN", + fieldtype="Data", + insert_after="bank_account_name", + print_hide=1, + fetch_from="bank_account.iban", + read_only=1, + ), + dict( + fieldname="bank_account_swift_number", + label="Swift Code (BIC)", + fieldtype="Data", + insert_after="bank_account_iban", + print_hide=1, + fetch_from="bank_account.swift_number", + read_only=1, + ), ], "Sales Invoice": [ - dict(fieldname='vat_collectability', label='VAT Collectability', - fieldtype='Select', insert_after='taxes_and_charges', print_hide=1, - options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options)), - fetch_from="company.vat_collectability"), - dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing', - fieldtype='Section Break', insert_after='against_income_account', print_hide=1), - dict(fieldname='company_fiscal_code', label='Company Fiscal Code', - fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, - fetch_from="company.fiscal_code"), - dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime', - fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1, - fetch_from="company.fiscal_regime"), - dict(fieldname='cb_e_invoicing_reference', fieldtype='Column Break', - insert_after='company_fiscal_regime', print_hide=1), - dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code', - fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1, - fetch_from="customer.fiscal_code"), - dict(fieldname='type_of_document', label='Type of Document', - fieldtype='Select', insert_after='customer_fiscal_code', - options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'), - ], - 'Purchase Invoice Item': invoice_item_fields, - 'Sales Order Item': invoice_item_fields, - 'Delivery Note Item': invoice_item_fields, - 'Sales Invoice Item': invoice_item_fields + customer_po_fields, - 'Quotation Item': invoice_item_fields, - 'Purchase Order Item': invoice_item_fields, - 'Purchase Receipt Item': invoice_item_fields, - 'Supplier Quotation Item': invoice_item_fields, - 'Address': [ - dict(fieldname='country_code', label='Country Code', - fieldtype='Data', insert_after='country', print_hide=1, read_only=0, - fetch_from="country.code"), - dict(fieldname='state_code', label='State Code', - fieldtype='Data', insert_after='state', print_hide=1) - ], - 'Purchase Invoice': [ - dict(fieldname='document_type', label='Document Type', - fieldtype='Data', insert_after='company', print_hide=1, read_only=1 + dict( + fieldname="vat_collectability", + label="VAT Collectability", + fieldtype="Select", + insert_after="taxes_and_charges", + print_hide=1, + options="\n".join( + map(lambda x: frappe.safe_decode(x, encoding="utf-8"), vat_collectability_options) ), - dict(fieldname='destination_code', label='Destination Code', - fieldtype='Data', insert_after='company', print_hide=1, read_only=1 - ), - dict(fieldname='imported_grand_total', label='Imported Grand Total', - fieldtype='Data', insert_after='update_auto_repeat_reference', print_hide=1, read_only=1 - ) + fetch_from="company.vat_collectability", + ), + dict( + fieldname="sb_e_invoicing_reference", + label="E-Invoicing", + fieldtype="Section Break", + insert_after="against_income_account", + print_hide=1, + ), + dict( + fieldname="company_fiscal_code", + label="Company Fiscal Code", + fieldtype="Data", + insert_after="sb_e_invoicing_reference", + print_hide=1, + read_only=1, + fetch_from="company.fiscal_code", + ), + dict( + fieldname="company_fiscal_regime", + label="Company Fiscal Regime", + fieldtype="Data", + insert_after="company_fiscal_code", + print_hide=1, + read_only=1, + fetch_from="company.fiscal_regime", + ), + dict( + fieldname="cb_e_invoicing_reference", + fieldtype="Column Break", + insert_after="company_fiscal_regime", + print_hide=1, + ), + dict( + fieldname="customer_fiscal_code", + label="Customer Fiscal Code", + fieldtype="Data", + insert_after="cb_e_invoicing_reference", + read_only=1, + fetch_from="customer.fiscal_code", + ), + dict( + fieldname="type_of_document", + label="Type of Document", + fieldtype="Select", + insert_after="customer_fiscal_code", + options="\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27", + ), ], - 'Purchase Taxes and Charges': [ - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Data', insert_after='parenttype', print_hide=1, read_only=0 - ) + "Purchase Invoice Item": invoice_item_fields, + "Sales Order Item": invoice_item_fields, + "Delivery Note Item": invoice_item_fields, + "Sales Invoice Item": invoice_item_fields + customer_po_fields, + "Quotation Item": invoice_item_fields, + "Purchase Order Item": invoice_item_fields, + "Purchase Receipt Item": invoice_item_fields, + "Supplier Quotation Item": invoice_item_fields, + "Address": [ + dict( + fieldname="country_code", + label="Country Code", + fieldtype="Data", + insert_after="country", + print_hide=1, + read_only=0, + fetch_from="country.code", + ), + dict( + fieldname="state_code", + label="State Code", + fieldtype="Data", + insert_after="state", + print_hide=1, + ), + ], + "Purchase Invoice": [ + dict( + fieldname="document_type", + label="Document Type", + fieldtype="Data", + insert_after="company", + print_hide=1, + read_only=1, + ), + dict( + fieldname="destination_code", + label="Destination Code", + fieldtype="Data", + insert_after="company", + print_hide=1, + read_only=1, + ), + dict( + fieldname="imported_grand_total", + label="Imported Grand Total", + fieldtype="Data", + insert_after="update_auto_repeat_reference", + print_hide=1, + read_only=1, + ), + ], + "Purchase Taxes and Charges": [ + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Data", + insert_after="parenttype", + print_hide=1, + read_only=0, + ) + ], + "Supplier": [ + dict( + fieldname="fiscal_code", + label="Fiscal Code", + fieldtype="Data", + insert_after="tax_id", + print_hide=1, + read_only=1, + ), + dict( + fieldname="fiscal_regime", + label="Fiscal Regime", + fieldtype="Select", + insert_after="fiscal_code", + print_hide=1, + read_only=1, + options="\nRF01\nRF02\nRF04\nRF05\nRF06\nRF07\nRF08\nRF09\nRF10\nRF11\nRF12\nRF13\nRF14\nRF15\nRF16\nRF17\nRF18\nRF19", + ), ], - 'Supplier': [ - dict(fieldname='fiscal_code', label='Fiscal Code', - fieldtype='Data', insert_after='tax_id', print_hide=1, read_only=1 - ), - dict(fieldname='fiscal_regime', label='Fiscal Regime', - fieldtype='Select', insert_after='fiscal_code', print_hide=1, read_only=1, - options= "\nRF01\nRF02\nRF04\nRF05\nRF06\nRF07\nRF08\nRF09\nRF10\nRF11\nRF12\nRF13\nRF14\nRF15\nRF16\nRF17\nRF18\nRF19" - ) - ] } - create_custom_fields(custom_fields, ignore_validate = frappe.flags.in_patch, update=update) + create_custom_fields(custom_fields, ignore_validate=frappe.flags.in_patch, update=update) + def setup_report(): - report_name = 'Electronic Invoice Register' + report_name = "Electronic Invoice Register" frappe.db.set_value("Report", report_name, "disabled", 0) - if not frappe.db.get_value('Custom Role', dict(report=report_name)): - frappe.get_doc(dict( - doctype='Custom Role', - report=report_name, - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report=report_name)): + frappe.get_doc( + dict( + doctype="Custom Role", + report=report_name, + roles=[dict(role="Accounts User"), dict(role="Accounts Manager")], + ) + ).insert() + def add_permissions(): - doctype = 'Import Supplier Invoice' - add_permission(doctype, 'All', 0) + doctype = "Import Supplier Invoice" + add_permission(doctype, "All", 0) - for role in ('Accounts Manager', 'Accounts User','Purchase User', 'Auditor'): + for role in ("Accounts Manager", "Accounts User", "Purchase User", "Auditor"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'print', 1) - update_permission_property(doctype, role, 0, 'report', 1) + update_permission_property(doctype, role, 0, "print", 1) + update_permission_property(doctype, role, 0, "report", 1) - if role in ('Accounts Manager', 'Accounts User'): - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + if role in ("Accounts Manager", "Accounts User"): + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1) - add_permission(doctype, 'Accounts Manager', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1) + update_permission_property(doctype, "Accounts Manager", 0, "delete", 1) + add_permission(doctype, "Accounts Manager", 1) + update_permission_property(doctype, "Accounts Manager", 1, "write", 1) + update_permission_property(doctype, "Accounts Manager", 1, "create", 1) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index 8d1558be227..8dffd99eadb 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -1,4 +1,3 @@ - import io import json @@ -13,41 +12,41 @@ from erpnext.regional.italy import state_codes def update_itemised_tax_data(doc): - if not doc.taxes: return + if not doc.taxes: + return - if doc.doctype == "Purchase Invoice": return + if doc.doctype == "Purchase Invoice": + return itemised_tax = get_itemised_tax(doc.taxes) for row in doc.items: tax_rate = 0.0 if itemised_tax.get(row.item_code): - tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()]) + tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + @frappe.whitelist() def export_invoices(filters=None): - frappe.has_permission('Sales Invoice', throw=True) + frappe.has_permission("Sales Invoice", throw=True) invoices = frappe.get_all( - "Sales Invoice", - filters=get_conditions(filters), - fields=["name", "company_tax_id"] + "Sales Invoice", filters=get_conditions(filters), fields=["name", "company_tax_id"] ) attachments = get_e_invoice_attachments(invoices) - zip_filename = "{0}-einvoices.zip".format( - frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) + zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) download_zip(attachments, zip_filename) def prepare_invoice(invoice, progressive_number): - #set company information + # set company information company = frappe.get_doc("Company", invoice.company) invoice.progressive_number = progressive_number @@ -56,15 +55,17 @@ def prepare_invoice(invoice, progressive_number): company_address = frappe.get_doc("Address", invoice.company_address) invoice.company_address_data = company_address - #Set invoice type + # Set invoice type if not invoice.type_of_document: if invoice.is_return and invoice.return_against: - invoice.type_of_document = "TD04" #Credit Note (Nota di Credito) - invoice.return_against_unamended = get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against)) + invoice.type_of_document = "TD04" # Credit Note (Nota di Credito) + invoice.return_against_unamended = get_unamended_name( + frappe.get_doc("Sales Invoice", invoice.return_against) + ) else: - invoice.type_of_document = "TD01" #Sales Invoice (Fattura) + invoice.type_of_document = "TD01" # Sales Invoice (Fattura) - #set customer information + # set customer information invoice.customer_data = frappe.get_doc("Customer", invoice.customer) customer_address = frappe.get_doc("Address", invoice.customer_address) invoice.customer_address_data = customer_address @@ -81,8 +82,10 @@ def prepare_invoice(invoice, progressive_number): tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes) invoice.tax_data = tax_data - #Check if stamp duty (Bollo) of 2 EUR exists. - stamp_duty_charge_row = next((tax for tax in invoice.taxes if tax.charge_type == "Actual" and tax.tax_amount == 2.0 ), None) + # Check if stamp duty (Bollo) of 2 EUR exists. + stamp_duty_charge_row = next( + (tax for tax in invoice.taxes if tax.charge_type == "Actual" and tax.tax_amount == 2.0), None + ) if stamp_duty_charge_row: invoice.stamp_duty = stamp_duty_charge_row.tax_amount @@ -92,24 +95,28 @@ def prepare_invoice(invoice, progressive_number): customer_po_data = {} for d in invoice.e_invoice_items: - if (d.customer_po_no and d.customer_po_date - and d.customer_po_no not in customer_po_data): + if d.customer_po_no and d.customer_po_date and d.customer_po_no not in customer_po_data: customer_po_data[d.customer_po_no] = d.customer_po_date invoice.customer_po_data = customer_po_data return invoice + def get_conditions(filters): filters = json.loads(filters) conditions = {"docstatus": 1, "company_tax_id": ("!=", "")} - if filters.get("company"): conditions["company"] = filters["company"] - if filters.get("customer"): conditions["customer"] = filters["customer"] + if filters.get("company"): + conditions["company"] = filters["company"] + if filters.get("customer"): + conditions["customer"] = filters["customer"] - if filters.get("from_date"): conditions["posting_date"] = (">=", filters["from_date"]) - if filters.get("to_date"): conditions["posting_date"] = ("<=", filters["to_date"]) + if filters.get("from_date"): + conditions["posting_date"] = (">=", filters["from_date"]) + if filters.get("to_date"): + conditions["posting_date"] = ("<=", filters["to_date"]) if filters.get("from_date") and filters.get("to_date"): conditions["posting_date"] = ("between", [filters.get("from_date"), filters.get("to_date")]) @@ -121,10 +128,9 @@ def download_zip(files, output_filename): import zipfile zip_stream = io.BytesIO() - with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file: + with zipfile.ZipFile(zip_stream, "w", zipfile.ZIP_DEFLATED) as zip_file: for file in files: - file_path = frappe.utils.get_files_path( - file.file_name, is_private=file.is_private) + file_path = frappe.utils.get_files_path(file.file_name, is_private=file.is_private) zip_file.write(file_path, arcname=file.file_name) @@ -133,20 +139,21 @@ def download_zip(files, output_filename): frappe.local.response.type = "download" zip_stream.close() + def get_invoice_summary(items, taxes): summary_data = frappe._dict() for tax in taxes: - #Include only VAT charges. + # Include only VAT charges. if tax.charge_type == "Actual": continue - #Charges to appear as items in the e-invoice. + # Charges to appear as items in the e-invoice. if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]: reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None) if reference_row: items.append( frappe._dict( - idx=len(items)+1, + idx=len(items) + 1, item_code=reference_row.description, item_name=reference_row.description, description=reference_row.description, @@ -159,11 +166,11 @@ def get_invoice_summary(items, taxes): net_amount=reference_row.tax_amount, taxable_amount=reference_row.tax_amount, item_tax_rate={tax.account_head: tax.rate}, - charges=True + charges=True, ) ) - #Check item tax rates if tax rate is zero. + # Check item tax rates if tax rate is zero. if tax.rate == 0: for item in items: item_tax_rate = item.item_tax_rate @@ -173,8 +180,15 @@ def get_invoice_summary(items, taxes): if item_tax_rate and tax.account_head in item_tax_rate: key = cstr(item_tax_rate[tax.account_head]) if key not in summary_data: - summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0, - "tax_exemption_reason": "", "tax_exemption_law": ""}) + summary_data.setdefault( + key, + { + "tax_amount": 0.0, + "taxable_amount": 0.0, + "tax_exemption_reason": "", + "tax_exemption_law": "", + }, + ) summary_data[key]["tax_amount"] += item.tax_amount summary_data[key]["taxable_amount"] += item.net_amount @@ -182,93 +196,138 @@ def get_invoice_summary(items, taxes): summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law - if summary_data.get("0.0") and tax.charge_type in ["On Previous Row Total", - "On Previous Row Amount"]: + if summary_data.get("0.0") and tax.charge_type in [ + "On Previous Row Total", + "On Previous Row Amount", + ]: summary_data[key]["taxable_amount"] = tax.total - if summary_data == {}: #Implies that Zero VAT has not been set on any item. - summary_data.setdefault("0.0", {"tax_amount": 0.0, "taxable_amount": tax.total, - "tax_exemption_reason": tax.tax_exemption_reason, "tax_exemption_law": tax.tax_exemption_law}) + if summary_data == {}: # Implies that Zero VAT has not been set on any item. + summary_data.setdefault( + "0.0", + { + "tax_amount": 0.0, + "taxable_amount": tax.total, + "tax_exemption_reason": tax.tax_exemption_reason, + "tax_exemption_law": tax.tax_exemption_law, + }, + ) else: item_wise_tax_detail = json.loads(tax.item_wise_tax_detail) - for rate_item in [tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate]: + for rate_item in [ + tax_item for tax_item in item_wise_tax_detail.items() if tax_item[1][0] == tax.rate + ]: key = cstr(tax.rate) - if not summary_data.get(key): summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0}) + if not summary_data.get(key): + summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0}) summary_data[key]["tax_amount"] += rate_item[1][1] - summary_data[key]["taxable_amount"] += sum([item.net_amount for item in items if item.item_code == rate_item[0]]) + summary_data[key]["taxable_amount"] += sum( + [item.net_amount for item in items if item.item_code == rate_item[0]] + ) for item in items: key = cstr(tax.rate) if item.get("charges"): - if not summary_data.get(key): summary_data.setdefault(key, {"taxable_amount": 0.0}) + if not summary_data.get(key): + summary_data.setdefault(key, {"taxable_amount": 0.0}) summary_data[key]["taxable_amount"] += item.taxable_amount return summary_data -#Preflight for successful e-invoice export. + +# Preflight for successful e-invoice export. def sales_invoice_validate(doc): - #Validate company - if doc.doctype != 'Sales Invoice': + # Validate company + if doc.doctype != "Sales Invoice": return if not doc.company_address: - frappe.throw(_("Please set an Address on the Company '%s'" % doc.company), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set an Address on the Company '%s'" % doc.company), + title=_("E-Invoicing Information Missing"), + ) else: validate_address(doc.company_address) - company_fiscal_regime = frappe.get_cached_value("Company", doc.company, 'fiscal_regime') + company_fiscal_regime = frappe.get_cached_value("Company", doc.company, "fiscal_regime") if not company_fiscal_regime: - frappe.throw(_("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}") - .format(doc.company)) + frappe.throw( + _("Fiscal Regime is mandatory, kindly set the fiscal regime in the company {0}").format( + doc.company + ) + ) else: doc.company_fiscal_regime = company_fiscal_regime - doc.company_tax_id = frappe.get_cached_value("Company", doc.company, 'tax_id') - doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, 'fiscal_code') + doc.company_tax_id = frappe.get_cached_value("Company", doc.company, "tax_id") + doc.company_fiscal_code = frappe.get_cached_value("Company", doc.company, "fiscal_code") if not doc.company_tax_id and not doc.company_fiscal_code: - frappe.throw(_("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set either the Tax ID or Fiscal Code on Company '%s'" % doc.company), + title=_("E-Invoicing Information Missing"), + ) - #Validate customer details + # Validate customer details customer = frappe.get_doc("Customer", doc.customer) if customer.customer_type == "Individual": doc.customer_fiscal_code = customer.fiscal_code if not doc.customer_fiscal_code: - frappe.throw(_("Please set Fiscal Code for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Fiscal Code for the customer '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) else: if customer.is_public_administration: doc.customer_fiscal_code = customer.fiscal_code if not doc.customer_fiscal_code: - frappe.throw(_("Please set Fiscal Code for the public administration '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Fiscal Code for the public administration '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) else: doc.tax_id = customer.tax_id if not doc.tax_id: - frappe.throw(_("Please set Tax ID for the customer '%s'" % doc.customer), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set Tax ID for the customer '%s'" % doc.customer), + title=_("E-Invoicing Information Missing"), + ) if not doc.customer_address: - frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing")) + frappe.throw(_("Please set the Customer Address"), title=_("E-Invoicing Information Missing")) else: validate_address(doc.customer_address) if not len(doc.taxes): - frappe.throw(_("Please set at least one row in the Taxes and Charges Table"), title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set at least one row in the Taxes and Charges Table"), + title=_("E-Invoicing Information Missing"), + ) else: for row in doc.taxes: if row.rate == 0 and row.tax_amount == 0 and not row.tax_exemption_reason: - frappe.throw(_("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format(row.idx), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges").format(row.idx), + title=_("E-Invoicing Information Missing"), + ) for schedule in doc.payment_schedule: if schedule.mode_of_payment and not schedule.mode_of_payment_code: - schedule.mode_of_payment_code = frappe.get_cached_value('Mode of Payment', - schedule.mode_of_payment, 'mode_of_payment_code') + schedule.mode_of_payment_code = frappe.get_cached_value( + "Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code" + ) -#Ensure payment details are valid for e-invoice. + +# Ensure payment details are valid for e-invoice. def sales_invoice_on_submit(doc, method): - #Validate payment details - if get_company_country(doc.company) not in ['Italy', - 'Italia', 'Italian Republic', 'Repubblica Italiana']: + # Validate payment details + if get_company_country(doc.company) not in [ + "Italy", + "Italia", + "Italian Republic", + "Repubblica Italiana", + ]: return if not len(doc.payment_schedule): @@ -276,38 +335,53 @@ def sales_invoice_on_submit(doc, method): else: for schedule in doc.payment_schedule: if not schedule.mode_of_payment: - frappe.throw(_("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), - title=_("E-Invoicing Information Missing")) - elif not frappe.db.get_value("Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code"): - frappe.throw(_("Row {0}: Please set the correct code on Mode of Payment {1}").format(schedule.idx, schedule.mode_of_payment), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Row {0}: Please set the Mode of Payment in Payment Schedule").format(schedule.idx), + title=_("E-Invoicing Information Missing"), + ) + elif not frappe.db.get_value( + "Mode of Payment", schedule.mode_of_payment, "mode_of_payment_code" + ): + frappe.throw( + _("Row {0}: Please set the correct code on Mode of Payment {1}").format( + schedule.idx, schedule.mode_of_payment + ), + title=_("E-Invoicing Information Missing"), + ) prepare_and_attach_invoice(doc) + def prepare_and_attach_invoice(doc, replace=False): progressive_name, progressive_number = get_progressive_name_and_number(doc, replace) invoice = prepare_invoice(doc, progressive_number) item_meta = frappe.get_meta("Sales Invoice Item") - invoice_xml = frappe.render_template('erpnext/regional/italy/e-invoice.xml', - context={"doc": invoice, "item_meta": item_meta}, is_path=True) + invoice_xml = frappe.render_template( + "erpnext/regional/italy/e-invoice.xml", + context={"doc": invoice, "item_meta": item_meta}, + is_path=True, + ) invoice_xml = invoice_xml.replace("&", "&") xml_filename = progressive_name + ".xml" - _file = frappe.get_doc({ - "doctype": "File", - "file_name": xml_filename, - "attached_to_doctype": doc.doctype, - "attached_to_name": doc.name, - "is_private": True, - "content": invoice_xml - }) + _file = frappe.get_doc( + { + "doctype": "File", + "file_name": xml_filename, + "attached_to_doctype": doc.doctype, + "attached_to_name": doc.name, + "is_private": True, + "content": invoice_xml, + } + ) _file.save() return _file + @frappe.whitelist() def generate_single_invoice(docname): doc = frappe.get_doc("Sales Invoice", docname) @@ -316,17 +390,24 @@ def generate_single_invoice(docname): e_invoice = prepare_and_attach_invoice(doc, True) return e_invoice.file_url + # Delete e-invoice attachment on cancel. def sales_invoice_on_cancel(doc, method): - if get_company_country(doc.company) not in ['Italy', - 'Italia', 'Italian Republic', 'Repubblica Italiana']: + if get_company_country(doc.company) not in [ + "Italy", + "Italia", + "Italian Republic", + "Repubblica Italiana", + ]: return for attachment in get_e_invoice_attachments(doc): remove_file(attachment.name, attached_to_doctype=doc.doctype, attached_to_name=doc.name) + def get_company_country(company): - return frappe.get_cached_value('Company', company, 'country') + return frappe.get_cached_value("Company", company, "country") + def get_e_invoice_attachments(invoices): if not isinstance(invoices, list): @@ -340,16 +421,14 @@ def get_e_invoice_attachments(invoices): invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id - ) for invoice in invoices + ) + for invoice in invoices } attachments = frappe.get_all( "File", fields=("name", "file_name", "attached_to_name", "is_private"), - filters= { - "attached_to_name": ('in', tax_id_map), - "attached_to_doctype": 'Sales Invoice' - } + filters={"attached_to_name": ("in", tax_id_map), "attached_to_doctype": "Sales Invoice"}, ) out = [] @@ -357,21 +436,24 @@ def get_e_invoice_attachments(invoices): if ( attachment.file_name and attachment.file_name.endswith(".xml") - and attachment.file_name.startswith( - tax_id_map.get(attachment.attached_to_name)) + and attachment.file_name.startswith(tax_id_map.get(attachment.attached_to_name)) ): out.append(attachment) return out + def validate_address(address_name): fields = ["pincode", "city", "country_code"] data = frappe.get_cached_value("Address", address_name, fields, as_dict=1) or {} for field in fields: if not data.get(field): - frappe.throw(_("Please set {0} for address {1}").format(field.replace('-',''), address_name), - title=_("E-Invoicing Information Missing")) + frappe.throw( + _("Please set {0} for address {1}").format(field.replace("-", ""), address_name), + title=_("E-Invoicing Information Missing"), + ) + def get_unamended_name(doc): attributes = ["naming_series", "amended_from"] @@ -384,6 +466,7 @@ def get_unamended_name(doc): else: return doc.name + def get_progressive_name_and_number(doc, replace=False): if replace: for attachment in get_e_invoice_attachments(doc): @@ -391,24 +474,30 @@ def get_progressive_name_and_number(doc, replace=False): filename = attachment.file_name.split(".xml")[0] return filename, filename.split("_")[1] - company_tax_id = doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id + company_tax_id = ( + doc.company_tax_id if doc.company_tax_id.startswith("IT") else "IT" + doc.company_tax_id + ) progressive_name = frappe.model.naming.make_autoname(company_tax_id + "_.#####") progressive_number = progressive_name.split("_")[1] return progressive_name, progressive_number + def set_state_code(doc, method): - if doc.get('country_code'): + if doc.get("country_code"): doc.country_code = doc.country_code.upper() - if not doc.get('state'): + if not doc.get("state"): return - if not (hasattr(doc, "state_code") and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"]): + if not ( + hasattr(doc, "state_code") + and doc.country in ["Italy", "Italia", "Italian Republic", "Repubblica Italiana"] + ): return - state_codes_lower = {key.lower():value for key,value in state_codes.items()} + state_codes_lower = {key.lower(): value for key, value in state_codes.items()} - state = doc.get('state','').lower() + state = doc.get("state", "").lower() if state_codes_lower.get(state): doc.state_code = state_codes_lower.get(state) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 5d6e8dff3fa..fb2f1cdcf05 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -27,134 +27,104 @@ COLUMNS = [ "label": "Umsatz (ohne Soll/Haben-Kz)", "fieldname": "Umsatz (ohne Soll/Haben-Kz)", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "label": "Soll/Haben-Kennzeichen", "fieldname": "Soll/Haben-Kennzeichen", "fieldtype": "Data", - "width": 100 - }, - { - "label": "Konto", - "fieldname": "Konto", - "fieldtype": "Data", - "width": 100 + "width": 100, }, + {"label": "Konto", "fieldname": "Konto", "fieldtype": "Data", "width": 100}, { "label": "Gegenkonto (ohne BU-Schlüssel)", "fieldname": "Gegenkonto (ohne BU-Schlüssel)", "fieldtype": "Data", - "width": 100 - }, - { - "label": "BU-Schlüssel", - "fieldname": "BU-Schlüssel", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Belegdatum", - "fieldname": "Belegdatum", - "fieldtype": "Date", - "width": 100 - }, - { - "label": "Belegfeld 1", - "fieldname": "Belegfeld 1", - "fieldtype": "Data", - "width": 150 - }, - { - "label": "Buchungstext", - "fieldname": "Buchungstext", - "fieldtype": "Text", - "width": 300 + "width": 100, }, + {"label": "BU-Schlüssel", "fieldname": "BU-Schlüssel", "fieldtype": "Data", "width": 100}, + {"label": "Belegdatum", "fieldname": "Belegdatum", "fieldtype": "Date", "width": 100}, + {"label": "Belegfeld 1", "fieldname": "Belegfeld 1", "fieldtype": "Data", "width": 150}, + {"label": "Buchungstext", "fieldname": "Buchungstext", "fieldtype": "Text", "width": 300}, { "label": "Beleginfo - Art 1", "fieldname": "Beleginfo - Art 1", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 1", "fieldname": "Beleginfo - Inhalt 1", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 1", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 2", "fieldname": "Beleginfo - Art 2", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 2", "fieldname": "Beleginfo - Inhalt 2", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 2", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 3", "fieldname": "Beleginfo - Art 3", "fieldtype": "Link", "options": "DocType", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 3", "fieldname": "Beleginfo - Inhalt 3", "fieldtype": "Dynamic Link", "options": "Beleginfo - Art 3", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 4", "fieldname": "Beleginfo - Art 4", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Inhalt 4", "fieldname": "Beleginfo - Inhalt 4", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Art 5", "fieldname": "Beleginfo - Art 5", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Inhalt 5", "fieldname": "Beleginfo - Inhalt 5", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": "Beleginfo - Art 6", "fieldname": "Beleginfo - Art 6", "fieldtype": "Data", - "width": 150 + "width": 150, }, { "label": "Beleginfo - Inhalt 6", "fieldname": "Beleginfo - Inhalt 6", "fieldtype": "Date", - "width": 100 + "width": 100, }, - { - "label": "Fälligkeit", - "fieldname": "Fälligkeit", - "fieldtype": "Date", - "width": 100 - } + {"label": "Fälligkeit", "fieldname": "Fälligkeit", "fieldtype": "Date", "width": 100}, ] @@ -162,8 +132,8 @@ def execute(filters=None): """Entry point for frappe.""" data = [] if filters and validate(filters): - fn = 'temporary_against_account_number' - filters[fn] = frappe.get_value('DATEV Settings', filters.get('company'), fn) + fn = "temporary_against_account_number" + filters[fn] = frappe.get_value("DATEV Settings", filters.get("company"), fn) data = get_transactions(filters, as_dict=0) return COLUMNS, data @@ -171,23 +141,23 @@ def execute(filters=None): def validate(filters): """Make sure all mandatory filters and settings are present.""" - company = filters.get('company') + company = filters.get("company") if not company: - frappe.throw(_('Company is a mandatory filter.')) + frappe.throw(_("Company is a mandatory filter.")) - from_date = filters.get('from_date') + from_date = filters.get("from_date") if not from_date: - frappe.throw(_('From Date is a mandatory filter.')) + frappe.throw(_("From Date is a mandatory filter.")) - to_date = filters.get('to_date') + to_date = filters.get("to_date") if not to_date: - frappe.throw(_('To Date is a mandatory filter.')) + frappe.throw(_("To Date is a mandatory filter.")) validate_fiscal_year(from_date, to_date, company) - if not frappe.db.exists('DATEV Settings', filters.get('company')): - msg = 'Please create DATEV Settings for Company {}'.format(filters.get('company')) - frappe.log_error(msg, title='DATEV Settings missing') + if not frappe.db.exists("DATEV Settings", filters.get("company")): + msg = "Please create DATEV Settings for Company {}".format(filters.get("company")) + frappe.log_error(msg, title="DATEV Settings missing") return False return True @@ -197,7 +167,7 @@ def validate_fiscal_year(from_date, to_date, company): from_fiscal_year = get_fiscal_year(date=from_date, company=company) to_fiscal_year = get_fiscal_year(date=to_date, company=company) if from_fiscal_year != to_fiscal_year: - frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date)) + frappe.throw(_("Dates {} and {} are not in the same fiscal year.").format(from_date, to_date)) def get_transactions(filters, as_dict=1): @@ -213,7 +183,7 @@ def get_transactions(filters, as_dict=1): # specific query methods for some voucher types "Payment Entry": get_payment_entry_params, "Sales Invoice": get_sales_invoice_params, - "Purchase Invoice": get_purchase_invoice_params + "Purchase Invoice": get_purchase_invoice_params, } only_voucher_type = filters.get("voucher_type") @@ -309,7 +279,9 @@ def get_generic_params(filters): if filters.get("exclude_voucher_types"): # exclude voucher types that are queried by a dedicated method - exclude = "({})".format(', '.join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))) + exclude = "({})".format( + ", ".join("'{}'".format(key) for key in filters.get("exclude_voucher_types")) + ) extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude) # if voucher type filter is set, allow only this type @@ -381,10 +353,8 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): {extra_filters} ORDER BY 'Belegdatum', gl.voucher_no""".format( - extra_fields=extra_fields, - extra_joins=extra_joins, - extra_filters=extra_filters - ) + extra_fields=extra_fields, extra_joins=extra_joins, extra_filters=extra_filters + ) gl_entries = frappe.db.sql(query, filters, as_dict=as_dict) @@ -398,7 +368,8 @@ def get_customers(filters): Arguments: filters -- dict of filters to be passed to the sql query """ - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT par.debtor_creditor_number as 'Konto', @@ -450,7 +421,10 @@ def get_customers(filters): on country.name = adr.country WHERE adr.is_primary_address = '1' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) def get_suppliers(filters): @@ -460,7 +434,8 @@ def get_suppliers(filters): Arguments: filters -- dict of filters to be passed to the sql query """ - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT par.debtor_creditor_number as 'Konto', @@ -513,11 +488,15 @@ def get_suppliers(filters): on country.name = adr.country WHERE adr.is_primary_address = '1' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) def get_account_names(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT account_number as 'Konto', @@ -528,7 +507,10 @@ def get_account_names(filters): WHERE company = %(company)s AND is_group = 0 AND account_number != '' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) @frappe.whitelist() @@ -548,40 +530,43 @@ def download_datev_csv(filters): filters = json.loads(filters) validate(filters) - company = filters.get('company') + company = filters.get("company") - fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=company) - filters['fiscal_year_start'] = fiscal_year[1] + fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company) + filters["fiscal_year_start"] = fiscal_year[1] # set chart of accounts used - coa = frappe.get_value('Company', company, 'chart_of_accounts') - filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + coa = frappe.get_value("Company", company, "chart_of_accounts") + filters["skr"] = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") - datev_settings = frappe.get_doc('DATEV Settings', company) - filters['account_number_length'] = datev_settings.account_number_length - filters['temporary_against_account_number'] = datev_settings.temporary_against_account_number + datev_settings = frappe.get_doc("DATEV Settings", company) + filters["account_number_length"] = datev_settings.account_number_length + filters["temporary_against_account_number"] = datev_settings.temporary_against_account_number transactions = get_transactions(filters) account_names = get_account_names(filters) customers = get_customers(filters) suppliers = get_suppliers(filters) - zip_name = '{} DATEV.zip'.format(frappe.utils.datetime.date.today()) - zip_and_download(zip_name, [ - { - 'file_name': 'EXTF_Buchungsstapel.csv', - 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions) - }, - { - 'file_name': 'EXTF_Kontenbeschriftungen.csv', - 'csv_data': get_datev_csv(account_names, filters, csv_class=AccountNames) - }, - { - 'file_name': 'EXTF_Kunden.csv', - 'csv_data': get_datev_csv(customers, filters, csv_class=DebtorsCreditors) - }, - { - 'file_name': 'EXTF_Lieferanten.csv', - 'csv_data': get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) - }, - ]) + zip_name = "{} DATEV.zip".format(frappe.utils.datetime.date.today()) + zip_and_download( + zip_name, + [ + { + "file_name": "EXTF_Buchungsstapel.csv", + "csv_data": get_datev_csv(transactions, filters, csv_class=Transactions), + }, + { + "file_name": "EXTF_Kontenbeschriftungen.csv", + "csv_data": get_datev_csv(account_names, filters, csv_class=AccountNames), + }, + { + "file_name": "EXTF_Kunden.csv", + "csv_data": get_datev_csv(customers, filters, csv_class=DebtorsCreditors), + }, + { + "file_name": "EXTF_Lieferanten.csv", + "csv_data": get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors), + }, + ], + ) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index 7ca0b1b50fe..6e6847b896b 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -25,15 +25,17 @@ from erpnext.regional.report.datev.datev import ( def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "EUR", - "country": "Germany", - "create_chart_of_accounts_based_on": "Standard Template", - "chart_of_accounts": "SKR04 mit Kontonummern" - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "EUR", + "country": "Germany", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "SKR04 mit Kontonummern", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -47,17 +49,20 @@ def make_company(company_name, abbr): company.save() return company + def setup_fiscal_year(): fiscal_year = None year = cstr(now_datetime().year) if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"): try: - fiscal_year = frappe.get_doc({ - "doctype": "Fiscal Year", - "year": year, - "year_start_date": "{0}-01-01".format(year), - "year_end_date": "{0}-12-31".format(year) - }) + fiscal_year = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": year, + "year_start_date": "{0}-01-01".format(year), + "year_end_date": "{0}-12-31".format(year), + } + ) fiscal_year.insert() except frappe.NameError: pass @@ -65,75 +70,78 @@ def setup_fiscal_year(): if fiscal_year: fiscal_year.set_as_default() + def make_customer_with_account(customer_name, company): - acc_name = frappe.db.get_value("Account", { - "account_name": customer_name, - "company": company.name - }, "name") + acc_name = frappe.db.get_value( + "Account", {"account_name": customer_name, "company": company.name}, "name" + ) if not acc_name: - acc = frappe.get_doc({ - "doctype": "Account", - "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", - "account_name": customer_name, - "company": company.name, - "account_type": "Receivable", - "account_number": "10001" - }) + acc = frappe.get_doc( + { + "doctype": "Account", + "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", + "account_name": customer_name, + "company": company.name, + "account_type": "Receivable", + "account_number": "10001", + } + ) acc.insert() acc_name = acc.name if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": customer_name, - "customer_type": "Company", - "accounts": [{ - "company": company.name, - "account": acc_name - }] - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": customer_name, + "customer_type": "Company", + "accounts": [{"company": company.name, "account": acc_name}], + } + ) customer.insert() else: customer = frappe.get_doc("Customer", customer_name) return customer + def make_item(item_code, company): - warehouse_name = frappe.db.get_value("Warehouse", { - "warehouse_name": "Stores", - "company": company.name - }, "name") + warehouse_name = frappe.db.get_value( + "Warehouse", {"warehouse_name": "Stores", "company": company.name}, "name" + ) if not frappe.db.exists("Item", item_code): - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "All Item Groups", - "is_stock_item": 0, - "is_purchase_item": 0, - "is_customer_provided_item": 0, - "item_defaults": [{ - "default_warehouse": warehouse_name, - "company": company.name - }] - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "All Item Groups", + "is_stock_item": 0, + "is_purchase_item": 0, + "is_customer_provided_item": 0, + "item_defaults": [{"default_warehouse": warehouse_name, "company": company.name}], + } + ) item.insert() else: item = frappe.get_doc("Item", item_code) return item + def make_datev_settings(company): if not frappe.db.exists("DATEV Settings", company.name): - frappe.get_doc({ - "doctype": "DATEV Settings", - "client": company.name, - "client_number": "12345", - "consultant_number": "67890", - "temporary_against_account_number": "9999" - }).insert() + frappe.get_doc( + { + "doctype": "DATEV Settings", + "client": company.name, + "client_number": "12345", + "consultant_number": "67890", + "temporary_against_account_number": "9999", + } + ).insert() class TestDatev(TestCase): @@ -144,27 +152,24 @@ class TestDatev(TestCase): "company": self.company.name, "from_date": today(), "to_date": today(), - "temporary_against_account_number": "9999" + "temporary_against_account_number": "9999", } make_datev_settings(self.company) item = make_item("_Test Item", self.company) setup_fiscal_year() - warehouse = frappe.db.get_value("Item Default", { - "parent": item.name, - "company": self.company.name - }, "default_warehouse") + warehouse = frappe.db.get_value( + "Item Default", {"parent": item.name, "company": self.company.name}, "default_warehouse" + ) - income_account = frappe.db.get_value("Account", { - "account_number": "4200", - "company": self.company.name - }, "name") + income_account = frappe.db.get_value( + "Account", {"account_number": "4200", "company": self.company.name}, "name" + ) - tax_account = frappe.db.get_value("Account", { - "account_number": "3806", - "company": self.company.name - }, "name") + tax_account = frappe.db.get_value( + "Account", {"account_number": "3806", "company": self.company.name}, "name" + ) si = create_sales_invoice( company=self.company.name, @@ -176,16 +181,19 @@ class TestDatev(TestCase): cost_center=self.company.cost_center, warehouse=warehouse, item=item.name, - do_not_save=1 + do_not_save=1, ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": tax_account, - "description": "Umsatzsteuer 19 %", - "rate": 19, - "cost_center": self.company.cost_center - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": "Umsatzsteuer 19 %", + "rate": 19, + "cost_center": self.company.cost_center, + }, + ) si.cost_center = self.company.cost_center @@ -221,16 +229,18 @@ class TestDatev(TestCase): self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors)) def test_csv(self): - test_data = [{ - "Umsatz (ohne Soll/Haben-Kz)": 100, - "Soll/Haben-Kennzeichen": "H", - "Kontonummer": "4200", - "Gegenkonto (ohne BU-Schlüssel)": "10000", - "Belegdatum": today(), - "Buchungstext": "No remark", - "Beleginfo - Art 1": "Sales Invoice", - "Beleginfo - Inhalt 1": "SINV-0001" - }] + test_data = [ + { + "Umsatz (ohne Soll/Haben-Kz)": 100, + "Soll/Haben-Kennzeichen": "H", + "Kontonummer": "4200", + "Gegenkonto (ohne BU-Schlüssel)": "10000", + "Belegdatum": today(), + "Buchungstext": "No remark", + "Beleginfo - Art 1": "Sales Invoice", + "Beleginfo - Inhalt 1": "SINV-0001", + } + ] get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions) def test_download(self): @@ -239,6 +249,6 @@ class TestDatev(TestCase): # zipfile.is_zipfile() expects a file-like object zip_buffer = BytesIO() - zip_buffer.write(frappe.response['filecontent']) + zip_buffer.write(frappe.response["filecontent"]) self.assertTrue(zipfile.is_zipfile(zip_buffer)) diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py index 5ec7d85b9dd..694615d7422 100644 --- a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py +++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py @@ -14,96 +14,68 @@ def execute(filters=None): return columns, data + def validate_filters(filters=None): filters = frappe._dict(filters or {}) if not filters.company: - frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter')) + frappe.throw( + _("{} is mandatory for generating E-Invoice Summary Report").format(_("Company")), + title=_("Invalid Filter"), + ) if filters.company: # validate if company has e-invoicing enabled pass if not filters.from_date or not filters.to_date: - frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter')) + frappe.throw( + _("From Date & To Date is mandatory for generating E-Invoice Summary Report"), + title=_("Invalid Filter"), + ) if filters.from_date > filters.to_date: - frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter')) + frappe.throw(_("From Date must be before To Date"), title=_("Invalid Filter")) + def get_data(filters=None): if not filters: filters = {} query_filters = { - 'posting_date': ['between', [filters.from_date, filters.to_date]], - 'einvoice_status': ['is', 'set'], - 'company': filters.company + "posting_date": ["between", [filters.from_date, filters.to_date]], + "einvoice_status": ["is", "set"], + "company": filters.company, } if filters.customer: - query_filters['customer'] = filters.customer + query_filters["customer"] = filters.customer if filters.status: - query_filters['einvoice_status'] = filters.status + query_filters["einvoice_status"] = filters.status data = frappe.get_all( - 'Sales Invoice', - filters=query_filters, - fields=[d.get('fieldname') for d in get_columns()] + "Sales Invoice", filters=query_filters, fields=[d.get("fieldname") for d in get_columns()] ) return data + def get_columns(): return [ - { - "fieldtype": "Date", - "fieldname": "posting_date", - "label": _("Posting Date"), - "width": 0 - }, + {"fieldtype": "Date", "fieldname": "posting_date", "label": _("Posting Date"), "width": 0}, { "fieldtype": "Link", "fieldname": "name", "label": _("Sales Invoice"), "options": "Sales Invoice", - "width": 140 - }, - { - "fieldtype": "Data", - "fieldname": "einvoice_status", - "label": _("Status"), - "width": 100 - }, - { - "fieldtype": "Link", - "fieldname": "customer", - "options": "Customer", - "label": _("Customer") - }, - { - "fieldtype": "Check", - "fieldname": "is_return", - "label": _("Is Return"), - "width": 85 - }, - { - "fieldtype": "Data", - "fieldname": "ack_no", - "label": "Ack. No.", - "width": 145 - }, - { - "fieldtype": "Data", - "fieldname": "ack_date", - "label": "Ack. Date", - "width": 165 - }, - { - "fieldtype": "Data", - "fieldname": "irn", - "label": _("IRN No."), - "width": 250 + "width": 140, }, + {"fieldtype": "Data", "fieldname": "einvoice_status", "label": _("Status"), "width": 100}, + {"fieldtype": "Link", "fieldname": "customer", "options": "Customer", "label": _("Customer")}, + {"fieldtype": "Check", "fieldname": "is_return", "label": _("Is Return"), "width": 85}, + {"fieldtype": "Data", "fieldname": "ack_no", "label": "Ack. No.", "width": 145}, + {"fieldtype": "Data", "fieldname": "ack_date", "label": "Ack. Date", "width": 165}, + {"fieldtype": "Data", "fieldname": "irn", "label": _("IRN No."), "width": 250}, { "fieldtype": "Currency", "options": "Company:company:default_currency", "fieldname": "base_grand_total", "label": _("Grand Total"), - "width": 120 - } + "width": 120, + }, ] diff --git a/erpnext/regional/report/eway_bill/eway_bill.py b/erpnext/regional/report/eway_bill/eway_bill.py index f3fe5e88488..8dcd6a365df 100644 --- a/erpnext/regional/report/eway_bill/eway_bill.py +++ b/erpnext/regional/report/eway_bill/eway_bill.py @@ -11,35 +11,41 @@ from frappe.utils import nowdate def execute(filters=None): - if not filters: filters.setdefault('posting_date', [nowdate(), nowdate()]) + if not filters: + filters.setdefault("posting_date", [nowdate(), nowdate()]) columns, data = [], [] columns = get_columns() data = get_data(filters) return columns, data + def get_data(filters): conditions = get_conditions(filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT dn.name as dn_id, dn.posting_date, dn.company, dn.company_gstin, dn.customer, dn.customer_gstin, dni.item_code, dni.item_name, dni.description, dni.gst_hsn_code, dni.uom, dni.qty, dni.amount, dn.mode_of_transport, dn.distance, dn.transporter_name, dn.gst_transporter_id, dn.lr_no, dn.lr_date, dn.vehicle_no, dn.gst_vehicle_type, dn.company_address, dn.shipping_address_name FROM `tabDelivery Note` AS dn join `tabDelivery Note Item` AS dni on (dni.parent = dn.name) WHERE dn.docstatus < 2 - %s """ % conditions, as_dict=1) + %s """ + % conditions, + as_dict=1, + ) unit = { - 'Bag': "BAGS", - 'Bottle': "BOTTLES", - 'Kg': "KILOGRAMS", - 'Liter': "LITERS", - 'Meter': "METERS", - 'Nos': "NUMBERS", - 'PKT': "PACKS", - 'Roll': "ROLLS", - 'Set': "SETS" + "Bag": "BAGS", + "Bottle": "BOTTLES", + "Kg": "KILOGRAMS", + "Liter": "LITERS", + "Meter": "METERS", + "Nos": "NUMBERS", + "PKT": "PACKS", + "Roll": "ROLLS", + "Set": "SETS", } # Regular expression set to remove all the special characters @@ -51,11 +57,11 @@ def get_data(filters): set_address_details(row, special_characters) # Eway Bill accepts date as dd/mm/yyyy and not dd-mm-yyyy - row.posting_date = '/'.join(str(row.posting_date).replace("-", "/").split('/')[::-1]) - row.lr_date = '/'.join(str(row.lr_date).replace("-", "/").split('/')[::-1]) + row.posting_date = "/".join(str(row.posting_date).replace("-", "/").split("/")[::-1]) + row.lr_date = "/".join(str(row.lr_date).replace("-", "/").split("/")[::-1]) - if row.gst_vehicle_type == 'Over Dimensional Cargo (ODC)': - row.gst_vehicle_type = 'ODC' + if row.gst_vehicle_type == "Over Dimensional Cargo (ODC)": + row.gst_vehicle_type = "ODC" row.item_name = re.sub(special_characters, " ", row.item_name) row.description = row.item_name @@ -67,58 +73,80 @@ def get_data(filters): return data + def get_conditions(filters): conditions = "" - conditions += filters.get('company') and " AND dn.company = '%s' " % filters.get('company') or "" - conditions += filters.get('posting_date') and " AND dn.posting_date >= '%s' AND dn.posting_date <= '%s' " % (filters.get('posting_date')[0], filters.get('posting_date')[1]) or "" - conditions += filters.get('delivery_note') and " AND dn.name = '%s' " % filters.get('delivery_note') or "" - conditions += filters.get('customer') and " AND dn.customer = '%s' " % filters.get('customer').replace("'", "\'") or "" + conditions += filters.get("company") and " AND dn.company = '%s' " % filters.get("company") or "" + conditions += ( + filters.get("posting_date") + and " AND dn.posting_date >= '%s' AND dn.posting_date <= '%s' " + % (filters.get("posting_date")[0], filters.get("posting_date")[1]) + or "" + ) + conditions += ( + filters.get("delivery_note") and " AND dn.name = '%s' " % filters.get("delivery_note") or "" + ) + conditions += ( + filters.get("customer") + and " AND dn.customer = '%s' " % filters.get("customer").replace("'", "'") + or "" + ) return conditions + def set_defaults(row): - row.setdefault(u'supply_type', "Outward") - row.setdefault(u'sub_type', "Supply") - row.setdefault(u'doc_type', "Delivery Challan") + row.setdefault("supply_type", "Outward") + row.setdefault("sub_type", "Supply") + row.setdefault("doc_type", "Delivery Challan") + def set_address_details(row, special_characters): - if row.get('company_address'): - address_line1, address_line2, city, pincode, state = frappe.db.get_value("Address", row.get('company_address'), ['address_line1', 'address_line2', 'city', 'pincode', 'state']) + if row.get("company_address"): + address_line1, address_line2, city, pincode, state = frappe.db.get_value( + "Address", + row.get("company_address"), + ["address_line1", "address_line2", "city", "pincode", "state"], + ) - row.update({'from_address_1': re.sub(special_characters, "", address_line1 or '')}) - row.update({'from_address_2': re.sub(special_characters, "", address_line2 or '')}) - row.update({'from_place': city and city.upper() or ''}) - row.update({'from_pin_code': pincode and pincode.replace(" ", "") or ''}) - row.update({'from_state': state and state.upper() or ''}) - row.update({'dispatch_state': row.from_state}) + row.update({"from_address_1": re.sub(special_characters, "", address_line1 or "")}) + row.update({"from_address_2": re.sub(special_characters, "", address_line2 or "")}) + row.update({"from_place": city and city.upper() or ""}) + row.update({"from_pin_code": pincode and pincode.replace(" ", "") or ""}) + row.update({"from_state": state and state.upper() or ""}) + row.update({"dispatch_state": row.from_state}) - if row.get('shipping_address_name'): - address_line1, address_line2, city, pincode, state = frappe.db.get_value("Address", row.get('shipping_address_name'), ['address_line1', 'address_line2', 'city', 'pincode', 'state']) + if row.get("shipping_address_name"): + address_line1, address_line2, city, pincode, state = frappe.db.get_value( + "Address", + row.get("shipping_address_name"), + ["address_line1", "address_line2", "city", "pincode", "state"], + ) + + row.update({"to_address_1": re.sub(special_characters, "", address_line1 or "")}) + row.update({"to_address_2": re.sub(special_characters, "", address_line2 or "")}) + row.update({"to_place": city and city.upper() or ""}) + row.update({"to_pin_code": pincode and pincode.replace(" ", "") or ""}) + row.update({"to_state": state and state.upper() or ""}) + row.update({"ship_to_state": row.to_state}) - row.update({'to_address_1': re.sub(special_characters, "", address_line1 or '')}) - row.update({'to_address_2': re.sub(special_characters, "", address_line2 or '')}) - row.update({'to_place': city and city.upper() or ''}) - row.update({'to_pin_code': pincode and pincode.replace(" ", "") or ''}) - row.update({'to_state': state and state.upper() or ''}) - row.update({'ship_to_state': row.to_state}) def set_taxes(row, filters): - taxes = frappe.get_all("Sales Taxes and Charges", - filters={ - 'parent': row.dn_id - }, - fields=('item_wise_tax_detail', 'account_head')) + taxes = frappe.get_all( + "Sales Taxes and Charges", + filters={"parent": row.dn_id}, + fields=("item_wise_tax_detail", "account_head"), + ) account_list = ["cgst_account", "sgst_account", "igst_account", "cess_account"] - taxes_list = frappe.get_all("GST Account", - filters={ - "parent": "GST Settings", - "company": filters.company - }, - fields=account_list) + taxes_list = frappe.get_all( + "GST Account", + filters={"parent": "GST Settings", "company": filters.company}, + fields=account_list, + ) if not taxes_list: frappe.throw(_("Please set GST Accounts in GST Settings")) @@ -141,253 +169,89 @@ def set_taxes(row, filters): item_tax_rate.pop(tax[key]) row.amount = float(row.amount) + sum(i[1] for i in item_tax_rate.values()) - row.update({'tax_rate': '+'.join(tax_rate)}) + row.update({"tax_rate": "+".join(tax_rate)}) + def get_columns(): columns = [ - { - "fieldname": "supply_type", - "label": _("Supply Type"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "sub_type", - "label": _("Sub Type"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "doc_type", - "label": _("Doc Type"), - "fieldtype": "Data", - "width": 100 - }, + {"fieldname": "supply_type", "label": _("Supply Type"), "fieldtype": "Data", "width": 100}, + {"fieldname": "sub_type", "label": _("Sub Type"), "fieldtype": "Data", "width": 100}, + {"fieldname": "doc_type", "label": _("Doc Type"), "fieldtype": "Data", "width": 100}, { "fieldname": "dn_id", "label": _("Doc Name"), "fieldtype": "Link", "options": "Delivery Note", - "width": 140 - }, - { - "fieldname": "posting_date", - "label": _("Doc Date"), - "fieldtype": "Data", - "width": 100 + "width": 140, }, + {"fieldname": "posting_date", "label": _("Doc Date"), "fieldtype": "Data", "width": 100}, { "fieldname": "company", "label": _("From Party Name"), "fieldtype": "Link", "options": "Company", - "width": 120 - }, - { - "fieldname": "company_gstin", - "label": _("From GSTIN"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "from_address_1", - "label": _("From Address 1"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "from_address_2", - "label": _("From Address 2"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "from_place", - "label": _("From Place"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "from_pin_code", - "label": _("From Pin Code"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "from_state", - "label": _("From State"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "dispatch_state", - "label": _("Dispatch State"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "customer", - "label": _("To Party Name"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "customer_gstin", - "label": _("To GSTIN"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_address_1", - "label": _("To Address 1"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_address_2", - "label": _("To Address 2"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "to_place", - "label": _("To Place"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "to_pin_code", - "label": _("To Pin Code"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "to_state", - "label": _("To State"), - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "ship_to_state", - "label": _("Ship To State"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "company_gstin", "label": _("From GSTIN"), "fieldtype": "Data", "width": 100}, + {"fieldname": "from_address_1", "label": _("From Address 1"), "fieldtype": "Data", "width": 120}, + {"fieldname": "from_address_2", "label": _("From Address 2"), "fieldtype": "Data", "width": 120}, + {"fieldname": "from_place", "label": _("From Place"), "fieldtype": "Data", "width": 80}, + {"fieldname": "from_pin_code", "label": _("From Pin Code"), "fieldtype": "Data", "width": 80}, + {"fieldname": "from_state", "label": _("From State"), "fieldtype": "Data", "width": 80}, + {"fieldname": "dispatch_state", "label": _("Dispatch State"), "fieldtype": "Data", "width": 100}, + {"fieldname": "customer", "label": _("To Party Name"), "fieldtype": "Data", "width": 120}, + {"fieldname": "customer_gstin", "label": _("To GSTIN"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_address_1", "label": _("To Address 1"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_address_2", "label": _("To Address 2"), "fieldtype": "Data", "width": 120}, + {"fieldname": "to_place", "label": _("To Place"), "fieldtype": "Data", "width": 80}, + {"fieldname": "to_pin_code", "label": _("To Pin Code"), "fieldtype": "Data", "width": 80}, + {"fieldname": "to_state", "label": _("To State"), "fieldtype": "Data", "width": 80}, + {"fieldname": "ship_to_state", "label": _("Ship To State"), "fieldtype": "Data", "width": 100}, { "fieldname": "item_name", "label": _("Product"), "fieldtype": "Link", "options": "Item", - "width": 120 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "gst_hsn_code", - "label": _("HSN"), - "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "uom", - "label": _("Unit"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "qty", - "label": _("Qty"), - "fieldtype": "Float", - "width": 100 - }, - { - "fieldname": "amount", - "label": _("Accessable Value"), - "fieldtype": "Float", - "width": 120 - }, - { - "fieldname": "tax_rate", - "label": _("Tax Rate"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "cgst_amount", - "label": _("CGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "sgst_amount", - "label": _("SGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "igst_amount", - "label": _("IGST Amount"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "cess_amount", - "label": _("CESS Amount"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 100}, + {"fieldname": "gst_hsn_code", "label": _("HSN"), "fieldtype": "Data", "width": 120}, + {"fieldname": "uom", "label": _("Unit"), "fieldtype": "Data", "width": 100}, + {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Float", "width": 100}, + {"fieldname": "amount", "label": _("Accessable Value"), "fieldtype": "Float", "width": 120}, + {"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 100}, + {"fieldname": "cgst_amount", "label": _("CGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "sgst_amount", "label": _("SGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "igst_amount", "label": _("IGST Amount"), "fieldtype": "Data", "width": 100}, + {"fieldname": "cess_amount", "label": _("CESS Amount"), "fieldtype": "Data", "width": 100}, { "fieldname": "mode_of_transport", "label": _("Mode of Transport"), "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "distance", - "label": _("Distance"), - "fieldtype": "Data", - "width": 100 + "width": 100, }, + {"fieldname": "distance", "label": _("Distance"), "fieldtype": "Data", "width": 100}, { "fieldname": "transporter_name", "label": _("Transporter Name"), "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "gst_transporter_id", "label": _("Transporter ID"), "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "lr_no", - "label": _("Transport Receipt No"), - "fieldtype": "Data", - "width": 120 + "width": 100, }, + {"fieldname": "lr_no", "label": _("Transport Receipt No"), "fieldtype": "Data", "width": 120}, { "fieldname": "lr_date", "label": _("Transport Receipt Date"), "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "vehicle_no", - "label": _("Vehicle No"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "gst_vehicle_type", - "label": _("Vehicle Type"), - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "vehicle_no", "label": _("Vehicle No"), "fieldtype": "Data", "width": 100}, + {"fieldname": "gst_vehicle_type", "label": _("Vehicle Type"), "fieldtype": "Data", "width": 100}, ] return columns diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py index 59888ff94e7..c75179ee5d1 100644 --- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py +++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py @@ -26,31 +26,42 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) - if not filters.get('fiscal_year'): - frappe.throw(_('{0} is mandatory').format(_('Fiscal Year'))) + if not filters.get("fiscal_year"): + frappe.throw(_("{0} is mandatory").format(_("Fiscal Year"))) def set_account_currency(filters): - filters["company_currency"] = frappe.get_cached_value('Company', filters.company, "default_currency") + filters["company_currency"] = frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) return filters def get_columns(filters): columns = [ - "JournalCode" + "::90", "JournalLib" + "::90", - "EcritureNum" + ":Dynamic Link:90", "EcritureDate" + "::90", - "CompteNum" + ":Link/Account:100", "CompteLib" + ":Link/Account:200", - "CompAuxNum" + "::90", "CompAuxLib" + "::90", - "PieceRef" + "::90", "PieceDate" + "::90", - "EcritureLib" + "::90", "Debit" + "::90", "Credit" + "::90", - "EcritureLet" + "::90", "DateLet" + - "::90", "ValidDate" + "::90", - "Montantdevise" + "::90", "Idevise" + "::90" + "JournalCode" + "::90", + "JournalLib" + "::90", + "EcritureNum" + ":Dynamic Link:90", + "EcritureDate" + "::90", + "CompteNum" + ":Link/Account:100", + "CompteLib" + ":Link/Account:200", + "CompAuxNum" + "::90", + "CompAuxLib" + "::90", + "PieceRef" + "::90", + "PieceDate" + "::90", + "EcritureLib" + "::90", + "Debit" + "::90", + "Credit" + "::90", + "EcritureLet" + "::90", + "DateLet" + "::90", + "ValidDate" + "::90", + "Montantdevise" + "::90", + "Idevise" + "::90", ] return columns @@ -66,10 +77,14 @@ def get_result(filters): def get_gl_entries(filters): - group_by_condition = "group by voucher_type, voucher_no, account" \ - if filters.get("group_by_voucher") else "group by gl.name" + group_by_condition = ( + "group by voucher_type, voucher_no, account" + if filters.get("group_by_voucher") + else "group by gl.name" + ) - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date, sum(gl.debit) as debit, sum(gl.credit) as credit, @@ -99,8 +114,12 @@ def get_gl_entries(filters): left join `tabMember` mem on gl.party = mem.name where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s {group_by_condition} - order by GlPostDate, voucher_no"""\ - .format(group_by_condition=group_by_condition), filters, as_dict=1) + order by GlPostDate, voucher_no""".format( + group_by_condition=group_by_condition + ), + filters, + as_dict=1, + ) return gl_entries @@ -108,25 +127,37 @@ def get_gl_entries(filters): def get_result_as_list(data, filters): result = [] - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") - accounts = frappe.get_all("Account", filters={"Company": filters.company}, fields=["name", "account_number"]) + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") + accounts = frappe.get_all( + "Account", filters={"Company": filters.company}, fields=["name", "account_number"] + ) for d in data: JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0] - if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith("{0}/".format(JournalCode)): + if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith( + "{0}/".format(JournalCode) + ): EcritureNum = re.split("-|/", d.get("voucher_no"))[1] else: - EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE).group(1) + EcritureNum = re.search( + r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE + ).group(1) EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - account_number = [account.account_number for account in accounts if account.name == d.get("account")] + account_number = [ + account.account_number for account in accounts if account.name == d.get("account") + ] if account_number[0] is not None: - CompteNum = account_number[0] + CompteNum = account_number[0] else: - frappe.throw(_("Account number for account {0} is not available.
    Please setup your Chart of Accounts correctly.").format(d.get("account"))) + frappe.throw( + _( + "Account number for account {0} is not available.
    Please setup your Chart of Accounts correctly." + ).format(d.get("account")) + ) if d.get("party_type") == "Customer": CompAuxNum = d.get("cusName") @@ -172,19 +203,45 @@ def get_result_as_list(data, filters): PieceDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - debit = '{:.2f}'.format(d.get("debit")).replace(".", ",") + debit = "{:.2f}".format(d.get("debit")).replace(".", ",") - credit = '{:.2f}'.format(d.get("credit")).replace(".", ",") + credit = "{:.2f}".format(d.get("credit")).replace(".", ",") Idevise = d.get("account_currency") if Idevise != company_currency: - Montantdevise = '{:.2f}'.format(d.get("debitCurr")).replace(".", ",") if d.get("debitCurr") != 0 else '{:.2f}'.format(d.get("creditCurr")).replace(".", ",") + Montantdevise = ( + "{:.2f}".format(d.get("debitCurr")).replace(".", ",") + if d.get("debitCurr") != 0 + else "{:.2f}".format(d.get("creditCurr")).replace(".", ",") + ) else: - Montantdevise = '{:.2f}'.format(d.get("debit")).replace(".", ",") if d.get("debit") != 0 else '{:.2f}'.format(d.get("credit")).replace(".", ",") + Montantdevise = ( + "{:.2f}".format(d.get("debit")).replace(".", ",") + if d.get("debit") != 0 + else "{:.2f}".format(d.get("credit")).replace(".", ",") + ) - row = [JournalCode, d.get("voucher_type"), EcritureNum, EcritureDate, CompteNum, d.get("account"), CompAuxNum, CompAuxLib, - PieceRef, PieceDate, EcritureLib, debit, credit, "", "", ValidDate, Montantdevise, Idevise] + row = [ + JournalCode, + d.get("voucher_type"), + EcritureNum, + EcritureDate, + CompteNum, + d.get("account"), + CompAuxNum, + CompAuxLib, + PieceRef, + PieceDate, + EcritureLib, + debit, + credit, + "", + "", + ValidDate, + Montantdevise, + Idevise, + ] result.append(row) diff --git a/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py b/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py index 528868cf176..fec63f2f18a 100644 --- a/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py +++ b/erpnext/regional/report/gst_itemised_purchase_register/gst_itemised_purchase_register.py @@ -8,24 +8,28 @@ from erpnext.accounts.report.item_wise_purchase_register.item_wise_purchase_regi def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Supplier GSTIN', fieldname="supplier_gstin", width=120), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130), - dict(fieldtype='Data', label='HSN Code', fieldname="gst_hsn_code", width=120), - dict(fieldtype='Data', label='Supplier Invoice No', fieldname="bill_no", width=120), - dict(fieldtype='Date', label='Supplier Invoice Date', fieldname="bill_date", width=100) - ], additional_query_columns=[ - 'supplier_gstin', - 'company_gstin', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin', - 'gst_hsn_code', - 'bill_no', - 'bill_date' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Supplier GSTIN", fieldname="supplier_gstin", width=120), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120), + dict(fieldtype="Data", label="Supplier Invoice No", fieldname="bill_no", width=120), + dict(fieldtype="Date", label="Supplier Invoice Date", fieldname="bill_date", width=100), + ], + additional_query_columns=[ + "supplier_gstin", + "company_gstin", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + "gst_hsn_code", + "bill_no", + "bill_date", + ], + ) diff --git a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py index 386e2197569..bb1843f1bd9 100644 --- a/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py +++ b/erpnext/regional/report/gst_itemised_sales_register/gst_itemised_sales_register.py @@ -6,24 +6,30 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Customer GSTIN', fieldname="customer_gstin", width=120), - dict(fieldtype='Data', label='Billing Address GSTIN', fieldname="billing_address_gstin", width=140), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Place of Supply', fieldname="place_of_supply", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130), - dict(fieldtype='Data', label='HSN Code', fieldname="gst_hsn_code", width=120) - ], additional_query_columns=[ - 'customer_gstin', - 'billing_address_gstin', - 'company_gstin', - 'place_of_supply', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin', - 'gst_hsn_code' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120), + dict( + fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140 + ), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + dict(fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120), + ], + additional_query_columns=[ + "customer_gstin", + "billing_address_gstin", + "company_gstin", + "place_of_supply", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + "gst_hsn_code", + ], + ) diff --git a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py index 2d994082c31..609dbbaf73b 100644 --- a/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py +++ b/erpnext/regional/report/gst_purchase_register/gst_purchase_register.py @@ -6,18 +6,22 @@ from erpnext.accounts.report.purchase_register.purchase_register import _execute def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Supplier GSTIN', fieldname="supplier_gstin", width=120), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130) - ], additional_query_columns=[ - 'supplier_gstin', - 'company_gstin', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Supplier GSTIN", fieldname="supplier_gstin", width=120), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + ], + additional_query_columns=[ + "supplier_gstin", + "company_gstin", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + ], + ) diff --git a/erpnext/regional/report/gst_sales_register/gst_sales_register.py b/erpnext/regional/report/gst_sales_register/gst_sales_register.py index a6f2b3dbf4d..94ceb197b1a 100644 --- a/erpnext/regional/report/gst_sales_register/gst_sales_register.py +++ b/erpnext/regional/report/gst_sales_register/gst_sales_register.py @@ -6,22 +6,28 @@ from erpnext.accounts.report.sales_register.sales_register import _execute def execute(filters=None): - return _execute(filters, additional_table_columns=[ - dict(fieldtype='Data', label='Customer GSTIN', fieldname="customer_gstin", width=120), - dict(fieldtype='Data', label='Billing Address GSTIN', fieldname="billing_address_gstin", width=140), - dict(fieldtype='Data', label='Company GSTIN', fieldname="company_gstin", width=120), - dict(fieldtype='Data', label='Place of Supply', fieldname="place_of_supply", width=120), - dict(fieldtype='Data', label='Reverse Charge', fieldname="reverse_charge", width=120), - dict(fieldtype='Data', label='GST Category', fieldname="gst_category", width=120), - dict(fieldtype='Data', label='Export Type', fieldname="export_type", width=120), - dict(fieldtype='Data', label='E-Commerce GSTIN', fieldname="ecommerce_gstin", width=130) - ], additional_query_columns=[ - 'customer_gstin', - 'billing_address_gstin', - 'company_gstin', - 'place_of_supply', - 'reverse_charge', - 'gst_category', - 'export_type', - 'ecommerce_gstin' - ]) + return _execute( + filters, + additional_table_columns=[ + dict(fieldtype="Data", label="Customer GSTIN", fieldname="customer_gstin", width=120), + dict( + fieldtype="Data", label="Billing Address GSTIN", fieldname="billing_address_gstin", width=140 + ), + dict(fieldtype="Data", label="Company GSTIN", fieldname="company_gstin", width=120), + dict(fieldtype="Data", label="Place of Supply", fieldname="place_of_supply", width=120), + dict(fieldtype="Data", label="Reverse Charge", fieldname="reverse_charge", width=120), + dict(fieldtype="Data", label="GST Category", fieldname="gst_category", width=120), + dict(fieldtype="Data", label="Export Type", fieldname="export_type", width=120), + dict(fieldtype="Data", label="E-Commerce GSTIN", fieldname="ecommerce_gstin", width=130), + ], + additional_query_columns=[ + "customer_gstin", + "billing_address_gstin", + "company_gstin", + "place_of_supply", + "reverse_charge", + "gst_category", + "export_type", + "ecommerce_gstin", + ], + ) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 1ba3d20bdbb..0aece86a681 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -16,6 +16,7 @@ from erpnext.regional.india.utils import get_gst_accounts def execute(filters=None): return Gstr1Report(filters).run() + class Gstr1Report(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -60,7 +61,7 @@ class Gstr1Report(object): return self.columns, self.data def get_data(self): - if self.filters.get("type_of_business") in ("B2C Small", "B2C Large"): + if self.filters.get("type_of_business") in ("B2C Small", "B2C Large"): self.get_b2c_data() elif self.filters.get("type_of_business") == "Advances": self.get_advance_data() @@ -84,15 +85,17 @@ class Gstr1Report(object): advances = self.get_advance_entries() for entry in advances: # only consider IGST and SGST so as to avoid duplication of taxable amount - if entry.account_head in self.gst_accounts.igst_account or \ - entry.account_head in self.gst_accounts.sgst_account: + if ( + entry.account_head in self.gst_accounts.igst_account + or entry.account_head in self.gst_accounts.sgst_account + ): advances_data.setdefault((entry.place_of_supply, entry.rate), [0.0, 0.0]) - advances_data[(entry.place_of_supply, entry.rate)][0] += (entry.amount * 100 / entry.rate) + advances_data[(entry.place_of_supply, entry.rate)][0] += entry.amount * 100 / entry.rate elif entry.account_head in self.gst_accounts.cess_account: advances_data[(entry.place_of_supply, entry.rate)][1] += entry.amount for key, value in advances_data.items(): - row= [key[0], key[1], value[0], value[1]] + row = [key[0], key[1], value[0], value[1]] self.data.append(row) def get_nil_rated_invoices(self): @@ -101,31 +104,31 @@ class Gstr1Report(object): "description": "Inter-State supplies to registered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Intra-State supplies to registered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Inter-State supplies to unregistered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 + "non_gst": 0.0, }, { "description": "Intra-State supplies to unregistered persons", "nil_rated": 0.0, "exempted": 0.0, - "non_gst": 0.0 - } + "non_gst": 0.0, + }, ] for invoice, details in self.nil_exempt_non_gst.items(): invoice_detail = self.invoices.get(invoice) - if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"): + if invoice_detail.get("gst_category") in ("Registered Regular", "Deemed Export", "SEZ"): if is_inter_state(invoice_detail): nil_exempt_output[0]["nil_rated"] += details[0] nil_exempt_output[0]["exempted"] += details[1] @@ -154,26 +157,34 @@ class Gstr1Report(object): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): place_of_supply = invoice_details.get("place_of_supply") - ecommerce_gstin = invoice_details.get("ecommerce_gstin") + ecommerce_gstin = invoice_details.get("ecommerce_gstin") - b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin), { - "place_of_supply": "", - "ecommerce_gstin": "", - "rate": "", - "taxable_value": 0, - "cess_amount": 0, - "type": "", - "invoice_number": invoice_details.get("invoice_number"), - "posting_date": invoice_details.get("posting_date"), - "invoice_value": invoice_details.get("base_grand_total"), - }) + b2cs_output.setdefault( + (rate, place_of_supply, ecommerce_gstin), + { + "place_of_supply": "", + "ecommerce_gstin": "", + "rate": "", + "taxable_value": 0, + "cess_amount": 0, + "type": "", + "invoice_number": invoice_details.get("invoice_number"), + "posting_date": invoice_details.get("posting_date"), + "invoice_value": invoice_details.get("base_grand_total"), + }, + ) row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) row["place_of_supply"] = place_of_supply row["ecommerce_gstin"] = ecommerce_gstin row["rate"] = rate - row["taxable_value"] += sum([abs(net_amount) - for item_code, net_amount in self.invoice_items.get(inv).items() if item_code in items]) + row["taxable_value"] += sum( + [ + abs(net_amount) + for item_code, net_amount in self.invoice_items.get(inv).items() + if item_code in items + ] + ) row["cess_amount"] += flt(self.invoice_cess.get(inv), 2) row["type"] = "E" if ecommerce_gstin else "OE" @@ -183,14 +194,17 @@ class Gstr1Report(object): def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items): row = [] for fieldname in self.invoice_fields: - if self.filters.get("type_of_business") in ("CDNR-REG", "CDNR-UNREG") and fieldname == "invoice_value": + if ( + self.filters.get("type_of_business") in ("CDNR-REG", "CDNR-UNREG") + and fieldname == "invoice_value" + ): row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total)) elif fieldname == "invoice_value": row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total) - elif fieldname in ('posting_date', 'shipping_bill_date'): - row.append(formatdate(invoice_details.get(fieldname), 'dd-MMM-YY')) + elif fieldname in ("posting_date", "shipping_bill_date"): + row.append(formatdate(invoice_details.get(fieldname), "dd-MMM-YY")) elif fieldname == "export_type": - export_type = "WPAY" if invoice_details.get(fieldname)=="With Payment of Tax" else "WOPAY" + export_type = "WPAY" if invoice_details.get(fieldname) == "With Payment of Tax" else "WOPAY" row.append(export_type) else: row.append(invoice_details.get(fieldname)) @@ -203,20 +217,25 @@ class Gstr1Report(object): for item_code, net_amount in self.invoice_items.get(invoice).items(): if item_code in items: - if self.item_tax_rate.get(invoice) and tax_rate/division_factor in self.item_tax_rate.get(invoice, {}).get(item_code, []): + if self.item_tax_rate.get(invoice) and tax_rate / division_factor in self.item_tax_rate.get( + invoice, {} + ).get(item_code, []): taxable_value += abs(net_amount) elif not self.item_tax_rate.get(invoice): taxable_value += abs(net_amount) elif tax_rate: taxable_value += abs(net_amount) - elif not tax_rate and self.filters.get('type_of_business') == 'EXPORT' \ - and invoice_details.get('export_type') == "Without Payment of Tax": + elif ( + not tax_rate + and self.filters.get("type_of_business") == "EXPORT" + and invoice_details.get("export_type") == "Without Payment of Tax" + ): taxable_value += abs(net_amount) row += [tax_rate or 0, taxable_value] for column in self.other_columns: - if column.get('fieldname') == 'cess_amount': + if column.get("fieldname") == "cess_amount": row.append(flt(self.invoice_cess.get(invoice), 2)) return row, taxable_value @@ -225,68 +244,82 @@ class Gstr1Report(object): self.invoices = frappe._dict() conditions = self.get_conditions() - invoice_data = frappe.db.sql(""" + invoice_data = frappe.db.sql( + """ select {select_columns} from `tab{doctype}` where docstatus = 1 {where_conditions} and is_opening = 'No' order by posting_date desc - """.format(select_columns=self.select_columns, doctype=self.doctype, - where_conditions=conditions), self.filters, as_dict=1) + """.format( + select_columns=self.select_columns, doctype=self.doctype, where_conditions=conditions + ), + self.filters, + as_dict=1, + ) for d in invoice_data: self.invoices.setdefault(d.invoice_number, d) def get_advance_entries(self): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT SUM(a.base_tax_amount) as amount, a.account_head, a.rate, p.place_of_supply FROM `tabPayment Entry` p, `tabAdvance Taxes and Charges` a WHERE p.docstatus = 1 AND p.name = a.parent AND posting_date between %s and %s GROUP BY a.account_head, p.place_of_supply, a.rate - """, (self.filters.get('from_date'), self.filters.get('to_date')), as_dict=1) + """, + (self.filters.get("from_date"), self.filters.get("to_date")), + as_dict=1, + ) def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), ("to_date", " and posting_date<=%(to_date)s"), ("company_address", " and company_address=%(company_address)s"), - ("company_gstin", " and company_gstin=%(company_gstin)s")): - if self.filters.get(opts[0]): - conditions += opts[1] + ("company_gstin", " and company_gstin=%(company_gstin)s"), + ): + if self.filters.get(opts[0]): + conditions += opts[1] - - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Registered Composition', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1" if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): - b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') + b2c_limit = frappe.db.get_single_value("GST Settings", "b2c_limit") if not b2c_limit: frappe.throw(_("Please set B2C Limit in GST Settings.")) - if self.filters.get("type_of_business") == "B2C Large": + if self.filters.get("type_of_business") == "B2C Large": conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') - AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit)) + AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format( + flt(b2c_limit) + ) - elif self.filters.get("type_of_business") == "B2C Small": + elif self.filters.get("type_of_business") == "B2C Small": conditions += """ AND ( SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2) - OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format(flt(b2c_limit)) + OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format( + flt(b2c_limit) + ) elif self.filters.get("type_of_business") == "CDNR-REG": conditions += """ AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ')""" elif self.filters.get("type_of_business") == "CDNR-UNREG": - b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') + b2c_limit = frappe.db.get_single_value("GST Settings", "b2c_limit") conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""" - elif self.filters.get("type_of_business") == "EXPORT": + elif self.filters.get("type_of_business") == "EXPORT": conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ conditions += " AND IFNULL(billing_address_gstin, '') != company_gstin" @@ -298,15 +331,22 @@ class Gstr1Report(object): self.item_tax_rate = frappe._dict() self.nil_exempt_non_gst = {} - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, is_non_gst from `tab%s Item` where parent in (%s) - """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (self.doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in items: self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0) + self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get( + "base_net_amount", 0 + ) item_tax_rate = {} @@ -320,15 +360,16 @@ class Gstr1Report(object): if d.is_nil_exempt: self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) if item_tax_rate: - self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][0] += d.get("taxable_value", 0) else: - self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][1] += d.get("taxable_value", 0) elif d.is_non_gst: self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0]) - self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0) + self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0) def get_items_based_on_tax_rate(self): - self.tax_details = frappe.db.sql(""" + self.tax_details = frappe.db.sql( + """ select parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` @@ -336,8 +377,10 @@ class Gstr1Report(object): parenttype = %s and docstatus = 1 and parent in (%s) order by account_head - """ % (self.tax_doctype, '%s', ', '.join(['%s']*len(self.invoices.keys()))), - tuple([self.doctype] + list(self.invoices.keys()))) + """ + % (self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), + tuple([self.doctype] + list(self.invoices.keys())), + ) self.items_based_on_tax_rate = {} self.invoice_cess = frappe._dict() @@ -353,8 +396,7 @@ class Gstr1Report(object): try: item_wise_tax_detail = json.loads(item_wise_tax_detail) cgst_or_sgst = False - if account in self.gst_accounts.cgst_account \ - or account in self.gst_accounts.sgst_account: + if account in self.gst_accounts.cgst_account or account in self.gst_accounts.sgst_account: cgst_or_sgst = True if not (cgst_or_sgst or account in self.gst_accounts.igst_account): @@ -371,22 +413,30 @@ class Gstr1Report(object): if parent not in self.cgst_sgst_invoices: self.cgst_sgst_invoices.append(parent) - rate_based_dict = self.items_based_on_tax_rate\ - .setdefault(parent, {}).setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: continue if unidentified_gst_accounts: - frappe.msgprint(_("Following accounts might be selected in GST Settings:") - + "
    " + "
    ".join(unidentified_gst_accounts), alert=True) + frappe.msgprint( + _("Following accounts might be selected in GST Settings:") + + "
    " + + "
    ".join(unidentified_gst_accounts), + alert=True, + ) # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ - and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \ - and self.invoices.get(invoice, {}).get('gst_category') in ("Overseas", "SEZ"): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) + if ( + invoice not in self.items_based_on_tax_rate + and invoice not in unidentified_gst_accounts_invoice + and self.invoices.get(invoice, {}).get("export_type") == "Without Payment of Tax" + and self.invoices.get(invoice, {}).get("gst_category") in ("Overseas", "SEZ") + ): + self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): self.other_columns = [] @@ -394,417 +444,258 @@ class Gstr1Report(object): if self.filters.get("type_of_business") != "NIL Rated": self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, + {"fieldname": "rate", "label": "Rate", "fieldtype": "Int", "width": 60}, { "fieldname": "taxable_value", "label": "Taxable Value", "fieldtype": "Currency", - "width": 100 - } + "width": 100, + }, ] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width":100 + "width": 150, }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 100}, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":100 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width":80 + "width": 100, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 80}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width":100 + "width": 100, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width":100 - }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data" - }, - { - "fieldname": "gst_category", - "label": "Invoice Type", - "fieldtype": "Data" + "width": 100, }, + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data"}, + {"fieldname": "gst_category", "label": "Invoice Type", "fieldtype": "Data"}, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width":120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } - ] + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} + ] - elif self.filters.get("type_of_business") == "B2C Large": + elif self.filters.get("type_of_business") == "B2C Large": self.invoice_columns = [ { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 100}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width": 130 - } + "width": 130, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } - ] + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} + ] elif self.filters.get("type_of_business") == "CDNR-REG": self.invoice_columns = [ { "fieldname": "billing_address_gstin", "label": "GSTIN/UIN of Recipient", "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width": 120 + "width": 150, }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 120}, { "fieldname": "return_against", "label": "Invoice/Advance Receipt Number", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 + "width": 120, }, { "fieldname": "posting_date", "label": "Invoice/Advance Receipt date", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Invoice/Advance Receipt Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data" - }, - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "hidden": 1 + "width": 120, }, + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data"}, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "hidden": 1}, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 140 + "width": 140, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "gst_category", - "label": "GST Category", - "fieldtype": "Data" + "width": 120, }, + {"fieldname": "gst_category", "label": "GST Category", "fieldtype": "Data"}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 80}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 80}, ] elif self.filters.get("type_of_business") == "CDNR-UNREG": self.invoice_columns = [ - { - "fieldname": "customer_name", - "label": "Receiver Name", - "fieldtype": "Data", - "width": 120 - }, + {"fieldname": "customer_name", "label": "Receiver Name", "fieldtype": "Data", "width": 120}, { "fieldname": "return_against", "label": "Issued Against", "fieldtype": "Link", "options": "Sales Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Note Date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Note Date", "fieldtype": "Date", "width": 120}, { "fieldname": "invoice_number", "label": "Note Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "hidden": 1 + "width": 120, }, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "hidden": 1}, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 140 + "width": 140, }, { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "gst_category", - "label": "GST Category", - "fieldtype": "Data" + "width": 120, }, + {"fieldname": "gst_category", "label": "GST Category", "fieldtype": "Data"}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 80}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 80}, ] - elif self.filters.get("type_of_business") == "B2C Small": + elif self.filters.get("type_of_business") == "B2C Small": self.invoice_columns = [ { "fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "ecommerce_gstin", "label": "E-Commerce GSTIN", "fieldtype": "Data", - "width": 130 - } + "width": 130, + }, ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "type", - "label": "Type", - "fieldtype": "Data", - "width": 50 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100}, + {"fieldname": "type", "label": "Type", "fieldtype": "Data", "width": 50}, ] - elif self.filters.get("type_of_business") == "EXPORT": + elif self.filters.get("type_of_business") == "EXPORT": self.invoice_columns = [ - { - "fieldname": "export_type", - "label": "Export Type", - "fieldtype": "Data", - "width":120 - }, + {"fieldname": "export_type", "label": "Export Type", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Sales Invoice", - "width":120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - }, - { - "fieldname": "port_code", - "label": "Port Code", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "port_code", "label": "Port Code", "fieldtype": "Data", "width": 120}, { "fieldname": "shipping_bill_number", "label": "Shipping Bill Number", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "shipping_bill_date", "label": "Shipping Bill Date", "fieldtype": "Data", - "width": 120 - } + "width": 120, + }, ] elif self.filters.get("type_of_business") == "Advances": self.invoice_columns = [ - { - "fieldname": "place_of_supply", - "label": "Place Of Supply", - "fieldtype": "Data", - "width": 120 - } + {"fieldname": "place_of_supply", "label": "Place Of Supply", "fieldtype": "Data", "width": 120} ] self.other_columns = [ - { - "fieldname": "cess_amount", - "label": "Cess Amount", - "fieldtype": "Currency", - "width": 100 - } + {"fieldname": "cess_amount", "label": "Cess Amount", "fieldtype": "Currency", "width": 100} ] elif self.filters.get("type_of_business") == "NIL Rated": self.invoice_columns = [ - { - "fieldname": "description", - "label": "Description", - "fieldtype": "Data", - "width": 420 - }, - { - "fieldname": "nil_rated", - "label": "Nil Rated", - "fieldtype": "Currency", - "width": 200 - }, - { - "fieldname": "exempted", - "label": "Exempted", - "fieldtype": "Currency", - "width": 200 - }, - { - "fieldname": "non_gst", - "label": "Non GST", - "fieldtype": "Currency", - "width": 200 - } + {"fieldname": "description", "label": "Description", "fieldtype": "Data", "width": 420}, + {"fieldname": "nil_rated", "label": "Nil Rated", "fieldtype": "Currency", "width": 200}, + {"fieldname": "exempted", "label": "Exempted", "fieldtype": "Currency", "width": 200}, + {"fieldname": "non_gst", "label": "Non GST", "fieldtype": "Currency", "width": 200}, ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns + @frappe.whitelist() def get_json(filters, report_name, data): filters = json.loads(filters) @@ -813,13 +704,14 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST3.0.4", - "hash": "hash", "gstin": gstin, "fp": fp} + gst_json = {"version": "GST3.0.4", "hash": "hash", "gstin": gstin, "fp": fp} res = {} if filters["type_of_business"] == "B2B": for item in report_data[:-1]: - res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"], []).append( + item + ) out = get_b2b_json(res, gstin) gst_json["b2b"] = out @@ -843,13 +735,15 @@ def get_json(filters, report_name, data): gst_json["exp"] = out elif filters["type_of_business"] == "CDNR-REG": for item in report_data[:-1]: - res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["billing_address_gstin"], {}).setdefault(item["invoice_number"], []).append( + item + ) out = get_cdnr_reg_json(res, gstin) gst_json["cdnr"] = out elif filters["type_of_business"] == "CDNR-UNREG": for item in report_data[:-1]: - res.setdefault(item["invoice_number"],[]).append(item) + res.setdefault(item["invoice_number"], []).append(item) out = get_cdnr_unreg_json(res, gstin) gst_json["cdnur"] = out @@ -857,10 +751,14 @@ def get_json(filters, report_name, data): elif filters["type_of_business"] == "Advances": for item in report_data[:-1]: if not item.get("place_of_supply"): - frappe.throw(_("""{0} not entered in some entries. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some entries. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - res.setdefault(item["place_of_supply"],[]).append(item) + res.setdefault(item["place_of_supply"], []).append(item) out = get_advances_json(res, gstin) gst_json["at"] = out @@ -870,30 +768,34 @@ def get_json(filters, report_name, data): out = get_exempted_json(res) gst_json["nil"] = out - return { - 'report_name': report_name, - 'report_type': filters['type_of_business'], - 'data': gst_json - } + return {"report_name": report_name, "report_type": filters["type_of_business"], "data": gst_json} + def get_b2b_json(res, gstin): out = [] for gst_in in res: b2b_item, inv = {"ctin": gst_in, "inv": []}, [] - if not gst_in: continue + if not gst_in: + continue for number, invoice in iteritems(res[gst_in]): if not invoice[0]["place_of_supply"]: - frappe.throw(_("""{0} not entered in Invoice {1}. - Please update and try again""").format(frappe.bold("Place Of Supply"), - frappe.bold(invoice[0]['invoice_number']))) + frappe.throw( + _( + """{0} not entered in Invoice {1}. + Please update and try again""" + ).format( + frappe.bold("Place Of Supply"), frappe.bold(invoice[0]["invoice_number"]) + ) + ) inv_item = get_basic_invoice_detail(invoice[0]) - inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]) + inv_item["pos"] = "%02d" % int(invoice[0]["place_of_supply"].split("-")[0]) inv_item["rchrg"] = invoice[0]["reverse_charge"] inv_item["inv_typ"] = get_invoice_type(invoice[0]) - if inv_item["pos"]=="00": continue + if inv_item["pos"] == "00": + continue inv_item["itms"] = [] for item in invoice: @@ -901,95 +803,101 @@ def get_b2b_json(res, gstin): inv.append(inv_item) - if not inv: continue + if not inv: + continue b2b_item["inv"] = inv out.append(b2b_item) return out + def get_b2cs_json(data, gstin): company_state_number = gstin[0:2] out = [] for d in data: if not d.get("place_of_supply"): - frappe.throw(_("""{0} not entered in some invoices. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some invoices. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - pos = d.get('place_of_supply').split('-')[0] + pos = d.get("place_of_supply").split("-")[0] tax_details = {} - rate = d.get('rate', 0) - tax = flt((d["taxable_value"]*rate)/100.0, 2) + rate = d.get("rate", 0) + tax = flt((d["taxable_value"] * rate) / 100.0, 2) if company_state_number == pos: - tax_details.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) + tax_details.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)}) else: tax_details.update({"iamt": tax}) inv = { "sply_ty": "INTRA" if company_state_number == pos else "INTER", "pos": pos, - "typ": d.get('type'), - "txval": flt(d.get('taxable_value'), 2), + "typ": d.get("type"), + "txval": flt(d.get("taxable_value"), 2), "rt": rate, - "iamt": flt(tax_details.get('iamt'), 2), - "camt": flt(tax_details.get('camt'), 2), - "samt": flt(tax_details.get('samt'), 2), - "csamt": flt(d.get('cess_amount'), 2) + "iamt": flt(tax_details.get("iamt"), 2), + "camt": flt(tax_details.get("camt"), 2), + "samt": flt(tax_details.get("samt"), 2), + "csamt": flt(d.get("cess_amount"), 2), } - if d.get('type') == "E" and d.get('ecommerce_gstin'): - inv.update({ - "etin": d.get('ecommerce_gstin') - }) + if d.get("type") == "E" and d.get("ecommerce_gstin"): + inv.update({"etin": d.get("ecommerce_gstin")}) out.append(inv) return out + def get_advances_json(data, gstin): company_state_number = gstin[0:2] out = [] for place_of_supply, items in iteritems(data): - supply_type = "INTRA" if company_state_number == place_of_supply.split('-')[0] else "INTER" - row = { - "pos": place_of_supply.split('-')[0], - "itms": [], - "sply_ty": supply_type - } + supply_type = "INTRA" if company_state_number == place_of_supply.split("-")[0] else "INTER" + row = {"pos": place_of_supply.split("-")[0], "itms": [], "sply_ty": supply_type} for item in items: itms = { - 'rt': item['rate'], - 'ad_amount': flt(item.get('taxable_value')), - 'csamt': flt(item.get('cess_amount')) + "rt": item["rate"], + "ad_amount": flt(item.get("taxable_value")), + "csamt": flt(item.get("cess_amount")), } if supply_type == "INTRA": - itms.update({ - "samt": flt((itms["ad_amount"] * itms["rt"]) / 100), - "camt": flt((itms["ad_amount"] * itms["rt"]) / 100), - "rt": itms["rt"] * 2 - }) + itms.update( + { + "samt": flt((itms["ad_amount"] * itms["rt"]) / 100), + "camt": flt((itms["ad_amount"] * itms["rt"]) / 100), + "rt": itms["rt"] * 2, + } + ) else: - itms.update({ - "iamt": flt((itms["ad_amount"] * itms["rt"]) / 100) - }) + itms.update({"iamt": flt((itms["ad_amount"] * itms["rt"]) / 100)}) - row['itms'].append(itms) + row["itms"].append(itms) out.append(row) return out + def get_b2cl_json(res, gstin): out = [] for pos in res: if not pos: - frappe.throw(_("""{0} not entered in some invoices. - Please update and try again""").format(frappe.bold("Place Of Supply"))) + frappe.throw( + _( + """{0} not entered in some invoices. + Please update and try again""" + ).format(frappe.bold("Place Of Supply")) + ) - b2cl_item, inv = {"pos": "%02d" % int(pos.split('-')[0]), "inv": []}, [] + b2cl_item, inv = {"pos": "%02d" % int(pos.split("-")[0]), "inv": []}, [] for row in res[pos]: inv_item = get_basic_invoice_detail(row) @@ -1005,6 +913,7 @@ def get_b2cl_json(res, gstin): return out + def get_export_json(res): out = [] for exp_type in res: @@ -1012,12 +921,9 @@ def get_export_json(res): for row in res[exp_type]: inv_item = get_basic_invoice_detail(row) - inv_item["itms"] = [{ - "txval": flt(row["taxable_value"], 2), - "rt": row["rate"] or 0, - "iamt": 0, - "csamt": 0 - }] + inv_item["itms"] = [ + {"txval": flt(row["taxable_value"], 2), "rt": row["rate"] or 0, "iamt": 0, "csamt": 0} + ] inv.append(inv_item) @@ -1026,27 +932,34 @@ def get_export_json(res): return out + def get_cdnr_reg_json(res, gstin): out = [] for gst_in in res: cdnr_item, inv = {"ctin": gst_in, "nt": []}, [] - if not gst_in: continue + if not gst_in: + continue for number, invoice in iteritems(res[gst_in]): if not invoice[0]["place_of_supply"]: - frappe.throw(_("""{0} not entered in Invoice {1}. - Please update and try again""").format(frappe.bold("Place Of Supply"), - frappe.bold(invoice[0]['invoice_number']))) + frappe.throw( + _( + """{0} not entered in Invoice {1}. + Please update and try again""" + ).format( + frappe.bold("Place Of Supply"), frappe.bold(invoice[0]["invoice_number"]) + ) + ) inv_item = { "nt_num": invoice[0]["invoice_number"], - "nt_dt": getdate(invoice[0]["posting_date"]).strftime('%d-%m-%Y'), + "nt_dt": getdate(invoice[0]["posting_date"]).strftime("%d-%m-%Y"), "val": abs(flt(invoice[0]["invoice_value"])), "ntty": invoice[0]["document_type"], - "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), + "pos": "%02d" % int(invoice[0]["place_of_supply"].split("-")[0]), "rchrg": invoice[0]["reverse_charge"], - "inv_typ": get_invoice_type(invoice[0]) + "inv_typ": get_invoice_type(invoice[0]), } inv_item["itms"] = [] @@ -1055,23 +968,25 @@ def get_cdnr_reg_json(res, gstin): inv.append(inv_item) - if not inv: continue + if not inv: + continue cdnr_item["nt"] = inv out.append(cdnr_item) return out + def get_cdnr_unreg_json(res, gstin): out = [] for invoice, items in iteritems(res): inv_item = { "nt_num": items[0]["invoice_number"], - "nt_dt": getdate(items[0]["posting_date"]).strftime('%d-%m-%Y'), + "nt_dt": getdate(items[0]["posting_date"]).strftime("%d-%m-%Y"), "val": abs(flt(items[0]["invoice_value"])), "ntty": items[0]["document_type"], - "pos": "%02d" % int(items[0]["place_of_supply"].split('-')[0]), - "typ": get_invoice_type(items[0]) + "pos": "%02d" % int(items[0]["place_of_supply"].split("-")[0]), + "typ": get_invoice_type(items[0]), } inv_item["itms"] = [] @@ -1082,63 +997,62 @@ def get_cdnr_unreg_json(res, gstin): return out + def get_exempted_json(data): out = { "inv": [ - { - "sply_ty": "INTRB2B" - }, - { - "sply_ty": "INTRAB2B" - }, - { - "sply_ty": "INTRB2C" - }, - { - "sply_ty": "INTRAB2C" - } + {"sply_ty": "INTRB2B"}, + {"sply_ty": "INTRAB2B"}, + {"sply_ty": "INTRB2C"}, + {"sply_ty": "INTRAB2C"}, ] } for i, v in enumerate(data): - if data[i].get('nil_rated'): - out['inv'][i]['nil_amt'] = data[i]['nil_rated'] + if data[i].get("nil_rated"): + out["inv"][i]["nil_amt"] = data[i]["nil_rated"] - if data[i].get('exempted'): - out['inv'][i]['expt_amt'] = data[i]['exempted'] + if data[i].get("exempted"): + out["inv"][i]["expt_amt"] = data[i]["exempted"] - if data[i].get('non_gst'): - out['inv'][i]['ngsup_amt'] = data[i]['non_gst'] + if data[i].get("non_gst"): + out["inv"][i]["ngsup_amt"] = data[i]["non_gst"] return out + def get_invoice_type(row): - gst_category = row.get('gst_category') + gst_category = row.get("gst_category") - if gst_category == 'SEZ': - return 'SEWP' if row.get('export_type') == 'WPAY' else 'SEWOP' + if gst_category == "SEZ": + return "SEWP" if row.get("export_type") == "WPAY" else "SEWOP" - if gst_category == 'Overseas': - return 'EXPWP' if row.get('export_type') == 'WPAY' else 'EXPWOP' + if gst_category == "Overseas": + return "EXPWP" if row.get("export_type") == "WPAY" else "EXPWOP" + + return ( + { + "Deemed Export": "DE", + "Registered Regular": "R", + "Registered Composition": "R", + "Unregistered": "B2CL", + } + ).get(gst_category) - return ({ - 'Deemed Export': 'DE', - 'Registered Regular': 'R', - 'Registered Composition': 'R', - 'Unregistered': 'B2CL' - }).get(gst_category) def get_basic_invoice_detail(row): return { "inum": row["invoice_number"], - "idt": getdate(row["posting_date"]).strftime('%d-%m-%Y'), - "val": flt(row["invoice_value"], 2) + "idt": getdate(row["posting_date"]).strftime("%d-%m-%Y"), + "val": flt(row["invoice_value"], 2), } + def get_rate_and_tax_details(row, gstin): - itm_det = {"txval": flt(row["taxable_value"], 2), + itm_det = { + "txval": flt(row["taxable_value"], 2), "rt": row["rate"], - "csamt": (flt(row.get("cess_amount"), 2) or 0) + "csamt": (flt(row.get("cess_amount"), 2) or 0), } # calculate rate @@ -1146,17 +1060,18 @@ def get_rate_and_tax_details(row, gstin): rate = row.get("rate") or 0 # calculate tax amount added - tax = flt((row["taxable_value"]*rate)/100.0, 2) - frappe.errprint([tax, tax/2]) + tax = flt((row["taxable_value"] * rate) / 100.0, 2) + frappe.errprint([tax, tax / 2]) if row.get("billing_address_gstin") and gstin[0:2] == row["billing_address_gstin"][0:2]: - itm_det.update({"camt": flt(tax/2.0, 2), "samt": flt(tax/2.0, 2)}) + itm_det.update({"camt": flt(tax / 2.0, 2), "samt": flt(tax / 2.0, 2)}) else: itm_det.update({"iamt": tax}) return {"num": int(num), "itm_det": itm_det} + def get_company_gstin_number(company, address=None, all_gstins=False): - gstin = '' + gstin = "" if address: gstin = frappe.db.get_value("Address", address, "gstin") @@ -1166,28 +1081,36 @@ def get_company_gstin_number(company, address=None, all_gstins=False): ["Dynamic Link", "link_doctype", "=", "Company"], ["Dynamic Link", "link_name", "=", company], ["Dynamic Link", "parenttype", "=", "Address"], - ["gstin", "!=", ''] + ["gstin", "!=", ""], ] - gstin = frappe.get_all("Address", filters=filters, pluck="gstin", order_by="is_primary_address desc") + gstin = frappe.get_all( + "Address", filters=filters, pluck="gstin", order_by="is_primary_address desc" + ) if gstin and not all_gstins: gstin = gstin[0] if not gstin: address = frappe.bold(address) if address else "" - frappe.throw(_("Please set valid GSTIN No. in Company Address {} for company {}").format( - address, frappe.bold(company) - )) + frappe.throw( + _("Please set valid GSTIN No. in Company Address {} for company {}").format( + address, frappe.bold(company) + ) + ) return gstin + @frappe.whitelist() def download_json_file(): - ''' download json content in a file ''' + """download json content in a file""" data = frappe._dict(frappe.local.form_dict) - frappe.response['filename'] = frappe.scrub("{0} {1}".format(data['report_name'], data['report_type'])) + '.json' - frappe.response['filecontent'] = data['data'] - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' + frappe.response["filename"] = ( + frappe.scrub("{0} {1}".format(data["report_name"], data["report_type"])) + ".json" + ) + frappe.response["filecontent"] = data["data"] + frappe.response["content_type"] = "application/json" + frappe.response["type"] = "download" + def is_inter_state(invoice_detail): if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]: @@ -1201,16 +1124,16 @@ def get_company_gstins(company): address = frappe.qb.DocType("Address") links = frappe.qb.DocType("Dynamic Link") - addresses = frappe.qb.from_(address).inner_join(links).on( - address.name == links.parent - ).select( - address.gstin - ).where( - links.link_doctype == 'Company' - ).where( - links.link_name == company - ).run(as_dict=1) + addresses = ( + frappe.qb.from_(address) + .inner_join(links) + .on(address.name == links.parent) + .select(address.gstin) + .where(links.link_doctype == "Company") + .where(links.link_name == company) + .run(as_dict=1) + ) - address_list = [''] + [d.gstin for d in addresses] + address_list = [""] + [d.gstin for d in addresses] - return address_list \ No newline at end of file + return address_list diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py index 47c856dfaae..a189d2a500b 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.py +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -12,6 +12,7 @@ from erpnext.regional.report.gstr_1.gstr_1 import Gstr1Report def execute(filters=None): return Gstr2Report(filters).run() + class Gstr2Report(Gstr1Report): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -47,7 +48,7 @@ class Gstr2Report(Gstr1Report): for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): - if rate or invoice_details.get('gst_category') == 'Registered Composition': + if rate or invoice_details.get("gst_category") == "Registered Composition": if inv not in self.igst_invoices: rate = rate / 2 row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) @@ -60,13 +61,13 @@ class Gstr2Report(Gstr1Report): row += [ self.invoice_cess.get(inv), - invoice_details.get('eligibility_for_itc'), - invoice_details.get('itc_integrated_tax'), - invoice_details.get('itc_central_tax'), - invoice_details.get('itc_state_tax'), - invoice_details.get('itc_cess_amount') + invoice_details.get("eligibility_for_itc"), + invoice_details.get("itc_integrated_tax"), + invoice_details.get("itc_central_tax"), + invoice_details.get("itc_state_tax"), + invoice_details.get("itc_cess_amount"), ] - if self.filters.get("type_of_business") == "CDNR": + if self.filters.get("type_of_business") == "CDNR": row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N") row.append("C" if invoice_details.return_against else "R") @@ -82,201 +83,158 @@ class Gstr2Report(Gstr1Report): def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): - if self.filters.get(opts[0]): - conditions += opts[1] + ("to_date", " and posting_date<=%(to_date)s"), + ): + if self.filters.get(opts[0]): + conditions += opts[1] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ', 'Registered Composition') and is_return != 1 " - elif self.filters.get("type_of_business") == "CDNR": + elif self.filters.get("type_of_business") == "CDNR": conditions += """ and is_return = 1 """ return conditions def get_columns(self): self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, - { - "fieldname": "taxable_value", - "label": "Taxable Value", - "fieldtype": "Currency", - "width": 100 - }, + {"fieldname": "rate", "label": "Rate", "fieldtype": "Int", "width": 60}, + {"fieldname": "taxable_value", "label": "Taxable Value", "fieldtype": "Currency", "width": 100}, { "fieldname": "integrated_tax_paid", "label": "Integrated Tax Paid", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "central_tax_paid", "label": "Central Tax Paid", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "state_tax_paid", "label": "State/UT Tax Paid", "fieldtype": "Currency", - "width": 100 - }, - { - "fieldname": "cess_amount", - "label": "Cess Paid", - "fieldtype": "Currency", - "width": 100 + "width": 100, }, + {"fieldname": "cess_amount", "label": "Cess Paid", "fieldtype": "Currency", "width": 100}, { "fieldname": "eligibility_for_itc", "label": "Eligibility For ITC", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "fieldname": "itc_integrated_tax", "label": "Availed ITC Integrated Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_central_tax", "label": "Availed ITC Central Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_state_tax", "label": "Availed ITC State/UT Tax", "fieldtype": "Currency", - "width": 100 + "width": 100, }, { "fieldname": "itc_cess_amount", "label": "Availed ITC Cess ", "fieldtype": "Currency", - "width": 100 - } + "width": 100, + }, ] self.other_columns = [] - if self.filters.get("type_of_business") == "B2B": + if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ { "fieldname": "supplier_gstin", "label": "GSTIN of Supplier", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Invoice Number", "fieldtype": "Link", "options": "Purchase Invoice", - "width": 120 - }, - { - "fieldname": "posting_date", - "label": "Invoice date", - "fieldtype": "Date", - "width": 120 + "width": 120, }, + {"fieldname": "posting_date", "label": "Invoice date", "fieldtype": "Date", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 + "width": 120, }, { "fieldname": "place_of_supply", "label": "Place of Supply", "fieldtype": "Data", - "width": 120 + "width": 120, }, - { - "fieldname": "reverse_charge", - "label": "Reverse Charge", - "fieldtype": "Data", - "width": 80 - }, - { - "fieldname": "gst_category", - "label": "Invoice Type", - "fieldtype": "Data", - "width": 80 - } + {"fieldname": "reverse_charge", "label": "Reverse Charge", "fieldtype": "Data", "width": 80}, + {"fieldname": "gst_category", "label": "Invoice Type", "fieldtype": "Data", "width": 80}, ] - elif self.filters.get("type_of_business") == "CDNR": + elif self.filters.get("type_of_business") == "CDNR": self.invoice_columns = [ { "fieldname": "supplier_gstin", "label": "GSTIN of Supplier", "fieldtype": "Data", - "width": 120 + "width": 120, }, { "fieldname": "invoice_number", "label": "Note/Refund Voucher Number", "fieldtype": "Link", - "options": "Purchase Invoice" + "options": "Purchase Invoice", }, { "fieldname": "posting_date", "label": "Note/Refund Voucher date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "fieldname": "return_against", "label": "Invoice/Advance Payment Voucher Number", "fieldtype": "Link", "options": "Purchase Invoice", - "width": 120 + "width": 120, }, { "fieldname": "posting_date", "label": "Invoice/Advance Payment Voucher date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "fieldname": "reason_for_issuing_document", "label": "Reason For Issuing document", "fieldtype": "Data", - "width": 120 - }, - { - "fieldname": "supply_type", - "label": "Supply Type", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"fieldname": "supply_type", "label": "Supply Type", "fieldtype": "Data", "width": 120}, { "fieldname": "invoice_value", "label": "Invoice Value", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] self.other_columns = [ - { - "fieldname": "pre_gst", - "label": "PRE GST", - "fieldtype": "Data", - "width": 50 - }, - { - "fieldname": "document_type", - "label": "Document Type", - "fieldtype": "Data", - "width": 50 - } + {"fieldname": "pre_gst", "label": "PRE GST", "fieldtype": "Data", "width": 50}, + {"fieldname": "document_type", "label": "Document Type", "fieldtype": "Data", "width": 50}, ] self.columns = self.invoice_columns + self.tax_columns + self.other_columns diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index ac78a8107df..da9dd40a010 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -18,8 +18,10 @@ from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) + def _execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() company_currency = erpnext.get_company_currency(filters.company) @@ -47,9 +49,10 @@ def _execute(filters=None): data.append(row) added_item.append((d.parent, d.item_code)) if data: - data = get_merged_data(columns, data) # merge same hsn code data + data = get_merged_data(columns, data) # merge same hsn code data return columns, data + def get_columns(): columns = [ { @@ -57,63 +60,47 @@ def get_columns(): "label": _("HSN/SAC"), "fieldtype": "Link", "options": "GST HSN Code", - "width": 100 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 300 - }, - { - "fieldname": "stock_uom", - "label": _("Stock UOM"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "stock_qty", - "label": _("Stock Qty"), - "fieldtype": "Float", - "width": 90 - }, - { - "fieldname": "total_amount", - "label": _("Total Amount"), - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, + {"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, + {"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, + {"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, { "fieldname": "taxable_amount", "label": _("Total Taxable Amount"), "fieldtype": "Currency", - "width": 170 - } + "width": 170, + }, ] return columns + def get_conditions(filters): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("gst_hsn_code", " and gst_hsn_code=%(gst_hsn_code)s"), ("company_gstin", " and company_gstin=%(company_gstin)s"), ("from_date", " and posting_date >= %(from_date)s"), - ("to_date", "and posting_date <= %(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + ("to_date", "and posting_date <= %(to_date)s"), + ): + if filters.get(opts[0]): + conditions += opts[1] return conditions + def get_items(filters): conditions = get_conditions(filters) match_conditions = frappe.build_match_conditions("Sales Invoice") if match_conditions: match_conditions = " and {0} ".format(match_conditions) - - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_uom, @@ -130,25 +117,41 @@ def get_items(filters): group by `tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code - """ % (conditions, match_conditions), filters, as_dict=1) + """ + % (conditions, match_conditions), + filters, + as_dict=1, + ) return items -def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): + +def get_tax_accounts( + item_list, + columns, + company_currency, + doctype="Sales Invoice", + tax_doctype="Sales Taxes and Charges", +): item_row_map = {} tax_columns = [] invoice_item_row = {} itemised_tax = {} conditions = "" - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"), - currency=company_currency) or 2 + tax_amount_precision = ( + get_field_precision( + frappe.get_meta(tax_doctype).get_field("tax_amount"), currency=company_currency + ) + or 2 + ) for d in item_list: invoice_item_row.setdefault(d.parent, []).append(d) item_row_map.setdefault(d.parent, {}).setdefault(d.item_code or d.item_name, []).append(d) - tax_details = frappe.db.sql(""" + tax_details = frappe.db.sql( + """ select parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount @@ -159,8 +162,10 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic and parent in (%s) %s order by description - """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), - tuple([doctype] + list(invoice_item_row))) + """ + % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), + tuple([doctype] + list(invoice_item_row)), + ) for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: @@ -184,74 +189,74 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ - "tax_amount": flt(item_tax_amount, tax_amount_precision) - }) + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict( + {"tax_amount": flt(item_tax_amount, tax_amount_precision)} + ) except ValueError: continue tax_columns.sort() for account_head in tax_columns: - columns.append({ - "label": account_head, - "fieldname": frappe.scrub(account_head), - "fieldtype": "Float", - "width": 110 - }) + columns.append( + { + "label": account_head, + "fieldname": frappe.scrub(account_head), + "fieldtype": "Float", + "width": 110, + } + ) return itemised_tax, tax_columns + def get_merged_data(columns, data): - merged_hsn_dict = {} # to group same hsn under one key and perform row addition + merged_hsn_dict = {} # to group same hsn under one key and perform row addition result = [] for row in data: merged_hsn_dict.setdefault(row[0], {}) for i, d in enumerate(columns): - if d['fieldtype'] not in ('Int', 'Float', 'Currency'): - merged_hsn_dict[row[0]][d['fieldname']] = row[i] + if d["fieldtype"] not in ("Int", "Float", "Currency"): + merged_hsn_dict[row[0]][d["fieldname"]] = row[i] else: - if merged_hsn_dict.get(row[0], {}).get(d['fieldname'], ''): - merged_hsn_dict[row[0]][d['fieldname']] += row[i] + if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""): + merged_hsn_dict[row[0]][d["fieldname"]] += row[i] else: - merged_hsn_dict[row[0]][d['fieldname']] = row[i] + merged_hsn_dict[row[0]][d["fieldname"]] = row[i] for key, value in iteritems(merged_hsn_dict): result.append(value) return result + @frappe.whitelist() def get_json(filters, report_name, data): filters = json.loads(filters) report_data = json.loads(data) - gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + gstin = filters.get("company_gstin") or get_company_gstin_number(filters["company"]) - if not filters.get('from_date') or not filters.get('to_date'): + if not filters.get("from_date") or not filters.get("to_date"): frappe.throw(_("Please enter From Date and To Date to generate JSON")) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST2.3.4", - "hash": "hash", "gstin": gstin, "fp": fp} + gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} - gst_json["hsn"] = { - "data": get_hsn_wise_json_data(filters, report_data) - } + gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)} + + return {"report_name": report_name, "data": gst_json} - return { - 'report_name': report_name, - 'data': gst_json - } @frappe.whitelist() def download_json_file(): - '''download json content in a file''' + """download json content in a file""" data = frappe._dict(frappe.local.form_dict) - frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' - frappe.response['filecontent'] = data['data'] - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' + frappe.response["filename"] = frappe.scrub("{0}".format(data["report_name"])) + ".json" + frappe.response["filecontent"] = data["data"] + frappe.response["content_type"] = "application/json" + frappe.response["type"] = "download" + def get_hsn_wise_json_data(filters, report_data): @@ -272,23 +277,22 @@ def get_hsn_wise_json_data(filters, report_data): "iamt": 0.0, "camt": 0.0, "samt": 0.0, - "csamt": 0.0 - + "csamt": 0.0, } - for account in gst_accounts.get('igst_account'): - row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("igst_account"): + row["iamt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('cgst_account'): - row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("cgst_account"): + row["camt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('sgst_account'): - row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("sgst_account"): + row["samt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) - for account in gst_accounts.get('cess_account'): - row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + for account in gst_accounts.get("cess_account"): + row["csamt"] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) data.append(row) - count +=1 + count += 1 return data diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py index 86dc458bdb1..090473f4fdc 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/test_hsn_wise_summary_of_outward_supplies.py @@ -28,7 +28,7 @@ class TestHSNWiseSummaryReport(TestCase): setup_company() setup_customers() setup_gst_settings() - make_item("Golf Car", properties={ "gst_hsn_code": "999900" }) + make_item("Golf Car", properties={"gst_hsn_code": "999900"}) @classmethod def tearDownClass(cls): @@ -37,53 +37,66 @@ class TestHSNWiseSummaryReport(TestCase): def test_hsn_summary_for_invoice_with_duplicate_items(self): si = create_sales_invoice( company="_Test Company GST", - customer = "_Test GST Customer", - currency = "INR", - warehouse = "Finished Goods - _GST", - debit_to = "Debtors - _GST", - income_account = "Sales - _GST", - expense_account = "Cost of Goods Sold - _GST", - cost_center = "Main - _GST", - do_not_save=1 + customer="_Test GST Customer", + currency="INR", + warehouse="Finished Goods - _GST", + debit_to="Debtors - _GST", + income_account="Sales - _GST", + expense_account="Cost of Goods Sold - _GST", + cost_center="Main - _GST", + do_not_save=1, ) si.items = [] - si.append("items", { - "item_code": "Golf Car", - "gst_hsn_code": "999900", - "qty": "1", - "rate": "120", - "cost_center": "Main - _GST" - }) - si.append("items", { - "item_code": "Golf Car", - "gst_hsn_code": "999900", - "qty": "1", - "rate": "140", - "cost_center": "Main - _GST" - }) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax IGST - _GST", - "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 - }) + si.append( + "items", + { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "120", + "cost_center": "Main - _GST", + }, + ) + si.append( + "items", + { + "item_code": "Golf Car", + "gst_hsn_code": "999900", + "qty": "1", + "rate": "140", + "cost_center": "Main - _GST", + }, + ) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18, + }, + ) si.posting_date = "2020-11-17" si.submit() si.reload() - [columns, data] = run_report(filters=frappe._dict({ - "company": "_Test Company GST", - "gst_hsn_code": "999900", - "company_gstin": si.company_gstin, - "from_date": si.posting_date, - "to_date": si.posting_date - })) + [columns, data] = run_report( + filters=frappe._dict( + { + "company": "_Test Company GST", + "gst_hsn_code": "999900", + "company_gstin": si.company_gstin, + "from_date": si.posting_date, + "to_date": si.posting_date, + } + ) + ) - filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data)) + filtered_rows = list(filter(lambda row: row["gst_hsn_code"] == "999900", data)) self.assertTrue(filtered_rows) hsn_row = filtered_rows[0] - self.assertEquals(hsn_row['stock_qty'], 2.0) - self.assertEquals(hsn_row['total_amount'], 306.8) + self.assertEquals(hsn_row["stock_qty"], 2.0) + self.assertEquals(hsn_row["total_amount"], 306.8) diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index b1a5d109621..749bf95e969 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -20,23 +20,22 @@ IRS_1099_FORMS_FILE_EXTENSION = ".pdf" def execute(filters=None): filters = filters if isinstance(filters, frappe._dict) else frappe._dict(filters) if not filters: - filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) - filters.setdefault('company', frappe.db.get_default("company")) + filters.setdefault("fiscal_year", get_fiscal_year(nowdate())[0]) + filters.setdefault("company", frappe.db.get_default("company")) - region = frappe.db.get_value("Company", - filters={"name": filters.company}, - fieldname=["country"]) + region = frappe.db.get_value("Company", filters={"name": filters.company}, fieldname=["country"]) - if region != 'United States': + if region != "United States": return [], [] data = [] columns = get_columns() conditions = "" if filters.supplier_group: - conditions += "AND s.supplier_group = %s" %frappe.db.escape(filters.get("supplier_group")) + conditions += "AND s.supplier_group = %s" % frappe.db.escape(filters.get("supplier_group")) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT s.supplier_group as "supplier_group", gl.party AS "supplier", @@ -57,10 +56,12 @@ def execute(filters=None): gl.party ORDER BY - gl.party DESC""".format(conditions=conditions), { - "fiscal_year": filters.fiscal_year, - "company": filters.company - }, as_dict=True) + gl.party DESC""".format( + conditions=conditions + ), + {"fiscal_year": filters.fiscal_year, "company": filters.company}, + as_dict=True, + ) return columns, data @@ -72,37 +73,29 @@ def get_columns(): "label": _("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group", - "width": 200 + "width": 200, }, { "fieldname": "supplier", "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", - "width": 200 + "width": 200, }, - { - "fieldname": "tax_id", - "label": _("Tax ID"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "payments", - "label": _("Total Payments"), - "fieldtype": "Currency", - "width": 200 - } + {"fieldname": "tax_id", "label": _("Tax ID"), "fieldtype": "Data", "width": 200}, + {"fieldname": "payments", "label": _("Total Payments"), "fieldtype": "Currency", "width": 200}, ] @frappe.whitelist() def irs_1099_print(filters): if not filters: - frappe._dict({ - "company": frappe.db.get_default("Company"), - "fiscal_year": frappe.db.get_default("Fiscal Year") - }) + frappe._dict( + { + "company": frappe.db.get_default("Company"), + "fiscal_year": frappe.db.get_default("Fiscal Year"), + } + ) else: filters = frappe._dict(json.loads(filters)) @@ -122,17 +115,21 @@ def irs_1099_print(filters): row["company_tin"] = company_tin row["payer_street_address"] = company_address row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html( - "Supplier", row.supplier) + "Supplier", row.supplier + ) row["payments"] = fmt_money(row["payments"], precision=0, currency="USD") pdf = get_pdf(render_template(template, row), output=output if output else None) - frappe.local.response.filename = f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}" + frappe.local.response.filename = ( + f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}" + ) frappe.local.response.filecontent = read_multi_pdf(output) frappe.local.response.type = "download" def get_payer_address_html(company): - address_list = frappe.db.sql(""" + address_list = frappe.db.sql( + """ SELECT name FROM @@ -142,7 +139,10 @@ def get_payer_address_html(company): ORDER BY address_type="Postal" DESC, address_type="Billing" DESC LIMIT 1 - """, {"company": company}, as_dict=True) + """, + {"company": company}, + as_dict=True, + ) address_display = "" if address_list: @@ -153,7 +153,8 @@ def get_payer_address_html(company): def get_street_address_html(party_type, party): - address_list = frappe.db.sql(""" + address_list = frappe.db.sql( + """ SELECT link.parent FROM @@ -166,7 +167,10 @@ def get_street_address_html(party_type, party): address.address_type="Postal" DESC, address.address_type="Billing" DESC LIMIT 1 - """, {"party": party}, as_dict=True) + """, + {"party": party}, + as_dict=True, + ) street_address = city_state = "" if address_list: diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index cc26bd7a57a..15996d2d1f8 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -14,6 +14,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(): return [ { @@ -48,101 +49,136 @@ def get_columns(): "label": _("Currency"), "fieldtype": "Currency", "width": 150, - "hidden": 1 - } + "hidden": 1, + }, ] + def get_data(filters): data = [] # Validate if vat settings exist - company = filters.get('company') - company_currency = frappe.get_cached_value('Company', company, "default_currency") + company = filters.get("company") + company_currency = frappe.get_cached_value("Company", company, "default_currency") - if frappe.db.exists('KSA VAT Setting', company) is None: - url = get_url_to_list('KSA VAT Setting') + if frappe.db.exists("KSA VAT Setting", company) is None: + url = get_url_to_list("KSA VAT Setting") frappe.msgprint(_('Create KSA VAT Setting for this company').format(url)) return data - ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company) + ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company) # Sales Heading - append_data(data, 'VAT on Sales', '', '', '', company_currency) + append_data(data, "VAT on Sales", "", "", "", company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 grand_total_tax = 0 for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts: - total_taxable_amount, total_taxable_adjustment_amount, \ - total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Sales Invoice') + ( + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice") # Adding results to data - append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax, company_currency) + append_data( + data, + vat_setting.title, + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + company_currency, + ) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_tax += total_tax # Sales Grand Total - append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) + append_data( + data, + "Grand Total", + grand_total_taxable_amount, + grand_total_taxable_adjustment_amount, + grand_total_tax, + company_currency, + ) # Blank Line - append_data(data, '', '', '', '', company_currency) + append_data(data, "", "", "", "", company_currency) # Purchase Heading - append_data(data, 'VAT on Purchases', '', '', '', company_currency) + append_data(data, "VAT on Purchases", "", "", "", company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 grand_total_tax = 0 for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts: - total_taxable_amount, total_taxable_adjustment_amount, \ - total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Purchase Invoice') + ( + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + ) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice") # Adding results to data - append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax, company_currency) + append_data( + data, + vat_setting.title, + total_taxable_amount, + total_taxable_adjustment_amount, + total_tax, + company_currency, + ) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount grand_total_tax += total_tax # Purchase Grand Total - append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) + append_data( + data, + "Grand Total", + grand_total_taxable_amount, + grand_total_taxable_adjustment_amount, + grand_total_tax, + company_currency, + ) return data + def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): - ''' + """ (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n calculates and returns \n - total_taxable_amount, total_taxable_adjustment_amount, total_tax''' - from_date = filters.get('from_date') - to_date = filters.get('to_date') + total_taxable_amount, total_taxable_adjustment_amount, total_tax""" + from_date = filters.get("from_date") + to_date = filters.get("to_date") # Initiate variables total_taxable_amount = 0 total_taxable_adjustment_amount = 0 total_tax = 0 # Fetch All Invoices - invoices = frappe.get_all(doctype, - filters ={ - 'docstatus': 1, - 'posting_date': ['between', [from_date, to_date]] - }, fields =['name', 'is_return']) + invoices = frappe.get_all( + doctype, + filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]}, + fields=["name", "is_return"], + ) for invoice in invoices: - invoice_items = frappe.get_all(f'{doctype} Item', - filters ={ - 'docstatus': 1, - 'parent': invoice.name, - 'item_tax_template': vat_setting.item_tax_template - }, fields =['item_code', 'net_amount']) + invoice_items = frappe.get_all( + f"{doctype} Item", + filters={ + "docstatus": 1, + "parent": invoice.name, + "item_tax_template": vat_setting.item_tax_template, + }, + fields=["item_code", "net_amount"], + ) for item in invoice_items: # Summing up total taxable amount @@ -158,24 +194,31 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): return total_taxable_amount, total_taxable_adjustment_amount, total_tax - def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency): """Returns data with appended value.""" - data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount, - "currency": company_currency}) + data.append( + { + "title": _(title), + "amount": amount, + "adjustment_amount": adjustment_amount, + "vat_amount": vat_amount, + "currency": company_currency, + } + ) + def get_tax_amount(item_code, account_head, doctype, parent): - if doctype == 'Sales Invoice': - tax_doctype = 'Sales Taxes and Charges' + if doctype == "Sales Invoice": + tax_doctype = "Sales Taxes and Charges" - elif doctype == 'Purchase Invoice': - tax_doctype = 'Purchase Taxes and Charges' + elif doctype == "Purchase Invoice": + tax_doctype = "Purchase Taxes and Charges" - item_wise_tax_detail = frappe.get_value(tax_doctype, { - 'docstatus': 1, - 'parent': parent, - 'account_head': account_head - }, 'item_wise_tax_detail') + item_wise_tax_detail = frappe.get_value( + tax_doctype, + {"docstatus": 1, "parent": parent, "account_head": account_head}, + "item_wise_tax_detail", + ) tax_amount = 0 if item_wise_tax_detail and len(item_wise_tax_detail) > 0: diff --git a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py index def43798289..17a62d5e5da 100644 --- a/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py +++ b/erpnext/regional/report/professional_tax_deductions/professional_tax_deductions.py @@ -16,6 +16,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -23,53 +24,54 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 + "width": 160, }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 140 - } + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 140}, ] return columns + def get_data(filters): data = [] - component_type_dict = frappe._dict(frappe.db.sql(""" select name, component_type from `tabSalary Component` - where component_type = 'Professional Tax' """)) + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type = 'Professional Tax' """ + ) + ) if not len(component_type_dict): return [] conditions = get_conditions(filters) - entry = frappe.db.sql(""" select sal.employee, sal.employee_name, ded.salary_component, ded.amount + entry = frappe.db.sql( + """ select sal.employee, sal.employee_name, ded.salary_component, ded.amount from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions , ", ".join(['%s']*len(component_type_dict))), tuple(component_type_dict.keys()), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) for d in entry: - employee = { - "employee": d.employee, - "employee_name": d.employee_name, - "amount": d.amount - } + employee = {"employee": d.employee, "employee_name": d.employee_name, "amount": d.amount} data.append(employee) diff --git a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py index 190f408fe0e..ab4b6e73b83 100644 --- a/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py +++ b/erpnext/regional/report/provident_fund_deductions/provident_fund_deductions.py @@ -13,6 +13,7 @@ def execute(filters=None): return columns, data + def get_columns(filters): columns = [ { @@ -20,57 +21,38 @@ def get_columns(filters): "options": "Employee", "fieldname": "employee", "fieldtype": "Link", - "width": 200 + "width": 200, }, { "label": _("Employee Name"), "options": "Employee", "fieldname": "employee_name", "fieldtype": "Link", - "width": 160 - }, - { - "label": _("PF Account"), - "fieldname": "pf_account", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("PF Amount"), - "fieldname": "pf_amount", - "fieldtype": "Currency", - "width": 140 + "width": 160, }, + {"label": _("PF Account"), "fieldname": "pf_account", "fieldtype": "Data", "width": 140}, + {"label": _("PF Amount"), "fieldname": "pf_amount", "fieldtype": "Currency", "width": 140}, { "label": _("Additional PF"), "fieldname": "additional_pf", "fieldtype": "Currency", - "width": 140 + "width": 140, }, - { - "label": _("PF Loan"), - "fieldname": "pf_loan", - "fieldtype": "Currency", - "width": 140 - }, - { - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Currency", - "width": 140 - } + {"label": _("PF Loan"), "fieldname": "pf_loan", "fieldtype": "Currency", "width": 140}, + {"label": _("Total"), "fieldname": "total", "fieldtype": "Currency", "width": 140}, ] return columns + def get_conditions(filters): conditions = [""] if filters.get("department"): - conditions.append("sal.department = '%s' " % (filters["department"]) ) + conditions.append("sal.department = '%s' " % (filters["department"])) if filters.get("branch"): - conditions.append("sal.branch = '%s' " % (filters["branch"]) ) + conditions.append("sal.branch = '%s' " % (filters["branch"])) if filters.get("company"): conditions.append("sal.company = '%s' " % (filters["company"])) @@ -86,10 +68,13 @@ def get_conditions(filters): return " and ".join(conditions) -def prepare_data(entry,component_type_dict): + +def prepare_data(entry, component_type_dict): data_list = {} - employee_account_dict = frappe._dict(frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""")) + employee_account_dict = frappe._dict( + frappe.db.sql(""" select name, provident_fund_account from `tabEmployee`""") + ) for d in entry: @@ -98,40 +83,57 @@ def prepare_data(entry,component_type_dict): if data_list.get(d.name): data_list[d.name][component_type] = d.amount else: - data_list.setdefault(d.name,{ - "employee": d.employee, - "employee_name": d.employee_name, - "pf_account": employee_account_dict.get(d.employee), - component_type: d.amount - }) + data_list.setdefault( + d.name, + { + "employee": d.employee, + "employee_name": d.employee_name, + "pf_account": employee_account_dict.get(d.employee), + component_type: d.amount, + }, + ) return data_list + def get_data(filters): data = [] conditions = get_conditions(filters) - salary_slips = frappe.db.sql(""" select sal.name from `tabSalary Slip` sal + salary_slips = frappe.db.sql( + """ select sal.name from `tabSalary Slip` sal where docstatus = 1 %s - """ % (conditions), as_dict=1) + """ + % (conditions), + as_dict=1, + ) - component_type_dict = frappe._dict(frappe.db.sql(""" select name, component_type from `tabSalary Component` - where component_type in ('Provident Fund', 'Additional Provident Fund', 'Provident Fund Loan')""")) + component_type_dict = frappe._dict( + frappe.db.sql( + """ select name, component_type from `tabSalary Component` + where component_type in ('Provident Fund', 'Additional Provident Fund', 'Provident Fund Loan')""" + ) + ) if not len(component_type_dict): return [] - entry = frappe.db.sql(""" select sal.name, sal.employee, sal.employee_name, ded.salary_component, ded.amount + entry = frappe.db.sql( + """ select sal.name, sal.employee, sal.employee_name, ded.salary_component, ded.amount from `tabSalary Slip` sal, `tabSalary Detail` ded where sal.name = ded.parent and ded.parentfield = 'deductions' and ded.parenttype = 'Salary Slip' and sal.docstatus = 1 %s and ded.salary_component in (%s) - """ % (conditions, ", ".join(['%s']*len(component_type_dict))), tuple(component_type_dict.keys()), as_dict=1) + """ + % (conditions, ", ".join(["%s"] * len(component_type_dict))), + tuple(component_type_dict.keys()), + as_dict=1, + ) - data_list = prepare_data(entry,component_type_dict) + data_list = prepare_data(entry, component_type_dict) for d in salary_slips: total = 0 @@ -139,7 +141,7 @@ def get_data(filters): employee = { "employee": data_list.get(d.name).get("employee"), "employee_name": data_list.get(d.name).get("employee_name"), - "pf_account": data_list.get(d.name).get("pf_account") + "pf_account": data_list.get(d.name).get("pf_account"), } if data_list.get(d.name).get("Provident Fund"): @@ -160,9 +162,12 @@ def get_data(filters): return data + @frappe.whitelist() def get_years(): - year_list = frappe.db.sql_list("""select distinct YEAR(end_date) from `tabSalary Slip` ORDER BY YEAR(end_date) DESC""") + year_list = frappe.db.sql_list( + """select distinct YEAR(end_date) from `tabSalary Slip` ORDER BY YEAR(end_date) DESC""" + ) if not year_list: year_list = [getdate().year] diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py index 62d694ba7ba..e021bc8789d 100644 --- a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py @@ -20,6 +20,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse_account test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] + class TestUaeVat201(TestCase): def setUp(self): frappe.set_user("Administrator") @@ -36,9 +37,9 @@ class TestUaeVat201(TestCase): create_warehouse("_Test UAE VAT Supplier Warehouse", company="_Test Company UAE VAT") - make_item("_Test UAE VAT Item", properties = {"is_zero_rated": 0, "is_exempt": 0}) - make_item("_Test UAE VAT Zero Rated Item", properties = {"is_zero_rated": 1, "is_exempt": 0}) - make_item("_Test UAE VAT Exempt Item", properties = {"is_zero_rated": 0, "is_exempt": 1}) + make_item("_Test UAE VAT Item", properties={"is_zero_rated": 0, "is_exempt": 0}) + make_item("_Test UAE VAT Zero Rated Item", properties={"is_zero_rated": 1, "is_exempt": 0}) + make_item("_Test UAE VAT Exempt Item", properties={"is_zero_rated": 0, "is_exempt": 1}) make_sales_invoices() @@ -54,27 +55,30 @@ class TestUaeVat201(TestCase): "raw_amount": amount, "raw_vat_amount": vat, } - self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],100) - self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5) - self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200) - self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10) - self.assertEqual(get_tourist_tax_return_total(filters),100) - self.assertEqual(get_tourist_tax_return_tax(filters),2) - self.assertEqual(get_zero_rated_total(filters),100) - self.assertEqual(get_exempt_total(filters),100) - self.assertEqual(get_standard_rated_expenses_total(filters),250) - self.assertEqual(get_standard_rated_expenses_tax(filters),1) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"], 100) + self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"], 5) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"], 200) + self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"], 10) + self.assertEqual(get_tourist_tax_return_total(filters), 100) + self.assertEqual(get_tourist_tax_return_tax(filters), 2) + self.assertEqual(get_zero_rated_total(filters), 100) + self.assertEqual(get_exempt_total(filters), 100) + self.assertEqual(get_standard_rated_expenses_total(filters), 250) + self.assertEqual(get_standard_rated_expenses_tax(filters), 1) + def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "AED", - "country": "United Arab Emirates", - "create_chart_of_accounts_based_on": "Standard Template", - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "AED", + "country": "United Arab Emirates", + "create_chart_of_accounts_based_on": "Standard Template", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -87,50 +91,53 @@ def make_company(company_name, abbr): company.save() return company + def set_vat_accounts(): if not frappe.db.exists("UAE VAT Settings", "_Test Company UAE VAT"): vat_accounts = frappe.get_all( "Account", fields=["name"], - filters = { - "company": "_Test Company UAE VAT", - "is_group": 0, - "account_type": "Tax" - } + filters={"company": "_Test Company UAE VAT", "is_group": 0, "account_type": "Tax"}, ) uae_vat_accounts = [] for account in vat_accounts: - uae_vat_accounts.append({ - "doctype": "UAE VAT Account", - "account": account.name - }) + uae_vat_accounts.append({"doctype": "UAE VAT Account", "account": account.name}) + + frappe.get_doc( + { + "company": "_Test Company UAE VAT", + "uae_vat_accounts": uae_vat_accounts, + "doctype": "UAE VAT Settings", + } + ).insert() - frappe.get_doc({ - "company": "_Test Company UAE VAT", - "uae_vat_accounts": uae_vat_accounts, - "doctype": "UAE VAT Settings", - }).insert() def make_customer(): if not frappe.db.exists("Customer", "_Test UAE Customer"): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test UAE Customer", - "customer_type": "Company", - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test UAE Customer", + "customer_type": "Company", + } + ) customer.insert() else: customer = frappe.get_doc("Customer", "_Test UAE Customer") + def make_supplier(): if not frappe.db.exists("Supplier", "_Test UAE Supplier"): - frappe.get_doc({ - "supplier_group": "Local", - "supplier_name": "_Test UAE Supplier", - "supplier_type": "Individual", - "doctype": "Supplier", - }).insert() + frappe.get_doc( + { + "supplier_group": "Local", + "supplier_name": "_Test UAE Supplier", + "supplier_type": "Individual", + "doctype": "Supplier", + } + ).insert() + def create_warehouse(warehouse_name, properties=None, company=None): if not company: @@ -150,17 +157,20 @@ def create_warehouse(warehouse_name, properties=None, company=None): else: return warehouse_id + def make_item(item_code, properties=None): if frappe.db.exists("Item", item_code): return frappe.get_doc("Item", item_code) - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) @@ -169,71 +179,77 @@ def make_item(item_code, properties=None): return item + def make_sales_invoices(): - def make_sales_invoices_wrapper(emirate, item, tax = True, tourist_tax= False): + def make_sales_invoices_wrapper(emirate, item, tax=True, tourist_tax=False): si = create_sales_invoice( company="_Test Company UAE VAT", - customer = '_Test UAE Customer', - currency = 'AED', - warehouse = 'Finished Goods - _TCUV', - debit_to = 'Debtors - _TCUV', - income_account = 'Sales - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - cost_center = 'Main - _TCUV', - item = item, - do_not_save=1 + customer="_Test UAE Customer", + currency="AED", + warehouse="Finished Goods - _TCUV", + debit_to="Debtors - _TCUV", + income_account="Sales - _TCUV", + expense_account="Cost of Goods Sold - _TCUV", + cost_center="Main - _TCUV", + item=item, + do_not_save=1, ) si.vat_emirate = emirate if tax: si.append( - "taxes", { + "taxes", + { "charge_type": "On Net Total", "account_head": "VAT 5% - _TCUV", "cost_center": "Main - _TCUV", "description": "VAT 5% @ 5.0", - "rate": 5.0 - } + "rate": 5.0, + }, ) if tourist_tax: si.tourist_tax_return = 2 si.submit() - #Define Item Names + # Define Item Names uae_item = "_Test UAE VAT Item" uae_exempt_item = "_Test UAE VAT Exempt Item" uae_zero_rated_item = "_Test UAE VAT Zero Rated Item" - #Sales Invoice with standard rated expense in Dubai - make_sales_invoices_wrapper('Dubai', uae_item) - #Sales Invoice with standard rated expense in Sharjah - make_sales_invoices_wrapper('Sharjah', uae_item) - #Sales Invoice with Tourist Tax Return - make_sales_invoices_wrapper('Dubai', uae_item, True, True) - #Sales Invoice with Exempt Item - make_sales_invoices_wrapper('Sharjah', uae_exempt_item, False) - #Sales Invoice with Zero Rated Item - make_sales_invoices_wrapper('Sharjah', uae_zero_rated_item, False) + # Sales Invoice with standard rated expense in Dubai + make_sales_invoices_wrapper("Dubai", uae_item) + # Sales Invoice with standard rated expense in Sharjah + make_sales_invoices_wrapper("Sharjah", uae_item) + # Sales Invoice with Tourist Tax Return + make_sales_invoices_wrapper("Dubai", uae_item, True, True) + # Sales Invoice with Exempt Item + make_sales_invoices_wrapper("Sharjah", uae_exempt_item, False) + # Sales Invoice with Zero Rated Item + make_sales_invoices_wrapper("Sharjah", uae_zero_rated_item, False) + def create_purchase_invoices(): pi = make_purchase_invoice( company="_Test Company UAE VAT", - supplier = '_Test UAE Supplier', - supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV', - currency = 'AED', - cost_center = 'Main - _TCUV', - expense_account = 'Cost of Goods Sold - _TCUV', - item = "_Test UAE VAT Item", + supplier="_Test UAE Supplier", + supplier_warehouse="_Test UAE VAT Supplier Warehouse - _TCUV", + warehouse="_Test UAE VAT Supplier Warehouse - _TCUV", + currency="AED", + cost_center="Main - _TCUV", + expense_account="Cost of Goods Sold - _TCUV", + item="_Test UAE VAT Item", do_not_save=1, - uom = "Nos" + uom="Nos", + ) + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TCUV", + "cost_center": "Main - _TCUV", + "description": "VAT 5% @ 5.0", + "rate": 5.0, + }, ) - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT 5% - _TCUV", - "cost_center": "Main - _TCUV", - "description": "VAT 5% @ 5.0", - "rate": 5.0 - }) pi.recoverable_standard_rated_expenses = 1 diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index f8379aa17ab..59ef58bfde3 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -11,21 +11,12 @@ def execute(filters=None): data, emirates, amounts_by_emirate = get_data(filters) return columns, data + def get_columns(): """Creates a list of dictionaries that are used to generate column headers of the data table.""" return [ - { - "fieldname": "no", - "label": _("No"), - "fieldtype": "Data", - "width": 50 - }, - { - "fieldname": "legend", - "label": _("Legend"), - "fieldtype": "Data", - "width": 300 - }, + {"fieldname": "no", "label": _("No"), "fieldtype": "Data", "width": 50}, + {"fieldname": "legend", "label": _("Legend"), "fieldtype": "Data", "width": 300}, { "fieldname": "amount", "label": _("Amount (AED)"), @@ -37,41 +28,53 @@ def get_columns(): "label": _("VAT Amount (AED)"), "fieldtype": "Currency", "width": 150, - } + }, ] -def get_data(filters = None): + +def get_data(filters=None): """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data.""" data = [] emirates, amounts_by_emirate = append_vat_on_sales(data, filters) append_vat_on_expenses(data, filters) return data, emirates, amounts_by_emirate + def append_vat_on_sales(data, filters): """Appends Sales and All Other Outputs.""" - append_data(data, '', _('VAT on Sales and All Other Outputs'), '', '') + append_data(data, "", _("VAT on Sales and All Other Outputs"), "", "") emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) - append_data(data, '2', - _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'), - frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'), - frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency')) + append_data( + data, + "2", + _("Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme"), + frappe.format((-1) * get_tourist_tax_return_total(filters), "Currency"), + frappe.format((-1) * get_tourist_tax_return_tax(filters), "Currency"), + ) - append_data(data, '3', _('Supplies subject to the reverse charge provision'), - frappe.format(get_reverse_charge_total(filters), 'Currency'), - frappe.format(get_reverse_charge_tax(filters), 'Currency')) + append_data( + data, + "3", + _("Supplies subject to the reverse charge provision"), + frappe.format(get_reverse_charge_total(filters), "Currency"), + frappe.format(get_reverse_charge_tax(filters), "Currency"), + ) - append_data(data, '4', _('Zero Rated'), - frappe.format(get_zero_rated_total(filters), 'Currency'), "-") + append_data( + data, "4", _("Zero Rated"), frappe.format(get_zero_rated_total(filters), "Currency"), "-" + ) - append_data(data, '5', _('Exempt Supplies'), - frappe.format(get_exempt_total(filters), 'Currency'),"-") + append_data( + data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-" + ) - append_data(data, '', '', '', '') + append_data(data, "", "", "", "") return emirates, amounts_by_emirate + def standard_rated_expenses_emiratewise(data, filters): """Append emiratewise standard rated expenses and vat.""" total_emiratewise = get_total_emiratewise(filters) @@ -82,44 +85,61 @@ def standard_rated_expenses_emiratewise(data, filters): "legend": emirate, "raw_amount": amount, "raw_vat_amount": vat, - "amount": frappe.format(amount, 'Currency'), - "vat_amount": frappe.format(vat, 'Currency'), + "amount": frappe.format(amount, "Currency"), + "vat_amount": frappe.format(vat, "Currency"), } amounts_by_emirate = append_emiratewise_expenses(data, emirates, amounts_by_emirate) return emirates, amounts_by_emirate + def append_emiratewise_expenses(data, emirates, amounts_by_emirate): """Append emiratewise standard rated expenses and vat.""" for no, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: - amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(no)) - amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate) + amounts_by_emirate[emirate]["no"] = _("1{0}").format(chr(no)) + amounts_by_emirate[emirate]["legend"] = _("Standard rated supplies in {0}").format(emirate) data.append(amounts_by_emirate[emirate]) else: - append_data(data, _('1{0}').format(chr(no)), - _('Standard rated supplies in {0}').format(emirate), - frappe.format(0, 'Currency'), frappe.format(0, 'Currency')) + append_data( + data, + _("1{0}").format(chr(no)), + _("Standard rated supplies in {0}").format(emirate), + frappe.format(0, "Currency"), + frappe.format(0, "Currency"), + ) return amounts_by_emirate + def append_vat_on_expenses(data, filters): """Appends Expenses and All Other Inputs.""" - append_data(data, '', _('VAT on Expenses and All Other Inputs'), '', '') - append_data(data, '9', _('Standard Rated Expenses'), - frappe.format(get_standard_rated_expenses_total(filters), 'Currency'), - frappe.format(get_standard_rated_expenses_tax(filters), 'Currency')) - append_data(data, '10', _('Supplies subject to the reverse charge provision'), - frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'), - frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency')) + append_data(data, "", _("VAT on Expenses and All Other Inputs"), "", "") + append_data( + data, + "9", + _("Standard Rated Expenses"), + frappe.format(get_standard_rated_expenses_total(filters), "Currency"), + frappe.format(get_standard_rated_expenses_tax(filters), "Currency"), + ) + append_data( + data, + "10", + _("Supplies subject to the reverse charge provision"), + frappe.format(get_reverse_charge_recoverable_total(filters), "Currency"), + frappe.format(get_reverse_charge_recoverable_tax(filters), "Currency"), + ) + def append_data(data, no, legend, amount, vat_amount): """Returns data with appended value.""" - data.append({"no": no, "legend":legend, "amount": amount, "vat_amount": vat_amount}) + data.append({"no": no, "legend": legend, "amount": amount, "vat_amount": vat_amount}) + def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return frappe.db.sql( + """ select s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount) from @@ -131,52 +151,54 @@ def get_total_emiratewise(filters): {where_conditions} group by s.vat_emirate; - """.format(where_conditions=conditions), filters) + """.format( + where_conditions=conditions + ), + filters, + ) except (IndexError, TypeError): return 0 + def get_emirates(): """Returns a List of emirates in the order that they are to be displayed.""" - return [ - 'Abu Dhabi', - 'Dubai', - 'Sharjah', - 'Ajman', - 'Umm Al Quwain', - 'Ras Al Khaimah', - 'Fujairah' - ] + return ["Abu Dhabi", "Dubai", "Sharjah", "Ajman", "Umm Al Quwain", "Ras Al Khaimah", "Fujairah"] + def get_filters(filters): """The conditions to be used to filter data to calculate the total sale.""" query_filters = [] if filters.get("company"): - query_filters.append(["company", '=', filters['company']]) + query_filters.append(["company", "=", filters["company"]]) if filters.get("from_date"): - query_filters.append(["posting_date", '>=', filters['from_date']]) + query_filters.append(["posting_date", ">=", filters["from_date"]]) if filters.get("from_date"): - query_filters.append(["posting_date", '<=', filters['to_date']]) + query_filters.append(["posting_date", "<=", filters["to_date"]]) return query_filters + def get_reverse_charge_total(filters): """Returns the sum of the total of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters.append(['reverse_charge', '=', 'Y']) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(debit) from `tabPurchase Invoice` p inner join `tabGL Entry` gl on @@ -187,28 +209,38 @@ def get_reverse_charge_tax(filters): and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) + def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters.append(['reverse_charge', '=', 'Y']) - query_filters.append(['recoverable_reverse_charge', '>', '0']) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["recoverable_reverse_charge", ">", "0"]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" conditions = get_conditions_join(filters) - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(debit * p.recoverable_reverse_charge / 100) from @@ -222,83 +254,107 @@ def get_reverse_charge_recoverable_tax(filters): and gl.docstatus = 1 and account in (select account from `tabUAE VAT Account` where parent=%(company)s) {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) + def get_conditions_join(filters): """The conditions to be used to filter data to calculate the total vat.""" conditions = "" - for opts in (("company", " and p.company=%(company)s"), + for opts in ( + ("company", " and p.company=%(company)s"), ("from_date", " and p.posting_date>=%(from_date)s"), - ("to_date", " and p.posting_date<=%(to_date)s")): + ("to_date", " and p.posting_date<=%(to_date)s"), + ): if filters.get(opts[0]): conditions += opts[1] return conditions + def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" query_filters = get_filters(filters) - query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" query_filters = get_filters(filters) - query_filters.append(['recoverable_standard_rated_expenses', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Purchase Invoice', - filters = query_filters, - fields = ['sum(recoverable_standard_rated_expenses)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Purchase Invoice", + filters=query_filters, + fields=["sum(recoverable_standard_rated_expenses)"], + as_list=True, + limit=1, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters.append(['tourist_tax_return', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Sales Invoice', - filters = query_filters, - fields = ['sum(total)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Sales Invoice", filters=query_filters, fields=["sum(total)"], as_list=True, limit=1 + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" query_filters = get_filters(filters) - query_filters.append(['tourist_tax_return', '>', 0]) - query_filters.append(['docstatus', '=', 1]) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) try: - return frappe.db.get_all('Sales Invoice', - filters = query_filters, - fields = ['sum(tourist_tax_return)'], - as_list=True, - limit = 1 - )[0][0] or 0 + return ( + frappe.db.get_all( + "Sales Invoice", + filters=query_filters, + fields=["sum(tourist_tax_return)"], + as_list=True, + limit=1, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_zero_rated_total(filters): """Returns the sum of each Sales Invoice Item Amount which is zero rated.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(i.base_amount) as total from @@ -308,15 +364,24 @@ def get_zero_rated_total(filters): where s.docstatus = 1 and i.is_zero_rated = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + def get_exempt_total(filters): """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt.""" conditions = get_conditions(filters) try: - return frappe.db.sql(""" + return ( + frappe.db.sql( + """ select sum(i.base_amount) as total from @@ -326,15 +391,25 @@ def get_exempt_total(filters): where s.docstatus = 1 and i.is_exempt = 1 {where_conditions} ; - """.format(where_conditions=conditions), filters)[0][0] or 0 + """.format( + where_conditions=conditions + ), + filters, + )[0][0] + or 0 + ) except (IndexError, TypeError): return 0 + + def get_conditions(filters): """The conditions to be used to filter data to calculate the total sale.""" conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): + ("to_date", " and posting_date<=%(to_date)s"), + ): if filters.get(opts[0]): conditions += opts[1] return conditions diff --git a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py index f22abae1ff8..a898a251043 100644 --- a/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/test_vat_audit_report.py @@ -18,14 +18,22 @@ class TestVATAuditReport(TestCase): frappe.set_user("Administrator") make_company("_Test Company SA VAT", "_TCSV") - create_account(account_name="VAT - 0%", account_type="Tax", - parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") - create_account(account_name="VAT - 15%", account_type="Tax", - parent_account="Duties and Taxes - _TCSV", company="_Test Company SA VAT") + create_account( + account_name="VAT - 0%", + account_type="Tax", + parent_account="Duties and Taxes - _TCSV", + company="_Test Company SA VAT", + ) + create_account( + account_name="VAT - 15%", + account_type="Tax", + parent_account="Duties and Taxes - _TCSV", + company="_Test Company SA VAT", + ) set_sa_vat_accounts() make_item("_Test SA VAT Item") - make_item("_Test SA VAT Zero Rated Item", properties = {"is_zero_rated": 1}) + make_item("_Test SA VAT Zero Rated Item", properties={"is_zero_rated": 1}) make_customer() make_supplier() @@ -38,34 +46,33 @@ class TestVATAuditReport(TestCase): frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company SA VAT'") def test_vat_audit_report(self): - filters = { - "company": "_Test Company SA VAT", - "from_date": today(), - "to_date": today() - } + filters = {"company": "_Test Company SA VAT", "from_date": today(), "to_date": today()} columns, data = execute(filters) total_tax_amount = 0 total_row_tax = 0 for row in data: keys = row.keys() # skips total row tax_amount in if.. and skips section header in elif.. - if 'voucher_no' in keys: - total_tax_amount = total_tax_amount + row['tax_amount'] - elif 'tax_amount' in keys: - total_row_tax = total_row_tax + row['tax_amount'] + if "voucher_no" in keys: + total_tax_amount = total_tax_amount + row["tax_amount"] + elif "tax_amount" in keys: + total_row_tax = total_row_tax + row["tax_amount"] self.assertEqual(total_tax_amount, total_row_tax) + def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): - company = frappe.get_doc({ - "doctype": "Company", - "company_name": company_name, - "abbr": abbr, - "default_currency": "ZAR", - "country": "South Africa", - "create_chart_of_accounts_based_on": "Standard Template" - }) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "ZAR", + "country": "South Africa", + "create_chart_of_accounts_based_on": "Standard Template", + } + ) company.insert() else: company = frappe.get_doc("Company", company_name) @@ -79,86 +86,95 @@ def make_company(company_name, abbr): return company + def set_sa_vat_accounts(): if not frappe.db.exists("South Africa VAT Settings", "_Test Company SA VAT"): vat_accounts = frappe.get_all( "Account", fields=["name"], - filters = { - "company": "_Test Company SA VAT", - "is_group": 0, - "account_type": "Tax" - } + filters={"company": "_Test Company SA VAT", "is_group": 0, "account_type": "Tax"}, ) sa_vat_accounts = [] for account in vat_accounts: - sa_vat_accounts.append({ - "doctype": "South Africa VAT Account", - "account": account.name - }) + sa_vat_accounts.append({"doctype": "South Africa VAT Account", "account": account.name}) + + frappe.get_doc( + { + "company": "_Test Company SA VAT", + "vat_accounts": sa_vat_accounts, + "doctype": "South Africa VAT Settings", + } + ).insert() - frappe.get_doc({ - "company": "_Test Company SA VAT", - "vat_accounts": sa_vat_accounts, - "doctype": "South Africa VAT Settings", - }).insert() def make_customer(): if not frappe.db.exists("Customer", "_Test SA Customer"): - frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test SA Customer", - "customer_type": "Company", - }).insert() + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test SA Customer", + "customer_type": "Company", + } + ).insert() + def make_supplier(): if not frappe.db.exists("Supplier", "_Test SA Supplier"): - frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": "_Test SA Supplier", - "supplier_type": "Company", - "supplier_group":"All Supplier Groups" - }).insert() + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": "_Test SA Supplier", + "supplier_type": "Company", + "supplier_group": "All Supplier Groups", + } + ).insert() + def make_item(item_code, properties=None): if not frappe.db.exists("Item", item_code): - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) item.insert() + def make_sales_invoices(): def make_sales_invoices_wrapper(item, rate, tax_account, tax_rate, tax=True): si = create_sales_invoice( company="_Test Company SA VAT", - customer = "_Test SA Customer", - currency = "ZAR", + customer="_Test SA Customer", + currency="ZAR", item=item, rate=rate, - warehouse = "Finished Goods - _TCSV", - debit_to = "Debtors - _TCSV", - income_account = "Sales - _TCSV", - expense_account = "Cost of Goods Sold - _TCSV", - cost_center = "Main - _TCSV", - do_not_save=1 + warehouse="Finished Goods - _TCSV", + debit_to="Debtors - _TCSV", + income_account="Sales - _TCSV", + expense_account="Cost of Goods Sold - _TCSV", + cost_center="Main - _TCSV", + do_not_save=1, ) if tax: - si.append("taxes", { + si.append( + "taxes", + { "charge_type": "On Net Total", "account_head": tax_account, "cost_center": "Main - _TCSV", "description": "VAT 15% @ 15.0", - "rate": tax_rate - }) + "rate": tax_rate, + }, + ) si.submit() @@ -168,27 +184,31 @@ def make_sales_invoices(): make_sales_invoices_wrapper(test_item, 100.0, "VAT - 15% - _TCSV", 15.0) make_sales_invoices_wrapper(test_zero_rated_item, 100.0, "VAT - 0% - _TCSV", 0.0) + def create_purchase_invoices(): pi = make_purchase_invoice( - company = "_Test Company SA VAT", - supplier = "_Test SA Supplier", - supplier_warehouse = "Finished Goods - _TCSV", - warehouse = "Finished Goods - _TCSV", - currency = "ZAR", - cost_center = "Main - _TCSV", - expense_account = "Cost of Goods Sold - _TCSV", - item = "_Test SA VAT Item", - qty = 1, - rate = 100, - uom = "Nos", - do_not_save = 1 + company="_Test Company SA VAT", + supplier="_Test SA Supplier", + supplier_warehouse="Finished Goods - _TCSV", + warehouse="Finished Goods - _TCSV", + currency="ZAR", + cost_center="Main - _TCSV", + expense_account="Cost of Goods Sold - _TCSV", + item="_Test SA VAT Item", + qty=1, + rate=100, + uom="Nos", + do_not_save=1, + ) + pi.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "VAT - 15% - _TCSV", + "cost_center": "Main - _TCSV", + "description": "VAT 15% @ 15.0", + "rate": 15.0, + }, ) - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "VAT - 15% - _TCSV", - "cost_center": "Main - _TCSV", - "description": "VAT 15% @ 15.0", - "rate": 15.0 - }) pi.submit() diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 17e50648b3b..6e5982465cf 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -12,8 +12,8 @@ from frappe.utils import formatdate, get_link_to_form def execute(filters=None): return VATAuditReport(filters).run() -class VATAuditReport(object): +class VATAuditReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.columns = [] @@ -27,8 +27,11 @@ class VATAuditReport(object): self.select_columns = """ name as voucher_no, posting_date, remarks""" - columns = ", supplier as party, credit_to as account" if doctype=="Purchase Invoice" \ + columns = ( + ", supplier as party, credit_to as account" + if doctype == "Purchase Invoice" else ", customer as party, debit_to as account" + ) self.select_columns += columns self.get_invoice_data(doctype) @@ -41,17 +44,21 @@ class VATAuditReport(object): return self.columns, self.data def get_sa_vat_accounts(self): - self.sa_vat_accounts = frappe.get_all("South Africa VAT Account", - filters = {"parent": self.filters.company}, pluck="account") + self.sa_vat_accounts = frappe.get_all( + "South Africa VAT Account", filters={"parent": self.filters.company}, pluck="account" + ) if not self.sa_vat_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: - link_to_settings = get_link_to_form("South Africa VAT Settings", "", label="South Africa VAT Settings") + link_to_settings = get_link_to_form( + "South Africa VAT Settings", "", label="South Africa VAT Settings" + ) frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings)) def get_invoice_data(self, doctype): conditions = self.get_conditions() self.invoices = frappe._dict() - invoice_data = frappe.db.sql(""" + invoice_data = frappe.db.sql( + """ SELECT {select_columns} FROM @@ -61,8 +68,12 @@ class VATAuditReport(object): and is_opening = "No" ORDER BY posting_date DESC - """.format(select_columns=self.select_columns, doctype=doctype, - where_conditions=conditions), self.filters, as_dict=1) + """.format( + select_columns=self.select_columns, doctype=doctype, where_conditions=conditions + ), + self.filters, + as_dict=1, + ) for d in invoice_data: self.invoices.setdefault(d.voucher_no, d) @@ -70,28 +81,34 @@ class VATAuditReport(object): def get_invoice_items(self, doctype): self.invoice_items = frappe._dict() - items = frappe.db.sql(""" + items = frappe.db.sql( + """ SELECT item_code, parent, base_net_amount, is_zero_rated FROM `tab%s Item` WHERE parent in (%s) - """ % (doctype, ", ".join(["%s"]*len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (doctype, ", ".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in items: if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, { - 'net_amount': 0.0}) - self.invoice_items[d.parent][d.item_code]['net_amount'] += d.get('base_net_amount', 0) - self.invoice_items[d.parent][d.item_code]['is_zero_rated'] = d.is_zero_rated + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0}) + self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0) + self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated def get_items_based_on_tax_rate(self, doctype): self.items_based_on_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict() - self.tax_doctype = "Purchase Taxes and Charges" if doctype=="Purchase Invoice" \ - else "Sales Taxes and Charges" + self.tax_doctype = ( + "Purchase Taxes and Charges" if doctype == "Purchase Invoice" else "Sales Taxes and Charges" + ) - self.tax_details = frappe.db.sql(""" + self.tax_details = frappe.db.sql( + """ SELECT parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount FROM @@ -101,8 +118,10 @@ class VATAuditReport(object): and parent in (%s) ORDER BY account_head - """ % (self.tax_doctype, "%s", ", ".join(["%s"]*len(self.invoices.keys()))), - tuple([doctype] + list(self.invoices.keys()))) + """ + % (self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))), + tuple([doctype] + list(self.invoices.keys())), + ) for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: if item_wise_tax_detail: @@ -113,14 +132,15 @@ class VATAuditReport(object): continue for item_code, taxes in item_wise_tax_detail.items(): is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated") - #to skip items with non-zero tax rate in multiple rows + # to skip items with non-zero tax rate in multiple rows if taxes[0] == 0 and not is_zero_rated: continue tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes) if tax_rate is not None: - rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}) \ - .setdefault(tax_rate, []) + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( + tax_rate, [] + ) if item_code not in rate_based_dict: rate_based_dict.append(item_code) except ValueError: @@ -131,13 +151,12 @@ class VATAuditReport(object): tax_rate = taxes[0] tax_amount = taxes[1] gross_amount = net_amount + tax_amount - item_amount_map = self.item_tax_rate.setdefault(parent, {}) \ - .setdefault(item_code, []) + item_amount_map = self.item_tax_rate.setdefault(parent, {}).setdefault(item_code, []) amount_dict = { "tax_rate": tax_rate, "gross_amount": gross_amount, "tax_amount": tax_amount, - "net_amount": net_amount + "net_amount": net_amount, } item_amount_map.append(amount_dict) @@ -145,9 +164,11 @@ class VATAuditReport(object): def get_conditions(self): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("from_date", " and posting_date>=%(from_date)s"), - ("to_date", " and posting_date<=%(to_date)s")): + ("to_date", " and posting_date<=%(to_date)s"), + ): if self.filters.get(opts[0]): conditions += opts[1] @@ -174,13 +195,13 @@ class VATAuditReport(object): "gross_amount": total_gross, "tax_amount": total_tax, "net_amount": total_net, - "bold":1 + "bold": 1, } self.data.append(total) self.data.append({}) def get_consolidated_data(self, doctype): - consolidated_data_map={} + consolidated_data_map = {} for inv, inv_data in self.invoices.items(): if self.items_based_on_tax_rate.get(inv): for rate, items in self.items_based_on_tax_rate.get(inv).items(): @@ -195,78 +216,53 @@ class VATAuditReport(object): row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier" row["party"] = inv_data.get("party") row["remarks"] = inv_data.get("remarks") - row["gross_amount"]= item_details[0].get("gross_amount") - row["tax_amount"]= item_details[0].get("tax_amount") - row["net_amount"]= item_details[0].get("net_amount") + row["gross_amount"] = item_details[0].get("gross_amount") + row["tax_amount"] = item_details[0].get("tax_amount") + row["net_amount"] = item_details[0].get("net_amount") consolidated_data_map[rate]["data"].append(row) return consolidated_data_map def get_columns(self): self.columns = [ - { - "fieldname": "posting_date", - "label": "Posting Date", - "fieldtype": "Data", - "width": 200 - }, + {"fieldname": "posting_date", "label": "Posting Date", "fieldtype": "Data", "width": 200}, { "fieldname": "account", "label": "Account", "fieldtype": "Link", "options": "Account", - "width": 150 + "width": 150, }, { "fieldname": "voucher_type", "label": "Voucher Type", "fieldtype": "Data", "width": 140, - "hidden": 1 + "hidden": 1, }, { "fieldname": "voucher_no", "label": "Reference", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": 150 + "width": 150, }, { "fieldname": "party_type", "label": "Party Type", "fieldtype": "Data", "width": 140, - "hidden": 1 + "hidden": 1, }, { "fieldname": "party", "label": "Party", "fieldtype": "Dynamic Link", "options": "party_type", - "width": 150 - }, - { - "fieldname": "remarks", - "label": "Details", - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "net_amount", - "label": "Net Amount", - "fieldtype": "Currency", - "width": 130 - }, - { - "fieldname": "tax_amount", - "label": "Tax Amount", - "fieldtype": "Currency", - "width": 130 - }, - { - "fieldname": "gross_amount", - "label": "Gross Amount", - "fieldtype": "Currency", - "width": 130 + "width": 150, }, + {"fieldname": "remarks", "label": "Details", "fieldtype": "Data", "width": 150}, + {"fieldname": "net_amount", "label": "Net Amount", "fieldtype": "Currency", "width": 130}, + {"fieldname": "tax_amount", "label": "Tax Amount", "fieldtype": "Currency", "width": 130}, + {"fieldname": "gross_amount", "label": "Gross Amount", "fieldtype": "Currency", "width": 130}, ] diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py index 2e31c03d5c6..0b9f753dcc3 100644 --- a/erpnext/regional/saudi_arabia/setup.py +++ b/erpnext/regional/saudi_arabia/setup.py @@ -4,15 +4,19 @@ 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 -from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting +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 + def setup(company=None, patch=True): uae_custom_fields() add_print_formats() 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) @@ -20,19 +24,27 @@ def add_print_formats(): 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'): + 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) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): - add_permission('KSA VAT Setting', role, 0) - update_permission_property('KSA VAT Setting', role, 0, 'write', 1) - update_permission_property('KSA VAT Setting', role, 0, 'create', 1) + add_permission("KSA VAT Setting", "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): + add_permission("KSA VAT Setting", role, 0) + update_permission_property("KSA VAT Setting", role, 0, "write", 1) + update_permission_property("KSA VAT Setting", role, 0, "create", 1) """Enable KSA VAT Report""" - frappe.db.set_value('Report', 'KSA VAT', 'disabled', 0) + frappe.db.set_value("Report", "KSA VAT", "disabled", 0) + def make_custom_fields(): """Create Custom fields @@ -41,41 +53,46 @@ def make_custom_fields(): - Address in Arabic """ custom_fields = { - 'Sales Invoice': [ + "Sales Invoice": [ dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, ) ], - 'POS Invoice': [ + "POS Invoice": [ dict( - fieldname='ksa_einv_qr', - label='KSA E-Invoicing QR', - fieldtype='Attach Image', - read_only=1, no_copy=1, hidden=1 + fieldname="ksa_einv_qr", + label="KSA E-Invoicing QR", + fieldtype="Attach Image", + read_only=1, + no_copy=1, + hidden=1, ) ], - 'Address': [ + "Address": [ dict( - fieldname='address_in_arabic', - label='Address in Arabic', - fieldtype='Data', - insert_after='address_line2' + fieldname="address_in_arabic", + label="Address in Arabic", + fieldtype="Data", + insert_after="address_line2", ) ], - 'Company': [ + "Company": [ dict( - fieldname='company_name_in_arabic', - label='Company Name In Arabic', - fieldtype='Data', - insert_after='company_name' + fieldname="company_name_in_arabic", + label="Company Name In Arabic", + fieldtype="Data", + insert_after="company_name", ) - ] + ], } create_custom_fields(custom_fields, update=True) + def update_regional_tax_settings(country, company): create_ksa_vat_setting(company) diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py index 60953ca6d83..4557730e4da 100644 --- a/erpnext/regional/saudi_arabia/utils.py +++ b/erpnext/regional/saudi_arabia/utils.py @@ -16,21 +16,25 @@ from erpnext import get_region def create_qr_code(doc, method=None): region = get_region(doc.company) - if region not in ['Saudi Arabia']: + if region not in ["Saudi Arabia"]: 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 - ) - ] - }) + 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("ksa_einv_qr") @@ -40,129 +44,129 @@ def create_qr_code(doc, method=None): meta = frappe.get_meta(doc.doctype) if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]: - ''' TLV conversion for + """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)) + 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])) + 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') + 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)) + 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])) + 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 = 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])) + 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])) + value = invoice_amount.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # VAT Amount vat_amount = str(get_vat_amount(doc)) tag = bytes([5]).hex() length = bytes([len(vat_amount)]).hex() - value = vat_amount.encode('utf-8').hex() - tlv_array.append(''.join([tag, length, value])) + value = vat_amount.encode("utf-8").hex() + tlv_array.append("".join([tag, length, value])) # Joining bytes into one - tlv_buff = ''.join(tlv_array) + tlv_buff = "".join(tlv_array) # 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 = qr_create(base64_string, error="L") url.png(qr_image, scale=2, quiet_zone=1) 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": "ksa_einv_qr" - }) + _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() # assigning to document - doc.db_set('ksa_einv_qr', _file.file_url) + doc.db_set("ksa_einv_qr", _file.file_url) doc.notify_update() + def get_vat_amount(doc): - vat_settings = frappe.db.get_value('KSA VAT Setting', {'company': doc.company}) + vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company}) vat_accounts = [] vat_amount = 0 if vat_settings: - vat_settings_doc = frappe.get_cached_doc('KSA VAT Setting', vat_settings) + vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings) - for row in vat_settings_doc.get('ksa_vat_sales_accounts'): + for row in vat_settings_doc.get("ksa_vat_sales_accounts"): vat_accounts.append(row.account) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.account_head in vat_accounts: vat_amount += tax.tax_amount return vat_amount + def delete_qr_code_file(doc, method=None): region = get_region(doc.company) - if region not in ['Saudi Arabia']: + if region not in ["Saudi Arabia"]: return - if hasattr(doc, 'ksa_einv_qr'): - if doc.get('ksa_einv_qr'): - file_doc = frappe.get_list('File', { - 'file_url': doc.get('ksa_einv_qr') - }) + if hasattr(doc, "ksa_einv_qr"): + if doc.get("ksa_einv_qr"): + file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")}) if len(file_doc): - frappe.delete_doc('File', file_doc[0].name) + frappe.delete_doc("File", file_doc[0].name) + def delete_vat_settings_for_company(doc, method=None): - if doc.country != 'Saudi Arabia': + if doc.country != "Saudi Arabia": return - if frappe.db.exists('KSA VAT Setting', doc.name): - frappe.delete_doc('KSA VAT Setting', doc.name) + if frappe.db.exists("KSA VAT Setting", doc.name): + frappe.delete_doc("KSA VAT Setting", doc.name) diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py index 97300dc3782..66d9df224e7 100644 --- a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py +++ b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py @@ -5,39 +5,42 @@ import frappe def create_ksa_vat_setting(company): - """On creation of first company. Creates KSA VAT Setting""" + """On creation of first company. Creates KSA VAT Setting""" - company = frappe.get_doc('Company', company) + company = frappe.get_doc("Company", company) - file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'ksa_vat_settings.json') - with open(file_path, 'r') as json_file: - account_data = json.load(json_file) + file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json") + with open(file_path, "r") as json_file: + account_data = json.load(json_file) - # Creating KSA VAT Setting - ksa_vat_setting = frappe.get_doc({ - 'doctype': 'KSA VAT Setting', - 'company': company.name - }) + # Creating KSA VAT Setting + ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name}) - for data in account_data: - if data['type'] == 'Sales Account': - for row in data['accounts']: - item_tax_template = row['item_tax_template'] - account = row['account'] - ksa_vat_setting.append('ksa_vat_sales_accounts', { - 'title': row['title'], - 'item_tax_template': f'{item_tax_template} - {company.abbr}', - 'account': f'{account} - {company.abbr}' - }) + for data in account_data: + if data["type"] == "Sales Account": + for row in data["accounts"]: + item_tax_template = row["item_tax_template"] + account = row["account"] + ksa_vat_setting.append( + "ksa_vat_sales_accounts", + { + "title": row["title"], + "item_tax_template": f"{item_tax_template} - {company.abbr}", + "account": f"{account} - {company.abbr}", + }, + ) - elif data['type'] == 'Purchase Account': - for row in data['accounts']: - item_tax_template = row['item_tax_template'] - account = row['account'] - ksa_vat_setting.append('ksa_vat_purchase_accounts', { - 'title': row['title'], - 'item_tax_template': f'{item_tax_template} - {company.abbr}', - 'account': f'{account} - {company.abbr}' - }) + elif data["type"] == "Purchase Account": + for row in data["accounts"]: + item_tax_template = row["item_tax_template"] + account = row["account"] + ksa_vat_setting.append( + "ksa_vat_purchase_accounts", + { + "title": row["title"], + "item_tax_template": f"{item_tax_template} - {company.abbr}", + "account": f"{account} - {company.abbr}", + }, + ) - ksa_vat_setting.save() + ksa_vat_setting.save() diff --git a/erpnext/regional/south_africa/setup.py b/erpnext/regional/south_africa/setup.py index f018de99272..289f2726e9b 100644 --- a/erpnext/regional/south_africa/setup.py +++ b/erpnext/regional/south_africa/setup.py @@ -11,40 +11,48 @@ def setup(company=None, patch=True): make_custom_fields() add_permissions() + def make_custom_fields(update=True): - is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', fetch_from='item_code.is_zero_rated', - insert_after='description', print_hide=1) + is_zero_rated = dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + fetch_from="item_code.is_zero_rated", + insert_after="description", + print_hide=1, + ) custom_fields = { - 'Item': [ - dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', insert_after='item_group', - print_hide=1) + "Item": [ + dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + insert_after="item_group", + print_hide=1, + ) ], - 'Sales Invoice Item': is_zero_rated, - 'Purchase Invoice Item': is_zero_rated + "Sales Invoice Item": is_zero_rated, + "Purchase Invoice Item": is_zero_rated, } create_custom_fields(custom_fields, update=update) + def add_permissions(): """Add Permissions for South Africa VAT Settings and South Africa VAT Account - and VAT Audit Report""" - for doctype in ('South Africa VAT Settings', 'South Africa VAT Account'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + and VAT Audit Report""" + for doctype in ("South Africa VAT Settings", "South Africa VAT Account"): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) - - if not frappe.db.get_value('Custom Role', dict(report="VAT Audit Report")): - frappe.get_doc(dict( - doctype='Custom Role', - report="VAT Audit Report", - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() \ No newline at end of file + if not frappe.db.get_value("Custom Role", dict(report="VAT Audit Report")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="VAT Audit Report", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() diff --git a/erpnext/regional/turkey/setup.py b/erpnext/regional/turkey/setup.py index c57ea06599d..c915189352c 100644 --- a/erpnext/regional/turkey/setup.py +++ b/erpnext/regional/turkey/setup.py @@ -1,4 +1,2 @@ - - def setup(company=None, patch=True): - pass + pass diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 11f25065eb0..2acab029149 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -16,145 +16,270 @@ def setup(company=None, patch=True): add_permissions() create_gratuity_rule() + def make_custom_fields(): - is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description', - print_hide=1) - is_exempt = dict(fieldname='is_exempt', label='Is Exempt', - fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated', - print_hide=1) + is_zero_rated = dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + fetch_from="item_code.is_zero_rated", + insert_after="description", + print_hide=1, + ) + is_exempt = dict( + fieldname="is_exempt", + label="Is Exempt", + fieldtype="Check", + fetch_from="item_code.is_exempt", + insert_after="is_zero_rated", + print_hide=1, + ) invoice_fields = [ - dict(fieldname='vat_section', label='VAT Details', fieldtype='Section Break', - insert_after='group_same_items', print_hide=1, collapsible=1), - dict(fieldname='permit_no', label='Permit Number', - fieldtype='Data', insert_after='vat_section', print_hide=1), + dict( + fieldname="vat_section", + label="VAT Details", + fieldtype="Section Break", + insert_after="group_same_items", + print_hide=1, + collapsible=1, + ), + dict( + fieldname="permit_no", + label="Permit Number", + fieldtype="Data", + insert_after="vat_section", + print_hide=1, + ), ] purchase_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='shipping_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Read Only', insert_after='supplier_name', - fetch_from='supplier.supplier_name_in_arabic', print_hide=1), - dict(fieldname='recoverable_standard_rated_expenses', print_hide=1, default='0', - label='Recoverable Standard Rated Expenses (AED)', insert_after='permit_no', - fieldtype='Currency', ), - dict(fieldname='reverse_charge', label='Reverse Charge Applicable', - fieldtype='Select', insert_after='recoverable_standard_rated_expenses', print_hide=1, - options='Y\nN', default='N'), - dict(fieldname='recoverable_reverse_charge', label='Recoverable Reverse Charge (Percentage)', - insert_after='reverse_charge', fieldtype='Percent', print_hide=1, - depends_on="eval:doc.reverse_charge=='Y'", default='100.000'), - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="shipping_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Read Only", + insert_after="supplier_name", + fetch_from="supplier.supplier_name_in_arabic", + print_hide=1, + ), + dict( + fieldname="recoverable_standard_rated_expenses", + print_hide=1, + default="0", + label="Recoverable Standard Rated Expenses (AED)", + insert_after="permit_no", + fieldtype="Currency", + ), + dict( + fieldname="reverse_charge", + label="Reverse Charge Applicable", + fieldtype="Select", + insert_after="recoverable_standard_rated_expenses", + print_hide=1, + options="Y\nN", + default="N", + ), + dict( + fieldname="recoverable_reverse_charge", + label="Recoverable Reverse Charge (Percentage)", + insert_after="reverse_charge", + fieldtype="Percent", + print_hide=1, + depends_on="eval:doc.reverse_charge=='Y'", + default="100.000", + ), + ] sales_invoice_fields = [ - dict(fieldname='company_trn', label='Company TRN', - fieldtype='Read Only', insert_after='company_address', - fetch_from='company.tax_id', print_hide=1), - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Read Only', insert_after='customer_name', - fetch_from='customer.customer_name_in_arabic', print_hide=1), - dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain', - fetch_from='company_address.emirate'), - dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)', - insert_after='vat_emirate', fieldtype='Currency', print_hide=1, default='0'), - ] + dict( + fieldname="company_trn", + label="Company TRN", + fieldtype="Read Only", + insert_after="company_address", + fetch_from="company.tax_id", + print_hide=1, + ), + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Read Only", + insert_after="customer_name", + fetch_from="customer.customer_name_in_arabic", + print_hide=1, + ), + dict( + fieldname="vat_emirate", + label="VAT Emirate", + insert_after="permit_no", + fieldtype="Select", + options="\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain", + fetch_from="company_address.emirate", + ), + dict( + fieldname="tourist_tax_return", + label="Tax Refund provided to Tourists (AED)", + insert_after="vat_emirate", + fieldtype="Currency", + print_hide=1, + default="0", + ), + ] invoice_item_fields = [ - dict(fieldname='tax_code', label='Tax Code', - fieldtype='Read Only', fetch_from='item_code.tax_code', insert_after='description', - allow_on_submit=1, print_hide=1), - dict(fieldname='tax_rate', label='Tax Rate', - fieldtype='Float', insert_after='tax_code', - print_hide=1, hidden=1, read_only=1), - dict(fieldname='tax_amount', label='Tax Amount', - fieldtype='Currency', insert_after='tax_rate', - print_hide=1, hidden=1, read_only=1, options="currency"), - dict(fieldname='total_amount', label='Total Amount', - fieldtype='Currency', insert_after='tax_amount', - print_hide=1, hidden=1, read_only=1, options="currency"), + dict( + fieldname="tax_code", + label="Tax Code", + fieldtype="Read Only", + fetch_from="item_code.tax_code", + insert_after="description", + allow_on_submit=1, + print_hide=1, + ), + dict( + fieldname="tax_rate", + label="Tax Rate", + fieldtype="Float", + insert_after="tax_code", + print_hide=1, + hidden=1, + read_only=1, + ), + dict( + fieldname="tax_amount", + label="Tax Amount", + fieldtype="Currency", + insert_after="tax_rate", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), + dict( + fieldname="total_amount", + label="Total Amount", + fieldtype="Currency", + insert_after="tax_amount", + print_hide=1, + hidden=1, + read_only=1, + options="currency", + ), ] delivery_date_field = [ - dict(fieldname='delivery_date', label='Delivery Date', - fieldtype='Date', insert_after='item_name', print_hide=1) + dict( + fieldname="delivery_date", + label="Delivery Date", + fieldtype="Date", + insert_after="item_name", + print_hide=1, + ) ] custom_fields = { - 'Item': [ - dict(fieldname='tax_code', label='Tax Code', - fieldtype='Data', insert_after='item_group'), - dict(fieldname='is_zero_rated', label='Is Zero Rated', - fieldtype='Check', insert_after='tax_code', - print_hide=1), - dict(fieldname='is_exempt', label='Is Exempt', - fieldtype='Check', insert_after='is_zero_rated', - print_hide=1) + "Item": [ + dict(fieldname="tax_code", label="Tax Code", fieldtype="Data", insert_after="item_group"), + dict( + fieldname="is_zero_rated", + label="Is Zero Rated", + fieldtype="Check", + insert_after="tax_code", + print_hide=1, + ), + dict( + fieldname="is_exempt", + label="Is Exempt", + fieldtype="Check", + insert_after="is_zero_rated", + print_hide=1, + ), ], - 'Customer': [ - dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic', - fieldtype='Data', insert_after='customer_name'), + "Customer": [ + dict( + fieldname="customer_name_in_arabic", + label="Customer Name in Arabic", + fieldtype="Data", + insert_after="customer_name", + ), ], - 'Supplier': [ - dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic', - fieldtype='Data', insert_after='supplier_name'), + "Supplier": [ + dict( + fieldname="supplier_name_in_arabic", + label="Supplier Name in Arabic", + fieldtype="Data", + insert_after="supplier_name", + ), ], - 'Address': [ - dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state', - options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain') + "Address": [ + dict( + fieldname="emirate", + label="Emirate", + fieldtype="Select", + insert_after="state", + options="\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain", + ) ], - 'Purchase Invoice': purchase_invoice_fields + invoice_fields, - 'Purchase Order': purchase_invoice_fields + invoice_fields, - 'Purchase Receipt': purchase_invoice_fields + invoice_fields, - 'Sales Invoice': sales_invoice_fields + invoice_fields, - 'POS Invoice': sales_invoice_fields + invoice_fields, - 'Sales Order': sales_invoice_fields + invoice_fields, - 'Delivery Note': sales_invoice_fields + invoice_fields, - 'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], - 'POS Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], - 'Purchase Invoice Item': invoice_item_fields, - 'Sales Order Item': invoice_item_fields, - 'Delivery Note Item': invoice_item_fields, - 'Quotation Item': invoice_item_fields, - 'Purchase Order Item': invoice_item_fields, - 'Purchase Receipt Item': invoice_item_fields, - 'Supplier Quotation Item': invoice_item_fields, + "Purchase Invoice": purchase_invoice_fields + invoice_fields, + "Purchase Order": purchase_invoice_fields + invoice_fields, + "Purchase Receipt": purchase_invoice_fields + invoice_fields, + "Sales Invoice": sales_invoice_fields + invoice_fields, + "POS Invoice": sales_invoice_fields + invoice_fields, + "Sales Order": sales_invoice_fields + invoice_fields, + "Delivery Note": sales_invoice_fields + invoice_fields, + "Sales Invoice Item": invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], + "POS Invoice Item": invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt], + "Purchase Invoice Item": invoice_item_fields, + "Sales Order Item": invoice_item_fields, + "Delivery Note Item": invoice_item_fields, + "Quotation Item": invoice_item_fields, + "Purchase Order Item": invoice_item_fields, + "Purchase Receipt Item": invoice_item_fields, + "Supplier Quotation Item": invoice_item_fields, } create_custom_fields(custom_fields) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "detailed_tax_invoice") frappe.reload_doc("regional", "print_format", "simplified_tax_invoice") frappe.reload_doc("regional", "print_format", "tax_invoice") - frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """) + frappe.db.sql( + """ update `tabPrint Format` set disabled = 0 where + name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """ + ) + def add_custom_roles_for_reports(): """Add Access Control to UAE VAT 201.""" - if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 201')): - frappe.get_doc(dict( - doctype='Custom Role', - report='UAE VAT 201', - roles= [ - dict(role='Accounts User'), - dict(role='Accounts Manager'), - dict(role='Auditor') - ] - )).insert() + if not frappe.db.get_value("Custom Role", dict(report="UAE VAT 201")): + frappe.get_doc( + dict( + doctype="Custom Role", + report="UAE VAT 201", + roles=[dict(role="Accounts User"), dict(role="Accounts Manager"), dict(role="Auditor")], + ) + ).insert() + def add_permissions(): """Add Permissions for UAE VAT Settings and UAE VAT Account.""" - for doctype in ('UAE VAT Settings', 'UAE VAT Account'): - add_permission(doctype, 'All', 0) - for role in ('Accounts Manager', 'Accounts User', 'System Manager'): + for doctype in ("UAE VAT Settings", "UAE VAT Account"): + add_permission(doctype, "All", 0) + for role in ("Accounts Manager", "Accounts User", "System Manager"): add_permission(doctype, role, 0) - update_permission_property(doctype, role, 0, 'write', 1) - update_permission_property(doctype, role, 0, 'create', 1) + update_permission_property(doctype, role, 0, "write", 1) + update_permission_property(doctype, role, 0, "create", 1) + def create_gratuity_rule(): rule_1 = rule_2 = rule_3 = None @@ -162,7 +287,11 @@ def create_gratuity_rule(): # Rule Under Limited Contract slabs = get_slab_for_limited_contract() if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"): - rule_1 = get_gratuity_rule("Rule Under Limited Contract (UAE)", slabs, calculate_gratuity_amount_based_on="Sum of all previous slabs") + rule_1 = get_gratuity_rule( + "Rule Under Limited Contract (UAE)", + slabs, + calculate_gratuity_amount_based_on="Sum of all previous slabs", + ) # Rule Under Unlimited Contract on termination slabs = get_slab_for_unlimited_contract_on_termination() @@ -174,7 +303,7 @@ def create_gratuity_rule(): if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"): rule_3 = get_gratuity_rule("Rule Under Unlimited Contract on resignation (UAE)", slabs) - #for applicable salary component user need to set this by its own + # for applicable salary component user need to set this by its own if rule_1: rule_1.flags.ignore_mandatory = True rule_1.save() @@ -187,61 +316,29 @@ def create_gratuity_rule(): def get_slab_for_limited_contract(): - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 5, "fraction_of_applicable_earnings": 21 / 30}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": 1}, + ] + def get_slab_for_unlimited_contract_on_termination(): - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 5, "fraction_of_applicable_earnings": 21 / 30}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": 1}, + ] + def get_slab_for_unlimited_contract_on_resignation(): - fraction_1 = 1/3 * 21/30 - fraction_2 = 2/3 * 21/30 - fraction_3 = 21/30 + fraction_1 = 1 / 3 * 21 / 30 + fraction_2 = 2 / 3 * 21 / 30 + fraction_3 = 21 / 30 - return [{ - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }, - { - "from_year": 1, - "to_year":3, - "fraction_of_applicable_earnings": fraction_1 - }, - { - "from_year": 3, - "to_year":5, - "fraction_of_applicable_earnings": fraction_2 - }, - { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": fraction_3 - }] + return [ + {"from_year": 0, "to_year": 1, "fraction_of_applicable_earnings": 0}, + {"from_year": 1, "to_year": 3, "fraction_of_applicable_earnings": fraction_1}, + {"from_year": 3, "to_year": 5, "fraction_of_applicable_earnings": fraction_2}, + {"from_year": 5, "to_year": 0, "fraction_of_applicable_earnings": fraction_3}, + ] diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index c18af93b2c8..b0312f7972b 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ from frappe.utils import flt, money_in_words, round_based_on_smallest_currency_fraction @@ -9,7 +8,8 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): - if not doc.taxes: return + if not doc.taxes: + return itemised_tax = get_itemised_tax(doc.taxes) @@ -26,40 +26,39 @@ def update_itemised_tax_data(doc): for account, rate in iteritems(item_tax_rate): tax_rate += rate elif row.item_code and itemised_tax.get(row.item_code): - tax_rate = sum([tax.get('tax_rate', 0) for d, tax in itemised_tax.get(row.item_code).items()]) + tax_rate = sum([tax.get("tax_rate", 0) for d, tax in itemised_tax.get(row.item_code).items()]) meta = frappe.get_meta(row.doctype) - if meta.has_field('tax_rate'): + if meta.has_field("tax_rate"): row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + def get_account_currency(account): """Helper function to get account currency.""" if not account: return + def generator(): account_currency, company = frappe.get_cached_value( - "Account", - account, - ["account_currency", - "company"] + "Account", account, ["account_currency", "company"] ) if not account_currency: - account_currency = frappe.get_cached_value('Company', company, "default_currency") + account_currency = frappe.get_cached_value("Company", company, "default_currency") return account_currency return frappe.local_cache("account_currency", account, generator) + def get_tax_accounts(company): """Get the list of tax accounts for a specific company.""" tax_accounts_dict = frappe._dict() - tax_accounts_list = frappe.get_all("UAE VAT Account", - filters={"parent": company}, - fields=["Account"] - ) + tax_accounts_list = frappe.get_all( + "UAE VAT Account", filters={"parent": company}, fields=["Account"] + ) if not tax_accounts_list and not frappe.flags.in_test: frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company)) @@ -69,23 +68,24 @@ def get_tax_accounts(company): return tax_accounts_dict + def update_grand_total_for_rcm(doc, method): """If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form.""" - country = frappe.get_cached_value('Company', doc.company, 'country') + country = frappe.get_cached_value("Company", doc.company, "country") - if country != 'United Arab Emirates': + if country != "United Arab Emirates": return if not doc.total_taxes_and_charges: return - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": tax_accounts = get_tax_accounts(doc.company) base_vat_tax = 0 vat_tax = 0 - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.category not in ("Total", "Valuation and Total"): continue @@ -100,6 +100,7 @@ def update_grand_total_for_rcm(doc, method): update_totals(vat_tax, base_vat_tax, doc) + def update_totals(vat_tax, base_vat_tax, doc): """Update the grand total values in the form.""" doc.base_grand_total -= base_vat_tax @@ -111,56 +112,67 @@ def update_totals(vat_tax, base_vat_tax, doc): doc.outstanding_amount = doc.grand_total else: - doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total, - doc.currency, doc.precision("rounded_total")) - doc.rounding_adjustment = flt(doc.rounded_total - doc.grand_total, - doc.precision("rounding_adjustment")) + doc.rounded_total = round_based_on_smallest_currency_fraction( + doc.grand_total, doc.currency, doc.precision("rounded_total") + ) + doc.rounding_adjustment = flt( + doc.rounded_total - doc.grand_total, doc.precision("rounding_adjustment") + ) doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.in_words = money_in_words(doc.grand_total, doc.currency) - doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) + doc.base_in_words = money_in_words( + doc.base_grand_total, erpnext.get_company_currency(doc.company) + ) doc.set_payment_schedule() + def make_regional_gl_entries(gl_entries, doc): """Hooked to make_regional_gl_entries in Purchase Invoice.It appends the region specific general ledger entries to the list of GL Entries.""" - country = frappe.get_cached_value('Company', doc.company, 'country') + country = frappe.get_cached_value("Company", doc.company, "country") - if country != 'United Arab Emirates': + if country != "United Arab Emirates": return gl_entries - if doc.reverse_charge == 'Y': + if doc.reverse_charge == "Y": tax_accounts = get_tax_accounts(doc.company) - for tax in doc.get('taxes'): + for tax in doc.get("taxes"): if tax.category not in ("Total", "Valuation and Total"): continue gl_entries = make_gl_entry(tax, gl_entries, doc, tax_accounts) return gl_entries + def make_gl_entry(tax, gl_entries, doc, tax_accounts): dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" - if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: + if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts: account_currency = get_account_currency(tax.account_head) - gl_entries.append(doc.get_gl_dict({ - "account": tax.account_head, - "cost_center": tax.cost_center, - "posting_date": doc.posting_date, - "against": doc.supplier, - dr_or_cr: tax.base_tax_amount_after_discount_amount, - dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ - if account_currency==doc.company_currency \ - else tax.tax_amount_after_discount_amount - }, account_currency, item=tax - )) + gl_entries.append( + doc.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "posting_date": doc.posting_date, + "against": doc.supplier, + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount + if account_currency == doc.company_currency + else tax.tax_amount_after_discount_amount, + }, + account_currency, + item=tax, + ) + ) return gl_entries def validate_returns(doc, method): """Standard Rated expenses should not be set when Reverse Charge Applicable is set.""" - country = frappe.get_cached_value('Company', doc.company, 'country') - if country != 'United Arab Emirates': + country = frappe.get_cached_value("Company", doc.company, "country") + if country != "United Arab Emirates": return - if doc.reverse_charge == 'Y' and flt(doc.recoverable_standard_rated_expenses) != 0: - frappe.throw(_( - "Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y" - )) + if doc.reverse_charge == "Y" and flt(doc.recoverable_standard_rated_expenses) != 0: + frappe.throw( + _("Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y") + ) diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index e2eb79b05b0..a7dee9deabc 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -11,38 +11,61 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields def setup(company=None, patch=True): # Company independent fixtures should be called only once at the first company setup - if frappe.db.count('Company', {'country': 'United States'}) <=1: + if frappe.db.count("Company", {"country": "United States"}) <= 1: setup_company_independent_fixtures(patch=patch) + def setup_company_independent_fixtures(company=None, patch=True): make_custom_fields() add_print_formats() + def make_custom_fields(update=True): custom_fields = { - 'Supplier': [ - dict(fieldname='irs_1099', fieldtype='Check', insert_after='tax_id', - label='Is IRS 1099 reporting required for supplier?') + "Supplier": [ + dict( + fieldname="irs_1099", + fieldtype="Check", + insert_after="tax_id", + label="Is IRS 1099 reporting required for supplier?", + ) ], - 'Sales Order': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges', - label='Is customer exempted from sales tax?') + "Sales Order": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_and_charges", + label="Is customer exempted from sales tax?", + ) ], - 'Sales Invoice': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_section', - label='Is customer exempted from sales tax?') + "Sales Invoice": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_section", + label="Is customer exempted from sales tax?", + ) ], - 'Customer': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='represents_company', - label='Is customer exempted from sales tax?') + "Customer": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="represents_company", + label="Is customer exempted from sales tax?", + ) + ], + "Quotation": [ + dict( + fieldname="exempt_from_sales_tax", + fieldtype="Check", + insert_after="taxes_and_charges", + label="Is customer exempted from sales tax?", + ) ], - 'Quotation': [ - dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges', - label='Is customer exempted from sales tax?') - ] } create_custom_fields(custom_fields, update=update) + def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0) diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 652b4835a18..83ba6ed3ad1 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -9,49 +9,51 @@ from erpnext.regional.report.irs_1099.irs_1099 import execute as execute_1099_re class TestUnitedStates(unittest.TestCase): - def test_irs_1099_custom_field(self): + def test_irs_1099_custom_field(self): - if not frappe.db.exists("Supplier", "_US 1099 Test Supplier"): - doc = frappe.new_doc("Supplier") - doc.supplier_name = "_US 1099 Test Supplier" - doc.supplier_group = "Services" - doc.supplier_type = "Company" - doc.country = "United States" - doc.tax_id = "04-1234567" - doc.irs_1099 = 1 - doc.save() - frappe.db.commit() - supplier = frappe.get_doc('Supplier', "_US 1099 Test Supplier") - self.assertEqual(supplier.irs_1099, 1) + if not frappe.db.exists("Supplier", "_US 1099 Test Supplier"): + doc = frappe.new_doc("Supplier") + doc.supplier_name = "_US 1099 Test Supplier" + doc.supplier_group = "Services" + doc.supplier_type = "Company" + doc.country = "United States" + doc.tax_id = "04-1234567" + doc.irs_1099 = 1 + doc.save() + frappe.db.commit() + supplier = frappe.get_doc("Supplier", "_US 1099 Test Supplier") + self.assertEqual(supplier.irs_1099, 1) - def test_irs_1099_report(self): - make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) - columns, data = execute_1099_report(filters) - expected_row = {'supplier': '_US 1099 Test Supplier', - 'supplier_group': 'Services', - 'payments': 100.0, - 'tax_id': '04-1234567'} - self.assertEqual(data[0], expected_row) + def test_irs_1099_report(self): + make_payment_entry_to_irs_1099_supplier() + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) + columns, data = execute_1099_report(filters) + expected_row = { + "supplier": "_US 1099 Test Supplier", + "supplier_group": "Services", + "payments": 100.0, + "tax_id": "04-1234567", + } + self.assertEqual(data[0], expected_row) def make_payment_entry_to_irs_1099_supplier(): - frappe.db.sql("delete from `tabGL Entry` where party='_US 1099 Test Supplier'") - frappe.db.sql("delete from `tabGL Entry` where against='_US 1099 Test Supplier'") - frappe.db.sql("delete from `tabPayment Entry` where party='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabGL Entry` where party='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabGL Entry` where against='_US 1099 Test Supplier'") + frappe.db.sql("delete from `tabPayment Entry` where party='_US 1099 Test Supplier'") - pe = frappe.new_doc("Payment Entry") - pe.payment_type = "Pay" - pe.company = "_Test Company 1" - pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC1" - pe.paid_to = "_Test Payable USD - _TC1" - pe.paid_amount = 100 - pe.received_amount = 100 - pe.reference_no = "For IRS 1099 testing" - pe.reference_date = "2016-01-10" - pe.party_type = "Supplier" - pe.party = "_US 1099 Test Supplier" - pe.insert() - pe.submit() + pe = frappe.new_doc("Payment Entry") + pe.payment_type = "Pay" + pe.company = "_Test Company 1" + pe.posting_date = "2016-01-10" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" + pe.paid_amount = 100 + pe.received_amount = 100 + pe.reference_no = "For IRS 1099 testing" + pe.reference_date = "2016-01-10" + pe.party_type = "Supplier" + pe.party = "_US 1099 Test Supplier" + pe.insert() + pe.submit() diff --git a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py index c91ef56142d..a2ebec0a4d6 100644 --- a/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py +++ b/erpnext/restaurant/doctype/restaurant/restaurant_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'restaurant', - 'transactions': [ - { - 'label': _('Setup'), - 'items': ['Restaurant Menu', 'Restaurant Table'] - }, - { - 'label': _('Operations'), - 'items': ['Restaurant Reservation', 'Sales Invoice'] - } - ] + "fieldname": "restaurant", + "transactions": [ + {"label": _("Setup"), "items": ["Restaurant Menu", "Restaurant Table"]}, + {"label": _("Operations"), "items": ["Restaurant Reservation", "Sales Invoice"]}, + ], } diff --git a/erpnext/restaurant/doctype/restaurant/test_restaurant.py b/erpnext/restaurant/doctype/restaurant/test_restaurant.py index f88f9801290..0276179323d 100644 --- a/erpnext/restaurant/doctype/restaurant/test_restaurant.py +++ b/erpnext/restaurant/doctype/restaurant/test_restaurant.py @@ -4,11 +4,22 @@ import unittest test_records = [ - dict(doctype='Restaurant', name='Test Restaurant 1', company='_Test Company 1', - invoice_series_prefix='Test-Rest-1-Inv-', default_customer='_Test Customer 1'), - dict(doctype='Restaurant', name='Test Restaurant 2', company='_Test Company 1', - invoice_series_prefix='Test-Rest-2-Inv-', default_customer='_Test Customer 1'), + dict( + doctype="Restaurant", + name="Test Restaurant 1", + company="_Test Company 1", + invoice_series_prefix="Test-Rest-1-Inv-", + default_customer="_Test Customer 1", + ), + dict( + doctype="Restaurant", + name="Test Restaurant 2", + company="_Test Company 1", + invoice_series_prefix="Test-Rest-2-Inv-", + default_customer="_Test Customer 1", + ), ] + class TestRestaurant(unittest.TestCase): pass diff --git a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py index 64eb40f3645..893c5123c6b 100644 --- a/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py +++ b/erpnext/restaurant/doctype/restaurant_menu/restaurant_menu.py @@ -10,45 +10,44 @@ class RestaurantMenu(Document): def validate(self): for d in self.items: if not d.rate: - d.rate = frappe.db.get_value('Item', d.item, 'standard_rate') + d.rate = frappe.db.get_value("Item", d.item, "standard_rate") def on_update(self): - '''Sync Price List''' + """Sync Price List""" self.make_price_list() def on_trash(self): - '''clear prices''' + """clear prices""" self.clear_item_price() def clear_item_price(self, price_list=None): - '''clear all item prices for this menu''' + """clear all item prices for this menu""" if not price_list: price_list = self.get_price_list().name - frappe.db.sql('delete from `tabItem Price` where price_list = %s', price_list) + frappe.db.sql("delete from `tabItem Price` where price_list = %s", price_list) def make_price_list(self): # create price list for menu price_list = self.get_price_list() - self.db_set('price_list', price_list.name) + self.db_set("price_list", price_list.name) # delete old items self.clear_item_price(price_list.name) for d in self.items: - frappe.get_doc(dict( - doctype = 'Item Price', - price_list = price_list.name, - item_code = d.item, - price_list_rate = d.rate - )).insert() + frappe.get_doc( + dict( + doctype="Item Price", price_list=price_list.name, item_code=d.item, price_list_rate=d.rate + ) + ).insert() def get_price_list(self): - '''Create price list for menu if missing''' - price_list_name = frappe.db.get_value('Price List', dict(restaurant_menu=self.name)) + """Create price list for menu if missing""" + price_list_name = frappe.db.get_value("Price List", dict(restaurant_menu=self.name)) if price_list_name: - price_list = frappe.get_doc('Price List', price_list_name) + price_list = frappe.get_doc("Price List", price_list_name) else: - price_list = frappe.new_doc('Price List') + price_list = frappe.new_doc("Price List") price_list.restaurant_menu = self.name price_list.price_list_name = self.name diff --git a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py b/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py index 27020eb869f..d8e23eb9a45 100644 --- a/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py +++ b/erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.py @@ -6,47 +6,70 @@ import unittest import frappe test_records = [ - dict(doctype='Item', item_code='Food Item 1', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 2', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 3', - item_group='Products', is_stock_item=0), - dict(doctype='Item', item_code='Food Item 4', - item_group='Products', is_stock_item=0), - dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 1', - items = [ - dict(item='Food Item 1', rate=400), - dict(item='Food Item 2', rate=300), - dict(item='Food Item 3', rate=200), - dict(item='Food Item 4', rate=100), - ]), - dict(doctype='Restaurant Menu', restaurant='Test Restaurant 1', name='Test Restaurant 1 Menu 2', - items = [ - dict(item='Food Item 1', rate=450), - dict(item='Food Item 2', rate=350), - ]) + dict(doctype="Item", item_code="Food Item 1", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Food Item 2", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Food Item 3", item_group="Products", is_stock_item=0), + dict(doctype="Item", item_code="Food Item 4", item_group="Products", is_stock_item=0), + dict( + doctype="Restaurant Menu", + restaurant="Test Restaurant 1", + name="Test Restaurant 1 Menu 1", + items=[ + dict(item="Food Item 1", rate=400), + dict(item="Food Item 2", rate=300), + dict(item="Food Item 3", rate=200), + dict(item="Food Item 4", rate=100), + ], + ), + dict( + doctype="Restaurant Menu", + restaurant="Test Restaurant 1", + name="Test Restaurant 1 Menu 2", + items=[ + dict(item="Food Item 1", rate=450), + dict(item="Food Item 2", rate=350), + ], + ), ] + class TestRestaurantMenu(unittest.TestCase): def test_price_list_creation_and_editing(self): - menu1 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 1') + menu1 = frappe.get_doc("Restaurant Menu", "Test Restaurant 1 Menu 1") menu1.save() - menu2 = frappe.get_doc('Restaurant Menu', 'Test Restaurant 1 Menu 2') + menu2 = frappe.get_doc("Restaurant Menu", "Test Restaurant 1 Menu 2") menu2.save() - self.assertTrue(frappe.db.get_value('Price List', 'Test Restaurant 1 Menu 1')) - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 400) - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 2', item_code='Food Item 1'), 'price_list_rate'), 450) + self.assertTrue(frappe.db.get_value("Price List", "Test Restaurant 1 Menu 1")) + self.assertEqual( + frappe.db.get_value( + "Item Price", + dict(price_list="Test Restaurant 1 Menu 1", item_code="Food Item 1"), + "price_list_rate", + ), + 400, + ) + self.assertEqual( + frappe.db.get_value( + "Item Price", + dict(price_list="Test Restaurant 1 Menu 2", item_code="Food Item 1"), + "price_list_rate", + ), + 450, + ) menu1.items[0].rate = 401 menu1.save() - self.assertEqual(frappe.db.get_value('Item Price', - dict(price_list = 'Test Restaurant 1 Menu 1', item_code='Food Item 1'), 'price_list_rate'), 401) + self.assertEqual( + frappe.db.get_value( + "Item Price", + dict(price_list="Test Restaurant 1 Menu 1", item_code="Food Item 1"), + "price_list_rate", + ), + 401, + ) menu1.items[0].rate = 400 menu1.save() diff --git a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py index f9e75b47a0f..b22f164382f 100644 --- a/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py +++ b/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py @@ -14,78 +14,84 @@ from erpnext.controllers.queries import item_query class RestaurantOrderEntry(Document): pass + @frappe.whitelist() def get_invoice(table): - '''returns the active invoice linked to the given table''' - invoice_name = frappe.get_value('Sales Invoice', dict(restaurant_table = table, docstatus=0)) + """returns the active invoice linked to the given table""" + invoice_name = frappe.get_value("Sales Invoice", dict(restaurant_table=table, docstatus=0)) restaurant, menu_name = get_restaurant_and_menu_name(table) if invoice_name: - invoice = frappe.get_doc('Sales Invoice', invoice_name) + invoice = frappe.get_doc("Sales Invoice", invoice_name) else: - invoice = frappe.new_doc('Sales Invoice') - invoice.naming_series = frappe.db.get_value('Restaurant', restaurant, 'invoice_series_prefix') + invoice = frappe.new_doc("Sales Invoice") + invoice.naming_series = frappe.db.get_value("Restaurant", restaurant, "invoice_series_prefix") invoice.is_pos = 1 - default_customer = frappe.db.get_value('Restaurant', restaurant, 'default_customer') + default_customer = frappe.db.get_value("Restaurant", restaurant, "default_customer") if not default_customer: - frappe.throw(_('Please set default customer in Restaurant Settings')) + frappe.throw(_("Please set default customer in Restaurant Settings")) invoice.customer = default_customer - invoice.taxes_and_charges = frappe.db.get_value('Restaurant', restaurant, 'default_tax_template') - invoice.selling_price_list = frappe.db.get_value('Price List', dict(restaurant_menu=menu_name, enabled=1)) + invoice.taxes_and_charges = frappe.db.get_value("Restaurant", restaurant, "default_tax_template") + invoice.selling_price_list = frappe.db.get_value( + "Price List", dict(restaurant_menu=menu_name, enabled=1) + ) return invoice + @frappe.whitelist() def sync(table, items): - '''Sync the sales order related to the table''' + """Sync the sales order related to the table""" invoice = get_invoice(table) items = json.loads(items) invoice.items = [] invoice.restaurant_table = table for d in items: - invoice.append('items', dict( - item_code = d.get('item'), - qty = d.get('qty') - )) + invoice.append("items", dict(item_code=d.get("item"), qty=d.get("qty"))) invoice.save() return invoice.as_dict() + @frappe.whitelist() def make_invoice(table, customer, mode_of_payment): - '''Make table based on Sales Order''' + """Make table based on Sales Order""" restaurant, menu = get_restaurant_and_menu_name(table) invoice = get_invoice(table) invoice.customer = customer invoice.restaurant = restaurant invoice.calculate_taxes_and_totals() - invoice.append('payments', dict(mode_of_payment=mode_of_payment, amount=invoice.grand_total)) + invoice.append("payments", dict(mode_of_payment=mode_of_payment, amount=invoice.grand_total)) invoice.save() invoice.submit() - frappe.msgprint(_('Invoice Created'), indicator='green', alert=True) + frappe.msgprint(_("Invoice Created"), indicator="green", alert=True) return invoice.name -@frappe.whitelist() -def item_query_restaurant(doctype='Item', txt='', searchfield='name', start=0, page_len=20, filters=None, as_dict=False): - '''Return items that are selected in active menu of the restaurant''' - restaurant, menu = get_restaurant_and_menu_name(filters['table']) - items = frappe.db.get_all('Restaurant Menu Item', ['item'], dict(parent = menu)) - del filters['table'] - filters['name'] = ('in', [d.item for d in items]) - return item_query('Item', txt, searchfield, start, page_len, filters, as_dict) +@frappe.whitelist() +def item_query_restaurant( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=None, as_dict=False +): + """Return items that are selected in active menu of the restaurant""" + restaurant, menu = get_restaurant_and_menu_name(filters["table"]) + items = frappe.db.get_all("Restaurant Menu Item", ["item"], dict(parent=menu)) + del filters["table"] + filters["name"] = ("in", [d.item for d in items]) + + return item_query("Item", txt, searchfield, start, page_len, filters, as_dict) + def get_restaurant_and_menu_name(table): if not table: - frappe.throw(_('Please select a table')) + frappe.throw(_("Please select a table")) - restaurant = frappe.db.get_value('Restaurant Table', table, 'restaurant') - menu = frappe.db.get_value('Restaurant', restaurant, 'active_menu') + restaurant = frappe.db.get_value("Restaurant Table", table, "restaurant") + menu = frappe.db.get_value("Restaurant", restaurant, "active_menu") if not menu: - frappe.throw(_('Please set an active menu for Restaurant {0}').format(restaurant)) + frappe.throw(_("Please set an active menu for Restaurant {0}").format(restaurant)) return restaurant, menu diff --git a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py index 29f8a1a12b1..79255253c5a 100644 --- a/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py +++ b/erpnext/restaurant/doctype/restaurant_table/restaurant_table.py @@ -10,5 +10,5 @@ from frappe.model.naming import make_autoname class RestaurantTable(Document): def autoname(self): - prefix = re.sub('-+', '-', self.restaurant.replace(' ', '-')) - self.name = make_autoname(prefix + '-.##') + prefix = re.sub("-+", "-", self.restaurant.replace(" ", "-")) + self.name = make_autoname(prefix + "-.##") diff --git a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py b/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py index 00d14d2bb2a..761c37f83fc 100644 --- a/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py +++ b/erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.py @@ -4,11 +4,12 @@ import unittest test_records = [ - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), - dict(restaurant='Test Restaurant 1', no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), + dict(restaurant="Test Restaurant 1", no_of_seats=5, minimum_seating=1), ] + class TestRestaurantTable(unittest.TestCase): pass diff --git a/erpnext/selling/doctype/campaign/campaign.py b/erpnext/selling/doctype/campaign/campaign.py index 1bc7e69a18f..efea8bfe472 100644 --- a/erpnext/selling/doctype/campaign/campaign.py +++ b/erpnext/selling/doctype/campaign/campaign.py @@ -9,7 +9,7 @@ from frappe.model.naming import set_name_by_naming_series class Campaign(Document): def autoname(self): - if frappe.defaults.get_global_default('campaign_naming_by') != 'Naming Series': + if frappe.defaults.get_global_default("campaign_naming_by") != "Naming Series": self.name = self.campaign_name else: set_name_by_naming_series(self) diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py index fd04e0ff6e2..e5c490ff356 100644 --- a/erpnext/selling/doctype/campaign/campaign_dashboard.py +++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py @@ -1,18 +1,11 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'campaign_name', - 'transactions': [ - { - 'label': _('Email Campaigns'), - 'items': ['Email Campaign'] - }, - { - 'label': _('Social Media Campaigns'), - 'items': ['Social Media Post'] - } - ] + "fieldname": "campaign_name", + "transactions": [ + {"label": _("Email Campaigns"), "items": ["Email Campaign"]}, + {"label": _("Social Media Campaigns"), "items": ["Social Media Post"]}, + ], } diff --git a/erpnext/selling/doctype/campaign/test_campaign.py b/erpnext/selling/doctype/campaign/test_campaign.py index 25001802857..f0c051e2b3e 100644 --- a/erpnext/selling/doctype/campaign/test_campaign.py +++ b/erpnext/selling/doctype/campaign/test_campaign.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Campaign') +test_records = frappe.get_test_records("Campaign") diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index c78227e4c86..2e5cbb80cb6 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -37,13 +37,13 @@ class Customer(TransactionBase): def load_dashboard_info(self): info = get_dashboard_info(self.doctype, self.name, self.loyalty_program) - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def autoname(self): - cust_master_name = frappe.defaults.get_global_default('cust_master_name') - if cust_master_name == 'Customer Name': + cust_master_name = frappe.defaults.get_global_default("cust_master_name") + if cust_master_name == "Customer Name": self.name = self.get_customer_name() - elif cust_master_name == 'Naming Series': + elif cust_master_name == "Naming Series": set_name_by_naming_series(self) else: self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) @@ -51,22 +51,30 @@ class Customer(TransactionBase): def get_customer_name(self): if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: - count = frappe.db.sql("""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer - where name like %s""", "%{0} - %".format(self.customer_name), as_list=1)[0][0] + count = frappe.db.sql( + """select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer + where name like %s""", + "%{0} - %".format(self.customer_name), + as_list=1, + )[0][0] count = cint(count) + 1 new_customer_name = "{0} - {1}".format(self.customer_name, cstr(count)) - msgprint(_("Changed customer name to '{}' as '{}' already exists.") - .format(new_customer_name, self.customer_name), - title=_("Note"), indicator="yellow") + msgprint( + _("Changed customer name to '{}' as '{}' already exists.").format( + new_customer_name, self.customer_name + ), + title=_("Note"), + indicator="yellow", + ) return new_customer_name return self.customer_name def after_insert(self): - '''If customer created from Lead, update customer id in quotations, opportunities''' + """If customer created from Lead, update customer id in quotations, opportunities""" self.update_lead_status() def validate(self): @@ -80,8 +88,8 @@ class Customer(TransactionBase): self.validate_internal_customer() # set loyalty program tier - if frappe.db.exists('Customer', self.name): - customer = frappe.get_doc('Customer', self.name) + if frappe.db.exists("Customer", self.name): + customer = frappe.get_doc("Customer", self.name) if self.loyalty_program == customer.loyalty_program and not self.loyalty_program_tier: self.loyalty_program_tier = customer.loyalty_program_tier @@ -91,7 +99,7 @@ class Customer(TransactionBase): @frappe.whitelist() def get_customer_group_details(self): - doc = frappe.get_doc('Customer Group', self.customer_group) + doc = frappe.get_doc("Customer Group", self.customer_group) self.accounts = self.credit_limits = [] self.payment_terms = self.default_price_list = "" @@ -100,14 +108,16 @@ class Customer(TransactionBase): for row in tables: table, field = row[0], row[1] - if not doc.get(table): continue + if not doc.get(table): + continue for entry in doc.get(table): child = self.append(table) child.update({"company": entry.company, field: entry.get(field)}) for field in fields: - if not doc.get(field): continue + if not doc.get(field): + continue self.update({field: doc.get(field)}) self.save() @@ -115,23 +125,37 @@ class Customer(TransactionBase): def check_customer_group_change(self): frappe.flags.customer_group_changed = False - if not self.get('__islocal'): - if self.customer_group != frappe.db.get_value('Customer', self.name, 'customer_group'): + if not self.get("__islocal"): + if self.customer_group != frappe.db.get_value("Customer", self.name, "customer_group"): frappe.flags.customer_group_changed = True def validate_default_bank_account(self): if self.default_bank_account: - is_company_account = frappe.db.get_value('Bank Account', self.default_bank_account, 'is_company_account') + is_company_account = frappe.db.get_value( + "Bank Account", self.default_bank_account, "is_company_account" + ) if not is_company_account: - frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))) + frappe.throw( + _("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)) + ) def validate_internal_customer(self): - internal_customer = frappe.db.get_value("Customer", - {"is_internal_customer": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name") + internal_customer = frappe.db.get_value( + "Customer", + { + "is_internal_customer": 1, + "represents_company": self.represents_company, + "name": ("!=", self.name), + }, + "name", + ) if internal_customer: - frappe.throw(_("Internal Customer for company {0} already exists").format( - frappe.bold(self.represents_company))) + frappe.throw( + _("Internal Customer for company {0} already exists").format( + frappe.bold(self.represents_company) + ) + ) def on_update(self): self.validate_name_with_customer_group() @@ -149,21 +173,22 @@ class Customer(TransactionBase): def update_customer_groups(self): ignore_doctypes = ["Lead", "Opportunity", "POS Profile", "Tax Rule", "Pricing Rule"] if frappe.flags.customer_group_changed: - update_linked_doctypes('Customer', self.name, 'Customer Group', - self.customer_group, ignore_doctypes) + update_linked_doctypes( + "Customer", self.name, "Customer Group", self.customer_group, ignore_doctypes + ) def create_primary_contact(self): if not self.customer_primary_contact and not self.lead_name: if self.mobile_no or self.email_id: contact = make_contact(self) - self.db_set('customer_primary_contact', contact.name) - self.db_set('mobile_no', self.mobile_no) - self.db_set('email_id', self.email_id) + self.db_set("customer_primary_contact", contact.name) + self.db_set("mobile_no", self.mobile_no) + self.db_set("email_id", self.email_id) def create_primary_address(self): from frappe.contacts.doctype.address.address import get_address_display - if self.flags.is_new_doc and self.get('address_line1'): + if self.flags.is_new_doc and self.get("address_line1"): address = make_address(self) address_display = get_address_display(address.name) @@ -171,8 +196,8 @@ class Customer(TransactionBase): self.db_set("primary_address", address_display) def update_lead_status(self): - '''If Customer created from Lead, update lead status to "Converted" - update Customer link in Quotation, Opportunity''' + """If Customer created from Lead, update lead status to "Converted" + update Customer link in Quotation, Opportunity""" if self.lead_name: frappe.db.set_value("Lead", self.lead_name, "status", "Converted") @@ -191,23 +216,36 @@ class Customer(TransactionBase): for row in linked_contacts_and_addresses: linked_doc = frappe.get_doc(row.doctype, row.name) - if not linked_doc.has_link('Customer', self.name): - linked_doc.append('links', dict(link_doctype='Customer', link_name=self.name)) + if not linked_doc.has_link("Customer", self.name): + linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name)) linked_doc.save(ignore_permissions=self.flags.ignore_permissions) - def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): - frappe.throw(_("A Customer Group exists with same name please change the Customer name or rename the Customer Group"), frappe.NameError) + frappe.throw( + _( + "A Customer Group exists with same name please change the Customer name or rename the Customer Group" + ), + frappe.NameError, + ) def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - past_credit_limits = [d.credit_limit - for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] + past_credit_limits = [ + d.credit_limit + for d in frappe.db.get_all( + "Customer Credit Limit", + filters={"parent": self.name}, + fields=["credit_limit"], + order_by="company", + ) + ] - current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] + current_credit_limits = [ + d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company) + ] if past_credit_limits == current_credit_limits: return @@ -215,7 +253,9 @@ class Customer(TransactionBase): company_record = [] for limit in self.credit_limits: if limit.company in company_record: - frappe.throw(_("Credit limit is already defined for the Company {0}").format(limit.company, self.name)) + frappe.throw( + _("Credit limit is already defined for the Company {0}").format(limit.company, self.name) + ) else: company_record.append(limit.company) @@ -223,11 +263,16 @@ class Customer(TransactionBase): self.name, limit.company, ignore_outstanding_sales_order=limit.bypass_credit_limit_check ) if flt(limit.credit_limit) < outstanding_amt: - frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt)) + frappe.throw( + _( + """New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""" + ).format(outstanding_amt) + ) def on_trash(self): if self.customer_primary_contact: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabCustomer` SET customer_primary_contact=null, @@ -235,14 +280,16 @@ class Customer(TransactionBase): mobile_no=null, email_id=null, primary_address=null - WHERE name=%(name)s""", {"name": self.name}) + WHERE name=%(name)s""", + {"name": self.name}, + ) - delete_contact_and_address('Customer', self.name) + delete_contact_and_address("Customer", self.name) if self.lead_name: frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name) def after_rename(self, olddn, newdn, merge=False): - if frappe.defaults.get_global_default('cust_master_name') == 'Customer Name': + if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": frappe.db.set(self, "customer_name", newdn) def set_loyalty_program(self): @@ -257,43 +304,49 @@ class Customer(TransactionBase): self.loyalty_program = loyalty_program[0] else: frappe.msgprint( - _("Multiple Loyalty Programs found for Customer {}. Please select manually.") - .format(frappe.bold(self.customer_name)) + _("Multiple Loyalty Programs found for Customer {}. Please select manually.").format( + frappe.bold(self.customer_name) + ) ) + def create_contact(contact, party_type, party, email): """Create contact based on given contact name""" - contact = contact.split(' ') + contact = contact.split(" ") - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': contact[0], - 'last_name': len(contact) > 1 and contact[1] or "" - }) - contact.append('email_ids', dict(email_id=email, is_primary=1)) - contact.append('links', dict(link_doctype=party_type, link_name=party)) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": contact[0], + "last_name": len(contact) > 1 and contact[1] or "", + } + ) + contact.append("email_ids", dict(email_id=email, is_primary=1)) + contact.append("links", dict(link_doctype=party_type, link_name=party)) contact.insert() + @frappe.whitelist() def make_quotation(source_name, target_doc=None): - def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, - {"Customer": { - "doctype": "Quotation", - "field_map": { - "name":"party_name" - } - }}, target_doc, set_missing_values) + target_doc = get_mapped_doc( + "Customer", + source_name, + {"Customer": {"doctype": "Quotation", "field_map": {"name": "party_name"}}}, + target_doc, + set_missing_values, + ) target_doc.quotation_to = "Customer" target_doc.run_method("set_missing_values") target_doc.run_method("set_other_charges") target_doc.run_method("calculate_taxes_and_totals") - price_list, currency = frappe.db.get_value("Customer", {'name': source_name}, ['default_price_list', 'default_currency']) + price_list, currency = frappe.db.get_value( + "Customer", {"name": source_name}, ["default_price_list", "default_currency"] + ) if price_list: target_doc.selling_price_list = price_list if currency: @@ -301,34 +354,53 @@ def make_quotation(source_name, target_doc=None): return target_doc + @frappe.whitelist() def make_opportunity(source_name, target_doc=None): def set_missing_values(source, target): _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, - {"Customer": { - "doctype": "Opportunity", - "field_map": { - "name": "party_name", - "doctype": "opportunity_from", + target_doc = get_mapped_doc( + "Customer", + source_name, + { + "Customer": { + "doctype": "Opportunity", + "field_map": { + "name": "party_name", + "doctype": "opportunity_from", + }, } - }}, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) return target_doc -def _set_missing_values(source, target): - address = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Address', - }, ['parent'], limit=1) - contact = frappe.get_all('Dynamic Link', { - 'link_doctype': source.doctype, - 'link_name': source.name, - 'parenttype': 'Contact', - }, ['parent'], limit=1) +def _set_missing_values(source, target): + address = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Address", + }, + ["parent"], + limit=1, + ) + + contact = frappe.get_all( + "Dynamic Link", + { + "link_doctype": source.doctype, + "link_name": source.name, + "parenttype": "Contact", + }, + ["parent"], + limit=1, + ) if address: target.customer_address = address[0].parent @@ -336,35 +408,41 @@ def _set_missing_values(source, target): if contact: target.contact_person = contact[0].parent + @frappe.whitelist() def get_loyalty_programs(doc): - ''' returns applicable loyalty programs for a customer ''' + """returns applicable loyalty programs for a customer""" lp_details = [] - loyalty_programs = frappe.get_all("Loyalty Program", + loyalty_programs = frappe.get_all( + "Loyalty Program", fields=["name", "customer_group", "customer_territory"], - filters={"auto_opt_in": 1, "from_date": ["<=", today()], - "ifnull(to_date, '2500-01-01')": [">=", today()]}) + filters={ + "auto_opt_in": 1, + "from_date": ["<=", today()], + "ifnull(to_date, '2500-01-01')": [">=", today()], + }, + ) for loyalty_program in loyalty_programs: if ( - (not loyalty_program.customer_group - or doc.customer_group in get_nested_links( - "Customer Group", - loyalty_program.customer_group, - doc.flags.ignore_permissions - )) - and (not loyalty_program.customer_territory - or doc.territory in get_nested_links( - "Territory", - loyalty_program.customer_territory, - doc.flags.ignore_permissions - )) + not loyalty_program.customer_group + or doc.customer_group + in get_nested_links( + "Customer Group", loyalty_program.customer_group, doc.flags.ignore_permissions + ) + ) and ( + not loyalty_program.customer_territory + or doc.territory + in get_nested_links( + "Territory", loyalty_program.customer_territory, doc.flags.ignore_permissions + ) ): lp_details.append(loyalty_program.name) return lp_details + def get_nested_links(link_doctype, link_name, ignore_permissions=False): from frappe.desk.treeview import _get_children @@ -374,10 +452,12 @@ def get_nested_links(link_doctype, link_name, ignore_permissions=False): return links + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): from erpnext.controllers.queries import get_fields + fields = ["name", "customer_name", "customer_group", "territory"] if frappe.db.get_default("cust_master_name") == "Customer Name": @@ -392,7 +472,8 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): filter_conditions = get_filters_cond(doctype, filters, []) match_conditions += "{}".format(filter_conditions) - return frappe.db.sql(""" + return frappe.db.sql( + """ select %s from `tabCustomer` where docstatus < 2 @@ -402,8 +483,12 @@ def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): case when name like %s then 0 else 1 end, case when customer_name like %s then 0 else 1 end, name, customer_name limit %s, %s - """.format(match_conditions=match_conditions) % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), - ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len)) + """.format( + match_conditions=match_conditions + ) + % (", ".join(fields), searchfield, "%s", "%s", "%s", "%s", "%s", "%s"), + ("%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, "%%%s%%" % txt, start, page_len), + ) def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, extra_amount=0): @@ -416,63 +501,87 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, customer_outstanding += flt(extra_amount) if credit_limit > 0 and flt(customer_outstanding) > credit_limit: - msgprint(_("Credit limit has been crossed for customer {0} ({1}/{2})") - .format(customer, customer_outstanding, credit_limit)) + msgprint( + _("Credit limit has been crossed for customer {0} ({1}/{2})").format( + customer, customer_outstanding, credit_limit + ) + ) # If not authorized person raise exception - credit_controller_role = frappe.db.get_single_value('Accounts Settings', 'credit_controller') + credit_controller_role = frappe.db.get_single_value("Accounts Settings", "credit_controller") if not credit_controller_role or credit_controller_role not in frappe.get_roles(): # form a list of emails for the credit controller users credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + credit_controller_users_formatted = [ + get_formatted_email(user).replace("<", "(").replace(">", ")") + for user in credit_controller_users + ] if not credit_controller_users_formatted: - frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer)) + frappe.throw( + _("Please contact your administrator to extend the credit limits for {0}.").format(customer) + ) message = """Please contact any of the following users to extend the credit limits for {0}: -

    • {1}
    """.format(customer, '
  • '.join(credit_controller_users_formatted)) +

    • {1}
    """.format( + customer, "
  • ".join(credit_controller_users_formatted) + ) # if the current user does not have permissions to override credit limit, # prompt them to send out an email to the controller users - frappe.msgprint(message, + frappe.msgprint( + message, title="Notify", raise_exception=1, primary_action={ - 'label': 'Send Email', - 'server_action': 'erpnext.selling.doctype.customer.customer.send_emails', - 'args': { - 'customer': customer, - 'customer_outstanding': customer_outstanding, - 'credit_limit': credit_limit, - 'credit_controller_users_list': credit_controller_users - } - } + "label": "Send Email", + "server_action": "erpnext.selling.doctype.customer.customer.send_emails", + "args": { + "customer": customer, + "customer_outstanding": customer_outstanding, + "credit_limit": credit_limit, + "credit_controller_users_list": credit_controller_users, + }, + }, ) + @frappe.whitelist() def send_emails(args): args = json.loads(args) - subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) - message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") - .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) - frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message) + subject = _("Credit limit reached for customer {0}").format(args.get("customer")) + message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format( + args.get("customer"), args.get("customer_outstanding"), args.get("credit_limit") + ) + frappe.sendmail( + recipients=args.get("credit_controller_users_list"), subject=subject, message=message + ) -def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): + +def get_customer_outstanding( + customer, company, ignore_outstanding_sales_order=False, cost_center=None +): # Outstanding based on GL Entries cond = "" if cost_center: - lft, rgt = frappe.get_cached_value("Cost Center", - cost_center, ['lft', 'rgt']) + lft, rgt = frappe.get_cached_value("Cost Center", cost_center, ["lft", "rgt"]) cond = """ and cost_center in (select name from `tabCost Center` where - lft >= {0} and rgt <= {1})""".format(lft, rgt) + lft >= {0} and rgt <= {1})""".format( + lft, rgt + ) - outstanding_based_on_gle = frappe.db.sql(""" + outstanding_based_on_gle = frappe.db.sql( + """ select sum(debit) - sum(credit) from `tabGL Entry` where party_type = 'Customer' - and party = %s and company=%s {0}""".format(cond), (customer, company)) + and party = %s and company=%s {0}""".format( + cond + ), + (customer, company), + ) outstanding_based_on_gle = flt(outstanding_based_on_gle[0][0]) if outstanding_based_on_gle else 0 @@ -482,18 +591,22 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F # if credit limit check is bypassed at sales order level, # we should not consider outstanding Sales Orders, when customer credit balance report is run if not ignore_outstanding_sales_order: - outstanding_based_on_so = frappe.db.sql(""" + outstanding_based_on_so = frappe.db.sql( + """ select sum(base_grand_total*(100 - per_billed)/100) from `tabSales Order` where customer=%s and docstatus = 1 and company=%s - and per_billed < 100 and status != 'Closed'""", (customer, company)) + and per_billed < 100 and status != 'Closed'""", + (customer, company), + ) outstanding_based_on_so = flt(outstanding_based_on_so[0][0]) if outstanding_based_on_so else 0 # Outstanding based on Delivery Note, which are not created against Sales Order outstanding_based_on_dn = 0 - unmarked_delivery_note_items = frappe.db.sql("""select + unmarked_delivery_note_items = frappe.db.sql( + """select dn_item.name, dn_item.amount, dn.base_net_total, dn.base_grand_total from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where @@ -502,21 +615,24 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F and dn.docstatus = 1 and dn.status not in ('Closed', 'Stopped') and ifnull(dn_item.against_sales_order, '') = '' and ifnull(dn_item.against_sales_invoice, '') = '' - """, (customer, company), as_dict=True) + """, + (customer, company), + as_dict=True, + ) if not unmarked_delivery_note_items: return outstanding_based_on_gle + outstanding_based_on_so - si_amounts = frappe.db.sql(""" + si_amounts = frappe.db.sql( + """ SELECT dn_detail, sum(amount) from `tabSales Invoice Item` WHERE docstatus = 1 and dn_detail in ({}) - GROUP BY dn_detail""".format(", ".join( - frappe.db.escape(dn_item.name) - for dn_item in unmarked_delivery_note_items - )) + GROUP BY dn_detail""".format( + ", ".join(frappe.db.escape(dn_item.name) for dn_item in unmarked_delivery_note_items) + ) ) si_amounts = {si_item[0]: si_item[1] for si_item in si_amounts} @@ -526,8 +642,9 @@ def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=F si_amount = flt(si_amounts.get(dn_item.name)) if dn_amount > si_amount and dn_item.base_net_total: - outstanding_based_on_dn += ((dn_amount - si_amount) - / dn_item.base_net_total) * dn_item.base_grand_total + outstanding_based_on_dn += ( + (dn_amount - si_amount) / dn_item.base_net_total + ) * dn_item.base_grand_total return outstanding_based_on_gle + outstanding_based_on_so + outstanding_based_on_dn @@ -536,75 +653,84 @@ def get_credit_limit(customer, company): credit_limit = None if customer: - credit_limit = frappe.db.get_value("Customer Credit Limit", - {'parent': customer, 'parenttype': 'Customer', 'company': company}, 'credit_limit') + credit_limit = frappe.db.get_value( + "Customer Credit Limit", + {"parent": customer, "parenttype": "Customer", "company": company}, + "credit_limit", + ) if not credit_limit: - customer_group = frappe.get_cached_value("Customer", customer, 'customer_group') - credit_limit = frappe.db.get_value("Customer Credit Limit", - {'parent': customer_group, 'parenttype': 'Customer Group', 'company': company}, 'credit_limit') + customer_group = frappe.get_cached_value("Customer", customer, "customer_group") + credit_limit = frappe.db.get_value( + "Customer Credit Limit", + {"parent": customer_group, "parenttype": "Customer Group", "company": company}, + "credit_limit", + ) if not credit_limit: - credit_limit = frappe.get_cached_value('Company', company, "credit_limit") + credit_limit = frappe.get_cached_value("Company", company, "credit_limit") return flt(credit_limit) + def make_contact(args, is_primary_contact=1): - contact = frappe.get_doc({ - 'doctype': 'Contact', - 'first_name': args.get('name'), - 'is_primary_contact': is_primary_contact, - 'links': [{ - 'link_doctype': args.get('doctype'), - 'link_name': args.get('name') - }] - }) - if args.get('email_id'): - contact.add_email(args.get('email_id'), is_primary=True) - if args.get('mobile_no'): - contact.add_phone(args.get('mobile_no'), is_primary_mobile_no=True) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": args.get("name"), + "is_primary_contact": is_primary_contact, + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + ) + if args.get("email_id"): + contact.add_email(args.get("email_id"), is_primary=True) + if args.get("mobile_no"): + contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True) contact.insert() return contact + def make_address(args, is_primary_address=1): reqd_fields = [] - for field in ['city', 'country']: + for field in ["city", "country"]: if not args.get(field): - reqd_fields.append( '
  • ' + field.title() + '
  • ') + reqd_fields.append("
  • " + field.title() + "
  • ") if reqd_fields: msg = _("Following fields are mandatory to create address:") - frappe.throw("{0}

      {1}
    ".format(msg, '\n'.join(reqd_fields)), - title = _("Missing Values Required")) + frappe.throw( + "{0}

      {1}
    ".format(msg, "\n".join(reqd_fields)), + title=_("Missing Values Required"), + ) - address = frappe.get_doc({ - 'doctype': 'Address', - 'address_title': args.get('name'), - 'address_line1': args.get('address_line1'), - 'address_line2': args.get('address_line2'), - 'city': args.get('city'), - 'state': args.get('state'), - 'pincode': args.get('pincode'), - 'country': args.get('country'), - 'links': [{ - 'link_doctype': args.get('doctype'), - 'link_name': args.get('name') - }] - }).insert() + address = frappe.get_doc( + { + "doctype": "Address", + "address_title": args.get("name"), + "address_line1": args.get("address_line1"), + "address_line2": args.get("address_line2"), + "city": args.get("city"), + "state": args.get("state"), + "pincode": args.get("pincode"), + "country": args.get("country"), + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + ).insert() return address + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): - customer = filters.get('customer') - return frappe.db.sql(""" + customer = filters.get("customer") + return frappe.db.sql( + """ select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabContact`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s and `tabDynamic Link`.link_doctype = 'Customer' and `tabContact`.name like %(txt)s - """, { - 'customer': customer, - 'txt': '%%%s%%' % txt - }) + """, + {"customer": customer, "txt": "%%%s%%" % txt}, + ) diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index faf8a4403ca..1b2296381e8 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -1,50 +1,31 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Customer. See timeline below for details'), - 'fieldname': 'customer', - 'non_standard_fieldnames': { - 'Payment Entry': 'party', - 'Quotation': 'party_name', - 'Opportunity': 'party_name', - 'Bank Account': 'party', - 'Subscription': 'party' + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Customer. See timeline below for details" + ), + "fieldname": "customer", + "non_standard_fieldnames": { + "Payment Entry": "party", + "Quotation": "party_name", + "Opportunity": "party_name", + "Bank Account": "party", + "Subscription": "party", }, - 'dynamic_links': { - 'party_name': ['Customer', 'quotation_to'] - }, - 'transactions': [ + "dynamic_links": {"party_name": ["Customer", "quotation_to"]}, + "transactions": [ + {"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]}, + {"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Payments"), "items": ["Payment Entry", "Bank Account"]}, { - 'label': _('Pre Sales'), - 'items': ['Opportunity', 'Quotation'] + "label": _("Support"), + "items": ["Issue", "Maintenance Visit", "Installation Note", "Warranty Claim"], }, - { - 'label': _('Orders'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Payments'), - 'items': ['Payment Entry', 'Bank Account'] - }, - { - 'label': _('Support'), - 'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - }, - { - 'label': _('Pricing'), - 'items': ['Pricing Rule'] - }, - { - 'label': _('Subscriptions'), - 'items': ['Subscription'] - } - ] + {"label": _("Projects"), "items": ["Project"]}, + {"label": _("Pricing"), "items": ["Pricing Rule"]}, + {"label": _("Subscriptions"), "items": ["Subscription"]}, + ], } diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 3da38a34522..4979b8f976a 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -13,19 +13,19 @@ from erpnext.selling.doctype.customer.customer import get_credit_limit, get_cust from erpnext.tests.utils import create_test_contact_and_address test_ignore = ["Price List"] -test_dependencies = ['Payment Term', 'Payment Terms Template'] -test_records = frappe.get_test_records('Customer') +test_dependencies = ["Payment Term", "Payment Terms Template"] +test_records = frappe.get_test_records("Customer") from six import iteritems class TestCustomer(FrappeTestCase): def setUp(self): - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') + if not frappe.get_value("Item", "_Test Item"): + make_test_records("Item") def tearDown(self): - set_credit_limit('_Test Customer', '_Test Company', 0) + set_credit_limit("_Test Customer", "_Test Company", 0) def test_get_customer_group_details(self): doc = frappe.new_doc("Customer Group") @@ -38,10 +38,7 @@ class TestCustomer(FrappeTestCase): "company": "_Test Company", "account": "Creditors - _TC", } - test_credit_limits = { - "company": "_Test Company", - "credit_limit": 350000 - } + test_credit_limits = {"company": "_Test Company", "credit_limit": 350000} doc.append("accounts", test_account_details) doc.append("credit_limits", test_credit_limits) doc.insert() @@ -50,7 +47,7 @@ class TestCustomer(FrappeTestCase): c_doc.customer_name = "Testing Customer" c_doc.customer_group = "_Testing Customer Group" c_doc.payment_terms = c_doc.default_price_list = "" - c_doc.accounts = c_doc.credit_limits= [] + c_doc.accounts = c_doc.credit_limits = [] c_doc.insert() c_doc.get_customer_group_details() self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") @@ -67,25 +64,26 @@ class TestCustomer(FrappeTestCase): from erpnext.accounts.party import get_party_details to_check = { - 'selling_price_list': None, - 'customer_group': '_Test Customer Group', - 'contact_designation': None, - 'customer_address': '_Test Address for Customer-Office', - 'contact_department': None, - 'contact_email': 'test_contact_customer@example.com', - 'contact_mobile': None, - 'sales_team': [], - 'contact_display': '_Test Contact for _Test Customer', - 'contact_person': '_Test Contact for _Test Customer-_Test Customer', - 'territory': u'_Test Territory', - 'contact_phone': '+91 0000000000', - 'customer_name': '_Test Customer' + "selling_price_list": None, + "customer_group": "_Test Customer Group", + "contact_designation": None, + "customer_address": "_Test Address for Customer-Office", + "contact_department": None, + "contact_email": "test_contact_customer@example.com", + "contact_mobile": None, + "sales_team": [], + "contact_display": "_Test Contact for _Test Customer", + "contact_person": "_Test Contact for _Test Customer-_Test Customer", + "territory": "_Test Territory", + "contact_phone": "+91 0000000000", + "customer_name": "_Test Customer", } create_test_contact_and_address() - frappe.db.set_value("Contact", "_Test Contact for _Test Customer-_Test Customer", - "is_primary_contact", 1) + frappe.db.set_value( + "Contact", "_Test Contact for _Test Customer-_Test Customer", "is_primary_contact", 1 + ) details = get_party_details("_Test Customer") @@ -106,32 +104,30 @@ class TestCustomer(FrappeTestCase): details = get_party_details("_Test Customer With Tax Category") self.assertEqual(details.tax_category, "_Test Tax Category 1") - billing_address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 2', - address_type='Billing', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Customer', - link_name='_Test Customer With Tax Category' - )] - )).insert() - shipping_address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 3', - address_type='Shipping', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Customer', - link_name='_Test Customer With Tax Category' - )] - )).insert() + billing_address = frappe.get_doc( + dict( + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 2", + address_type="Billing", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Customer", link_name="_Test Customer With Tax Category")], + ) + ).insert() + shipping_address = frappe.get_doc( + dict( + doctype="Address", + address_title="_Test Address With Tax Category", + tax_category="_Test Tax Category 3", + address_type="Shipping", + address_line1="Station Road", + city="_Test City", + country="India", + links=[dict(link_doctype="Customer", link_name="_Test Customer With Tax Category")], + ) + ).insert() settings = frappe.get_single("Accounts Settings") rollback_setting = settings.determine_address_tax_category_from @@ -159,12 +155,16 @@ class TestCustomer(FrappeTestCase): new_name = "_Test Customer 1 Renamed" for name in ("_Test Customer 1", new_name): - frappe.db.sql("""delete from `tabComment` + frappe.db.sql( + """delete from `tabComment` where reference_doctype=%s and reference_name=%s""", - ("Customer", name)) + ("Customer", name), + ) # add comments - comment = frappe.get_doc("Customer", "_Test Customer 1").add_comment("Comment", "Test Comment for Rename") + comment = frappe.get_doc("Customer", "_Test Customer 1").add_comment( + "Comment", "Test Comment for Rename" + ) # rename frappe.rename_doc("Customer", "_Test Customer 1", new_name) @@ -174,11 +174,17 @@ class TestCustomer(FrappeTestCase): self.assertFalse(frappe.db.exists("Customer", "_Test Customer 1")) # test that comment gets linked to renamed doc - self.assertEqual(frappe.db.get_value("Comment", { - "reference_doctype": "Customer", - "reference_name": new_name, - "content": "Test Comment for Rename" - }), comment.name) + self.assertEqual( + frappe.db.get_value( + "Comment", + { + "reference_doctype": "Customer", + "reference_name": new_name, + "content": "Test Comment for Rename", + }, + ), + comment.name, + ) # rename back to original frappe.rename_doc("Customer", new_name, "_Test Customer 1") @@ -192,7 +198,7 @@ class TestCustomer(FrappeTestCase): from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order - so = make_sales_order(do_not_save= True) + so = make_sales_order(do_not_save=True) self.assertRaises(PartyFrozen, so.save) @@ -201,13 +207,14 @@ class TestCustomer(FrappeTestCase): so.save() def test_delete_customer_contact(self): - customer = frappe.get_doc( - get_customer_dict('_Test Customer for delete')).insert(ignore_permissions=True) + customer = frappe.get_doc(get_customer_dict("_Test Customer for delete")).insert( + ignore_permissions=True + ) customer.mobile_no = "8989889890" customer.save() self.assertTrue(customer.customer_primary_contact) - frappe.delete_doc('Customer', customer.name) + frappe.delete_doc("Customer", customer.name) def test_disabled_customer(self): make_test_records("Item") @@ -228,13 +235,15 @@ class TestCustomer(FrappeTestCase): frappe.db.sql("delete from `tabCustomer` where customer_name='_Test Customer 1'") if not frappe.db.get_value("Customer", "_Test Customer 1"): - test_customer_1 = frappe.get_doc( - get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) + test_customer_1 = frappe.get_doc(get_customer_dict("_Test Customer 1")).insert( + ignore_permissions=True + ) else: test_customer_1 = frappe.get_doc("Customer", "_Test Customer 1") - duplicate_customer = frappe.get_doc( - get_customer_dict('_Test Customer 1')).insert(ignore_permissions=True) + duplicate_customer = frappe.get_doc(get_customer_dict("_Test Customer 1")).insert( + ignore_permissions=True + ) self.assertEqual("_Test Customer 1", test_customer_1.name) self.assertEqual("_Test Customer 1 - 1", duplicate_customer.name) @@ -242,15 +251,16 @@ class TestCustomer(FrappeTestCase): def get_customer_outstanding_amount(self): from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order - outstanding_amt = get_customer_outstanding('_Test Customer', '_Test Company') + + outstanding_amt = get_customer_outstanding("_Test Customer", "_Test Company") # If outstanding is negative make a transaction to get positive outstanding amount if outstanding_amt > 0.0: return outstanding_amt - item_qty = int((abs(outstanding_amt) + 200)/100) + item_qty = int((abs(outstanding_amt) + 200) / 100) make_sales_order(qty=item_qty) - return get_customer_outstanding('_Test Customer', '_Test Company') + return get_customer_outstanding("_Test Customer", "_Test Company") def test_customer_credit_limit(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -259,14 +269,14 @@ class TestCustomer(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note outstanding_amt = self.get_customer_outstanding_amount() - credit_limit = get_credit_limit('_Test Customer', '_Test Company') + credit_limit = get_credit_limit("_Test Customer", "_Test Company") if outstanding_amt <= 0.0: - item_qty = int((abs(outstanding_amt) + 200)/100) + item_qty = int((abs(outstanding_amt) + 200) / 100) make_sales_order(qty=item_qty) if not credit_limit: - set_credit_limit('_Test Customer', '_Test Company', outstanding_amt - 50) + set_credit_limit("_Test Customer", "_Test Company", outstanding_amt - 50) # Sales Order so = make_sales_order(do_not_submit=True) @@ -281,7 +291,7 @@ class TestCustomer(FrappeTestCase): self.assertRaises(frappe.ValidationError, si.submit) if credit_limit > outstanding_amt: - set_credit_limit('_Test Customer', '_Test Company', credit_limit) + set_credit_limit("_Test Customer", "_Test Company", credit_limit) # Makes Sales invoice from Sales Order so.save(ignore_permissions=True) @@ -291,16 +301,21 @@ class TestCustomer(FrappeTestCase): def test_customer_credit_limit_on_change(self): outstanding_amt = self.get_customer_outstanding_amount() - customer = frappe.get_doc("Customer", '_Test Customer') - customer.append('credit_limits', {'credit_limit': flt(outstanding_amt - 100), 'company': '_Test Company'}) + customer = frappe.get_doc("Customer", "_Test Customer") + customer.append( + "credit_limits", {"credit_limit": flt(outstanding_amt - 100), "company": "_Test Company"} + ) - ''' define new credit limit for same company ''' - customer.append('credit_limits', {'credit_limit': flt(outstanding_amt - 100), 'company': '_Test Company'}) + """ define new credit limit for same company """ + customer.append( + "credit_limits", {"credit_limit": flt(outstanding_amt - 100), "company": "_Test Company"} + ) self.assertRaises(frappe.ValidationError, customer.save) def test_customer_payment_terms(self): frappe.db.set_value( - "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3") + "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 3" + ) due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template") self.assertEqual(due_date, "2016-02-21") @@ -309,7 +324,8 @@ class TestCustomer(FrappeTestCase): self.assertEqual(due_date, "2017-02-21") frappe.db.set_value( - "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 1") + "Customer", "_Test Customer With Template", "payment_terms", "_Test Payment Term Template 1" + ) due_date = get_due_date("2016-01-22", "Customer", "_Test Customer With Template") self.assertEqual(due_date, "2016-02-29") @@ -329,13 +345,14 @@ class TestCustomer(FrappeTestCase): def get_customer_dict(customer_name): return { - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "doctype": "Customer", - "territory": "_Test Territory" + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", } + def set_credit_limit(customer, company, credit_limit): customer = frappe.get_doc("Customer", customer) existing_row = None @@ -347,31 +364,29 @@ def set_credit_limit(customer, company, credit_limit): break if not existing_row: - customer.append('credit_limits', { - 'company': company, - 'credit_limit': credit_limit - }) + customer.append("credit_limits", {"company": company, "credit_limit": credit_limit}) customer.credit_limits[-1].db_insert() + def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Customer", customer_name): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_group": "_Test Customer Group", - "customer_name": customer_name, - "customer_type": "Individual", - "territory": "_Test Territory", - "is_internal_customer": 1, - "represents_company": represents_company - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company, + } + ) - customer.append("companies", { - "company": allowed_to_interact_with - }) + customer.append("companies", {"company": allowed_to_interact_with}) customer.insert() customer_name = customer.name else: customer_name = frappe.db.get_value("Customer", customer_name) - return customer_name \ No newline at end of file + return customer_name diff --git a/erpnext/selling/doctype/industry_type/test_industry_type.py b/erpnext/selling/doctype/industry_type/test_industry_type.py index 250c2bec485..eb5f905f104 100644 --- a/erpnext/selling/doctype/industry_type/test_industry_type.py +++ b/erpnext/selling/doctype/industry_type/test_industry_type.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Industry Type') +test_records = frappe.get_test_records("Industry Type") diff --git a/erpnext/selling/doctype/installation_note/installation_note.py b/erpnext/selling/doctype/installation_note/installation_note.py index 36acdbea612..dd0b1e87517 100644 --- a/erpnext/selling/doctype/installation_note/installation_note.py +++ b/erpnext/selling/doctype/installation_note/installation_note.py @@ -13,26 +13,29 @@ from erpnext.utilities.transaction_base import TransactionBase class InstallationNote(TransactionBase): def __init__(self, *args, **kwargs): super(InstallationNote, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Installation Note Item', - 'target_dt': 'Delivery Note Item', - 'target_field': 'installed_qty', - 'target_ref_field': 'qty', - 'join_field': 'prevdoc_detail_docname', - 'target_parent_dt': 'Delivery Note', - 'target_parent_field': 'per_installed', - 'source_field': 'qty', - 'percent_join_field': 'prevdoc_docname', - 'status_field': 'installation_status', - 'keyword': 'Installed', - 'overflow_type': 'installation' - }] + self.status_updater = [ + { + "source_dt": "Installation Note Item", + "target_dt": "Delivery Note Item", + "target_field": "installed_qty", + "target_ref_field": "qty", + "join_field": "prevdoc_detail_docname", + "target_parent_dt": "Delivery Note", + "target_parent_field": "per_installed", + "source_field": "qty", + "percent_join_field": "prevdoc_docname", + "status_field": "installation_status", + "keyword": "Installed", + "overflow_type": "installation", + } + ] def validate(self): self.validate_installation_date() self.check_item_table() from erpnext.controllers.selling_controller import set_default_income_account_for_item + set_default_income_account_for_item(self) def is_serial_no_added(self, item_code, serial_no): @@ -48,18 +51,19 @@ class InstallationNote(TransactionBase): frappe.throw(_("Serial No {0} does not exist").format(x)) def get_prevdoc_serial_no(self, prevdoc_detail_docname): - serial_nos = frappe.db.get_value("Delivery Note Item", - prevdoc_detail_docname, "serial_no") + serial_nos = frappe.db.get_value("Delivery Note Item", prevdoc_detail_docname, "serial_no") return get_valid_serial_nos(serial_nos) def is_serial_no_match(self, cur_s_no, prevdoc_s_no, prevdoc_docname): for sr in cur_s_no: if sr not in prevdoc_s_no: - frappe.throw(_("Serial No {0} does not belong to Delivery Note {1}").format(sr, prevdoc_docname)) + frappe.throw( + _("Serial No {0} does not belong to Delivery Note {1}").format(sr, prevdoc_docname) + ) def validate_serial_no(self): prevdoc_s_no, sr_list = [], [] - for d in self.get('items'): + for d in self.get("items"): self.is_serial_no_added(d.item_code, d.serial_no) if d.serial_no: sr_list = get_valid_serial_nos(d.serial_no, d.qty, d.item_code) @@ -69,26 +73,27 @@ class InstallationNote(TransactionBase): if prevdoc_s_no: self.is_serial_no_match(sr_list, prevdoc_s_no, d.prevdoc_docname) - def validate_installation_date(self): - for d in self.get('items'): + for d in self.get("items"): if d.prevdoc_docname: d_date = frappe.db.get_value("Delivery Note", d.prevdoc_docname, "posting_date") if d_date > getdate(self.inst_date): - frappe.throw(_("Installation date cannot be before delivery date for Item {0}").format(d.item_code)) + frappe.throw( + _("Installation date cannot be before delivery date for Item {0}").format(d.item_code) + ) def check_item_table(self): - if not(self.get('items')): + if not (self.get("items")): frappe.throw(_("Please pull items from Delivery Note")) def on_update(self): - frappe.db.set(self, 'status', 'Draft') + frappe.db.set(self, "status", "Draft") def on_submit(self): self.validate_serial_no() self.update_prevdoc_status() - frappe.db.set(self, 'status', 'Submitted') + frappe.db.set(self, "status", "Submitted") def on_cancel(self): self.update_prevdoc_status() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") diff --git a/erpnext/selling/doctype/installation_note/test_installation_note.py b/erpnext/selling/doctype/installation_note/test_installation_note.py index d3c8be53574..56e0fe160ab 100644 --- a/erpnext/selling/doctype/installation_note/test_installation_note.py +++ b/erpnext/selling/doctype/installation_note/test_installation_note.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Installation Note') + class TestInstallationNote(unittest.TestCase): pass diff --git a/erpnext/selling/doctype/party_specific_item/party_specific_item.py b/erpnext/selling/doctype/party_specific_item/party_specific_item.py index a408af56420..0aef7d362e4 100644 --- a/erpnext/selling/doctype/party_specific_item/party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/party_specific_item.py @@ -8,12 +8,14 @@ from frappe.model.document import Document class PartySpecificItem(Document): def validate(self): - exists = frappe.db.exists({ - 'doctype': 'Party Specific Item', - 'party_type': self.party_type, - 'party': self.party, - 'restrict_based_on': self.restrict_based_on, - 'based_on': self.based_on_value, - }) + exists = frappe.db.exists( + { + "doctype": "Party Specific Item", + "party_type": self.party_type, + "party": self.party, + "restrict_based_on": self.restrict_based_on, + "based_on": self.based_on_value, + } + ) if exists: frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type)) diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index 9b672b4b5d3..f98cbd7e9a3 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -6,16 +6,18 @@ from frappe.tests.utils import FrappeTestCase from erpnext.controllers.queries import item_query -test_dependencies = ['Item', 'Customer', 'Supplier'] +test_dependencies = ["Item", "Customer", "Supplier"] + def create_party_specific_item(**args): psi = frappe.new_doc("Party Specific Item") - psi.party_type = args.get('party_type') - psi.party = args.get('party') - psi.restrict_based_on = args.get('restrict_based_on') - psi.based_on_value = args.get('based_on_value') + psi.party_type = args.get("party_type") + psi.party = args.get("party") + psi.restrict_based_on = args.get("restrict_based_on") + psi.based_on_value = args.get("based_on_value") psi.insert() + class TestPartySpecificItem(FrappeTestCase): def setUp(self): self.customer = frappe.get_last_doc("Customer") @@ -23,15 +25,29 @@ class TestPartySpecificItem(FrappeTestCase): self.item = frappe.get_last_doc("Item") def test_item_query_for_customer(self): - create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name) - filters = {'is_sales_item': 1, 'customer': self.customer.name} - items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + create_party_specific_item( + party_type="Customer", + party=self.customer.name, + restrict_based_on="Item", + based_on_value=self.item.name, + ) + filters = {"is_sales_item": 1, "customer": self.customer.name} + items = item_query( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False + ) for item in items: self.assertEqual(item[0], self.item.name) def test_item_query_for_supplier(self): - create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group) - filters = {'supplier': self.supplier.name, 'is_purchase_item': 1} - items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False) + create_party_specific_item( + party_type="Supplier", + party=self.supplier.name, + restrict_based_on="Item Group", + based_on_value=self.item.item_group, + ) + filters = {"supplier": self.supplier.name, "is_purchase_item": 1} + items = item_query( + doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters=filters, as_dict=False + ) for item in items: self.assertEqual(item[2], self.item.item_group) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 2bb876e6d0f..575b956686a 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -16,11 +16,22 @@ class ProductBundle(Document): self.validate_main_item() self.validate_child_items() from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "uom", "qty") def on_trash(self): - linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice", - "Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"] + linked_doctypes = [ + "Delivery Note", + "Sales Invoice", + "POS Invoice", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + "Stock Reconciliation", + "Sales Order", + "Purchase Order", + "Material Request", + ] invoice_links = [] for doctype in linked_doctypes: @@ -29,15 +40,20 @@ class ProductBundle(Document): if doctype == "Stock Entry": item_doctype = doctype + " Detail" - invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"]) + invoices = frappe.db.get_all( + item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"] + ) for invoice in invoices: - invoice_links.append(get_link_to_form(doctype, invoice['parent'])) + invoice_links.append(get_link_to_form(doctype, invoice["parent"])) if len(invoice_links): frappe.throw( - "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle" - .format(", ".join(invoice_links)), title=_("Not Allowed")) + "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle".format( + ", ".join(invoice_links) + ), + title=_("Not Allowed"), + ) def validate_main_item(self): """Validates, main Item is not a stock item""" @@ -47,15 +63,22 @@ class ProductBundle(Document): def validate_child_items(self): for item in self.items: if frappe.db.exists("Product Bundle", item.item_code): - frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code))) + frappe.throw( + _( + "Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save" + ).format(item.idx, frappe.bold(item.item_code)) + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - return frappe.db.sql("""select name, item_name, description from tabItem + return frappe.db.sql( + """select name, item_name, description from tabItem where is_stock_item=0 and name not in (select name from `tabProduct Bundle`) - and %s like %s %s limit %s, %s""" % (searchfield, "%s", - get_match_cond(doctype),"%s", "%s"), - ("%%%s%%" % txt, start, page_len)) + and %s like %s %s limit %s, %s""" + % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"), + ("%%%s%%" % txt, start, page_len), + ) diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.py b/erpnext/selling/doctype/product_bundle/test_product_bundle.py index b966c62f66c..82fe892edf7 100644 --- a/erpnext/selling/doctype/product_bundle/test_product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/test_product_bundle.py @@ -1,19 +1,16 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt import frappe -test_records = frappe.get_test_records('Product Bundle') +test_records = frappe.get_test_records("Product Bundle") + def make_product_bundle(parent, items, qty=None): if frappe.db.exists("Product Bundle", parent): return frappe.get_doc("Product Bundle", parent) - product_bundle = frappe.get_doc({ - "doctype": "Product Bundle", - "new_item_code": parent - }) + product_bundle = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": parent}) for item in items: product_bundle.append("items", {"item_code": item, "qty": qty or 1}) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index bf87ba46fc8..7cd7456ee01 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -9,18 +9,17 @@ from frappe.utils import flt, getdate, nowdate from erpnext.controllers.selling_controller import SellingController -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class Quotation(SellingController): def set_indicator(self): - if self.docstatus==1: - self.indicator_color = 'blue' - self.indicator_title = 'Submitted' + if self.docstatus == 1: + self.indicator_color = "blue" + self.indicator_title = "Submitted" if self.valid_till and getdate(self.valid_till) < getdate(nowdate()): - self.indicator_color = 'gray' - self.indicator_title = 'Expired' + self.indicator_color = "gray" + self.indicator_title = "Expired" def validate(self): super(Quotation, self).validate() @@ -32,6 +31,7 @@ class Quotation(SellingController): self.with_items = 1 from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) def validate_valid_till(self): @@ -46,10 +46,12 @@ class Quotation(SellingController): frappe.get_doc("Lead", self.party_name).set_status(update=True) def set_customer_name(self): - if self.party_name and self.quotation_to == 'Customer': + if self.party_name and self.quotation_to == "Customer": self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name") - elif self.party_name and self.quotation_to == 'Lead': - lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"]) + elif self.party_name and self.quotation_to == "Lead": + lead_name, company_name = frappe.db.get_value( + "Lead", self.party_name, ["lead_name", "company_name"] + ) self.customer_name = company_name or lead_name def update_opportunity(self, status): @@ -70,21 +72,24 @@ class Quotation(SellingController): @frappe.whitelist() def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): if not self.has_sales_order(): - get_lost_reasons = frappe.get_list('Quotation Lost Reason', - fields = ["name"]) - lost_reasons_lst = [reason.get('name') for reason in get_lost_reasons] - frappe.db.set(self, 'status', 'Lost') + get_lost_reasons = frappe.get_list("Quotation Lost Reason", fields=["name"]) + lost_reasons_lst = [reason.get("name") for reason in get_lost_reasons] + frappe.db.set(self, "status", "Lost") if detailed_reason: - frappe.db.set(self, 'order_lost_reason', detailed_reason) + frappe.db.set(self, "order_lost_reason", detailed_reason) for reason in lost_reasons_list: - if reason.get('lost_reason') in lost_reasons_lst: - self.append('lost_reasons', reason) + if reason.get("lost_reason") in lost_reasons_lst: + self.append("lost_reasons", reason) else: - frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason')))) + frappe.throw( + _("Invalid lost reason {0}, please create a new lost reason").format( + frappe.bold(reason.get("lost_reason")) + ) + ) - self.update_opportunity('Lost') + self.update_opportunity("Lost") self.update_lead() self.save() @@ -93,11 +98,12 @@ class Quotation(SellingController): def on_submit(self): # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) - #update enquiry status - self.update_opportunity('Quotation') + # update enquiry status + self.update_opportunity("Quotation") self.update_lead() def on_cancel(self): @@ -105,14 +111,14 @@ class Quotation(SellingController): self.lost_reasons = [] super(Quotation, self).on_cancel() - #update enquiry status + # update enquiry status self.set_status(update=True) - self.update_opportunity('Open') + self.update_opportunity("Open") self.update_lead() - def print_other_charges(self,docname): + def print_other_charges(self, docname): print_lst = [] - for d in self.get('taxes'): + for d in self.get("taxes"): lst1 = [] lst1.append(d.description) lst1.append(d.total) @@ -122,25 +128,35 @@ class Quotation(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Quotations'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Quotations"), + } + ) return list_context + @frappe.whitelist() def make_sales_order(source_name, target_doc=None): - quotation = frappe.db.get_value("Quotation", source_name, ["transaction_date", "valid_till"], as_dict = 1) - if quotation.valid_till and (quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())): + quotation = frappe.db.get_value( + "Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1 + ) + if quotation.valid_till and ( + quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate()) + ): frappe.throw(_("Validity period of this quotation has ended.")) return _make_sales_order(source_name, target_doc) + def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) @@ -149,8 +165,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.customer = customer.name target.customer_name = customer.customer_name if source.referral_sales_partner: - target.sales_partner=source.referral_sales_partner - target.commission_rate=frappe.get_value('Sales Partner', source.referral_sales_partner, 'commission_rate') + target.sales_partner = source.referral_sales_partner + target.commission_rate = frappe.get_value( + "Sales Partner", source.referral_sales_partner, "commission_rate" + ) target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -163,39 +181,31 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.blanket_order = obj.blanket_order target.blanket_order_rate = obj.blanket_order_rate - doclist = get_mapped_doc("Quotation", source_name, { - "Quotation": { - "doctype": "Sales Order", - "validation": { - "docstatus": ["=", 1] - } - }, + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}}, "Quotation Item": { "doctype": "Sales Order Item", - "field_map": { - "parent": "prevdoc_docname" - }, - "postprocess": update_item + "field_map": {"parent": "prevdoc_docname"}, + "postprocess": update_item, }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - }, - "Payment Schedule": { - "doctype": "Payment Schedule", - "add_if_empty": True - } - }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, + "Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True}, + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) # postprocess: fetch shipping address, set missing values - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist + def set_expired_status(): # filter out submitted non expired quotations whose validity has been ended cond = "qo.docstatus = 1 and qo.status != 'Expired' and qo.valid_till < %s" @@ -210,15 +220,18 @@ def set_expired_status(): # if not exists any SO, set status as Expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""" - .format(cond=cond, so_against_quo=so_against_quo), - (nowdate()) - ) + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""".format( + cond=cond, so_against_quo=so_against_quo + ), + (nowdate()), + ) + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): return _make_sales_invoice(source_name, target_doc) + def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): customer = _make_customer(source_name, ignore_permissions) @@ -235,54 +248,52 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.cost_center = None target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) - doclist = get_mapped_doc("Quotation", source_name, { - "Quotation": { - "doctype": "Sales Invoice", - "validation": { - "docstatus": ["=", 1] - } - }, - "Quotation Item": { - "doctype": "Sales Invoice Item", - "postprocess": update_item - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } - }, target_doc, set_missing_values, ignore_permissions=ignore_permissions) + doclist = get_mapped_doc( + "Quotation", + source_name, + { + "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, + "Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, + }, + target_doc, + set_missing_values, + ignore_permissions=ignore_permissions, + ) - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist -def _make_customer(source_name, ignore_permissions=False): - quotation = frappe.db.get_value("Quotation", - source_name, ["order_type", "party_name", "customer_name"], as_dict=1) - if quotation and quotation.get('party_name'): +def _make_customer(source_name, ignore_permissions=False): + quotation = frappe.db.get_value( + "Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1 + ) + + if quotation and quotation.get("party_name"): if not frappe.db.exists("Customer", quotation.get("party_name")): lead_name = quotation.get("party_name") - customer_name = frappe.db.get_value("Customer", {"lead_name": lead_name}, - ["name", "customer_name"], as_dict=True) + customer_name = frappe.db.get_value( + "Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True + ) if not customer_name: from erpnext.crm.doctype.lead.lead import _make_customer + customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions) customer = frappe.get_doc(customer_doclist) customer.flags.ignore_permissions = ignore_permissions if quotation.get("party_name") == "Shopping Cart": - customer.customer_group = frappe.db.get_value("E Commerce Settings", None, - "default_customer_group") + customer.customer_group = frappe.db.get_value( + "E Commerce Settings", None, "default_customer_group" + ) try: customer.insert() return customer except frappe.NameError: - if frappe.defaults.get_global_default('cust_master_name') == "Customer Name": + if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": customer.run_method("autoname") customer.name += "-" + lead_name customer.insert() @@ -290,12 +301,14 @@ def _make_customer(source_name, ignore_permissions=False): else: raise except frappe.MandatoryError as e: - mandatory_fields = e.args[0].split(':')[1].split(',') + mandatory_fields = e.args[0].split(":")[1].split(",") mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] frappe.local.message_log = [] lead_link = frappe.utils.get_link_to_form("Lead", lead_name) - message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + message = ( + _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + ) message += "
    • " + "
    • ".join(mandatory_fields) + "
    " message += _("Please create Customer from Lead {0}.").format(lead_link) diff --git a/erpnext/selling/doctype/quotation/quotation_dashboard.py b/erpnext/selling/doctype/quotation/quotation_dashboard.py index 46de292cc4d..7bfa034c532 100644 --- a/erpnext/selling/doctype/quotation/quotation_dashboard.py +++ b/erpnext/selling/doctype/quotation/quotation_dashboard.py @@ -1,21 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'prevdoc_docname', - 'non_standard_fieldnames': { - 'Auto Repeat': 'reference_document', + "fieldname": "prevdoc_docname", + "non_standard_fieldnames": { + "Auto Repeat": "reference_document", }, - 'transactions': [ - { - 'label': _('Sales Order'), - 'items': ['Sales Order'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Sales Order"), "items": ["Sales Order"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index a749d9e1f1f..b44fa5e5516 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -11,7 +11,7 @@ test_dependencies = ["Product Bundle"] class TestQuotation(FrappeTestCase): def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) - self.assertFalse(quotation.get('payment_schedule')) + self.assertFalse(quotation.get("payment_schedule")) quotation.insert() @@ -28,7 +28,7 @@ class TestQuotation(FrappeTestCase): sales_order = make_sales_order(quotation.name) - self.assertTrue(sales_order.get('payment_schedule')) + self.assertTrue(sales_order.get("payment_schedule")) def test_make_sales_order_with_different_currency(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -80,9 +80,7 @@ class TestQuotation(FrappeTestCase): quotation = frappe.copy_doc(test_records[0]) quotation.transaction_date = nowdate() quotation.valid_till = add_months(quotation.transaction_date, 1) - quotation.update( - {"payment_terms_template": "_Test Payment Term Template"} - ) + quotation.update({"payment_terms_template": "_Test Payment Term Template"}) quotation.insert() self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) @@ -92,7 +90,9 @@ class TestQuotation(FrappeTestCase): self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.00) self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date) self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.00) - self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30)) + self.assertEqual( + quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30) + ) sales_order = make_sales_order(quotation.name) @@ -108,7 +108,7 @@ class TestQuotation(FrappeTestCase): sales_order.insert() # Remove any unknown taxes if applied - sales_order.set('taxes', []) + sales_order.set("taxes", []) sales_order.save() self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) @@ -137,11 +137,11 @@ class TestQuotation(FrappeTestCase): make_sales_invoice, ) - rate_with_margin = flt((1500*18.75)/100 + 1500) + rate_with_margin = flt((1500 * 18.75) / 100 + 1500) - test_records[0]['items'][0]['price_list_rate'] = 1500 - test_records[0]['items'][0]['margin_type'] = 'Percentage' - test_records[0]['items'][0]['margin_rate_or_amount'] = 18.75 + test_records[0]["items"][0]["price_list_rate"] = 1500 + test_records[0]["items"][0]["margin_type"] = "Percentage" + test_records[0]["items"][0]["margin_rate_or_amount"] = 18.75 quotation = frappe.copy_doc(test_records[0]) quotation.transaction_date = nowdate() @@ -174,11 +174,9 @@ class TestQuotation(FrappeTestCase): def test_create_two_quotations(self): from erpnext.stock.doctype.item.test_item import make_item - first_item = make_item("_Test Laptop", - {"is_stock_item": 1}) + first_item = make_item("_Test Laptop", {"is_stock_item": 1}) - second_item = make_item("_Test CPU", - {"is_stock_item": 1}) + second_item = make_item("_Test CPU", {"is_stock_item": 1}) qo_item1 = [ { @@ -187,7 +185,7 @@ class TestQuotation(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", } ] @@ -197,7 +195,7 @@ class TestQuotation(FrappeTestCase): "warehouse": "_Test Warehouse - _TC", "qty": 2, "rate": 300, - "conversion_factor": 1.0 + "conversion_factor": 1.0, } ] @@ -209,17 +207,12 @@ class TestQuotation(FrappeTestCase): def test_quotation_expiry(self): from erpnext.selling.doctype.quotation.quotation import set_expired_status - quotation_item = [ - { - "item_code": "_Test Item", - "warehouse":"", - "qty": 1, - "rate": 500 - } - ] + quotation_item = [{"item_code": "_Test Item", "warehouse": "", "qty": 1, "rate": 500}] yesterday = add_days(nowdate(), -1) - expired_quotation = make_quotation(item_list=quotation_item, transaction_date=yesterday, do_not_submit=True) + expired_quotation = make_quotation( + item_list=quotation_item, transaction_date=yesterday, do_not_submit=True + ) expired_quotation.valid_till = yesterday expired_quotation.save() expired_quotation.submit() @@ -236,24 +229,49 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) quotation = make_quotation(item_code="_Test Product Bundle", qty=1, rate=100) sales_order = make_sales_order(quotation.name) - quotation_item = [quotation.items[0].item_code, quotation.items[0].rate, quotation.items[0].qty, quotation.items[0].amount] - so_item = [sales_order.items[0].item_code, sales_order.items[0].rate, sales_order.items[0].qty, sales_order.items[0].amount] + quotation_item = [ + quotation.items[0].item_code, + quotation.items[0].rate, + quotation.items[0].qty, + quotation.items[0].amount, + ] + so_item = [ + sales_order.items[0].item_code, + sales_order.items[0].rate, + sales_order.items[0].qty, + sales_order.items[0].amount, + ] self.assertEqual(quotation_item, so_item) quotation_packed_items = [ - [quotation.packed_items[0].parent_item, quotation.packed_items[0].item_code, quotation.packed_items[0].qty], - [quotation.packed_items[1].parent_item, quotation.packed_items[1].item_code, quotation.packed_items[1].qty] + [ + quotation.packed_items[0].parent_item, + quotation.packed_items[0].item_code, + quotation.packed_items[0].qty, + ], + [ + quotation.packed_items[1].parent_item, + quotation.packed_items[1].item_code, + quotation.packed_items[1].qty, + ], ] so_packed_items = [ - [sales_order.packed_items[0].parent_item, sales_order.packed_items[0].item_code, sales_order.packed_items[0].qty], - [sales_order.packed_items[1].parent_item, sales_order.packed_items[1].item_code, sales_order.packed_items[1].qty] + [ + sales_order.packed_items[0].parent_item, + sales_order.packed_items[0].item_code, + sales_order.packed_items[0].qty, + ], + [ + sales_order.packed_items[1].parent_item, + sales_order.packed_items[1].item_code, + sales_order.packed_items[1].qty, + ], ] self.assertEqual(quotation_packed_items, so_packed_items) @@ -266,8 +284,7 @@ class TestQuotation(FrappeTestCase): bundle_item1 = make_item("_Test Bundle Item 1", {"is_stock_item": 1}) bundle_item2 = make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) bundle_item1.valuation_rate = 100 bundle_item1.save() @@ -286,8 +303,7 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) enable_calculate_bundle_price() @@ -301,7 +317,9 @@ class TestQuotation(FrappeTestCase): enable_calculate_bundle_price(enable=0) - def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self): + def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked( + self, + ): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.item.test_item import make_item @@ -311,10 +329,8 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 2", {"is_stock_item": 1}) make_item("_Test Bundle Item 3", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle 1", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) - make_product_bundle("_Test Product Bundle 2", - ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 1", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", ["_Test Bundle Item 2", "_Test Bundle Item 3"]) enable_calculate_bundle_price() @@ -325,7 +341,7 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 2", @@ -333,8 +349,8 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] quotation = make_quotation(item_list=item_list, do_not_submit=1) @@ -347,7 +363,7 @@ class TestQuotation(FrappeTestCase): expected_values = [300, 500] for item in quotation.items: - self.assertEqual(item.amount, expected_values[item.idx-1]) + self.assertEqual(item.amount, expected_values[item.idx - 1]) enable_calculate_bundle_price(enable=0) @@ -362,12 +378,9 @@ class TestQuotation(FrappeTestCase): make_item("_Test Bundle Item 2", {"is_stock_item": 1}) make_item("_Test Bundle Item 3", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle 1", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) - make_product_bundle("_Test Product Bundle 2", - ["_Test Bundle Item 2", "_Test Bundle Item 3"]) - make_product_bundle("_Test Product Bundle 3", - ["_Test Bundle Item 3", "_Test Bundle Item 1"]) + make_product_bundle("_Test Product Bundle 1", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle 2", ["_Test Bundle Item 2", "_Test Bundle Item 3"]) + make_product_bundle("_Test Product Bundle 3", ["_Test Bundle Item 3", "_Test Bundle Item 1"]) item_list = [ { @@ -376,7 +389,7 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 2", @@ -384,7 +397,7 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Product Bundle 3", @@ -392,8 +405,8 @@ class TestQuotation(FrappeTestCase): "qty": 1, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] quotation = make_quotation(item_list=item_list, do_not_submit=1) @@ -404,29 +417,26 @@ class TestQuotation(FrappeTestCase): expected_index = id + 1 self.assertEqual(item.idx, expected_index) -test_records = frappe.get_test_records('Quotation') + +test_records = frappe.get_test_records("Quotation") + def enable_calculate_bundle_price(enable=1): selling_settings = frappe.get_doc("Selling Settings") selling_settings.editable_bundle_item_rates = enable selling_settings.save() + def get_quotation_dict(party_name=None, item_code=None): if not party_name: - party_name = '_Test Customer' + party_name = "_Test Customer" if not item_code: - item_code = '_Test Item' + item_code = "_Test Item" return { - 'doctype': 'Quotation', - 'party_name': party_name, - 'items': [ - { - 'item_code': item_code, - 'qty': 1, - 'rate': 100 - } - ] + "doctype": "Quotation", + "party_name": party_name, + "items": [{"item_code": item_code, "qty": 1, "rate": 100}], } @@ -450,13 +460,16 @@ def make_quotation(**args): qo.append("items", item) else: - qo.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse, - "qty": args.qty or 10, - "uom": args.uom or None, - "rate": args.rate or 100 - }) + qo.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse, + "qty": args.qty or 10, + "uom": args.uom or None, + "rate": args.rate or 100, + }, + ) qo.delivery_date = add_days(qo.transaction_date, 10) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 7809a9330ed..3cb422c587d 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -28,11 +28,12 @@ from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + + +class WarehouseRequired(frappe.ValidationError): + pass -class WarehouseRequired(frappe.ValidationError): pass class SalesOrder(SellingController): def __init__(self, *args, **kwargs): @@ -49,20 +50,26 @@ class SalesOrder(SellingController): self.validate_warehouse() self.validate_drop_ship() self.validate_serial_no_based_delivery() - validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_order_reference) + validate_inter_company_party( + self.doctype, self.customer, self.company, self.inter_company_order_reference + ) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) self.validate_with_previous_doc() self.set_status() - if not self.billing_status: self.billing_status = 'Not Billed' - if not self.delivery_status: self.delivery_status = 'Not Delivered' + 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") @@ -71,55 +78,82 @@ class SalesOrder(SellingController): if self.po_date and not self.skip_delivery_note: for d in self.get("items"): if d.delivery_date and getdate(self.po_date) > getdate(d.delivery_date): - frappe.throw(_("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date") - .format(d.idx)) + frappe.throw( + _("Row #{0}: Expected Delivery Date cannot be before Purchase Order Date").format(d.idx) + ) if self.po_no and self.customer and not self.skip_delivery_note: - so = frappe.db.sql("select name from `tabSales Order` \ + so = frappe.db.sql( + "select name from `tabSales Order` \ where ifnull(po_no, '') = %s and name != %s and docstatus < 2\ - and customer = %s", (self.po_no, self.name, self.customer)) - if so and so[0][0] and not cint(frappe.db.get_single_value("Selling Settings", - "allow_against_multiple_purchase_orders")): - frappe.msgprint(_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(so[0][0], self.po_no)) + and customer = %s", + (self.po_no, self.name, self.customer), + ) + if ( + so + and so[0][0] + and not cint( + frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders") + ) + ): + frappe.msgprint( + _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( + so[0][0], self.po_no + ) + ) def validate_for_items(self): - for d in self.get('items'): + for d in self.get("items"): # used for production plan d.transaction_date = self.transaction_date - tot_avail_qty = frappe.db.sql("select projected_qty from `tabBin` \ - where item_code = %s and warehouse = %s", (d.item_code, d.warehouse)) + tot_avail_qty = frappe.db.sql( + "select projected_qty from `tabBin` \ + where item_code = %s and warehouse = %s", + (d.item_code, d.warehouse), + ) d.projected_qty = tot_avail_qty and flt(tot_avail_qty[0][0]) or 0 def product_bundle_has_stock_item(self, product_bundle): """Returns true if product bundle has stock item""" - ret = len(frappe.db.sql("""select i.name from tabItem i, `tabProduct Bundle Item` pbi - where pbi.parent = %s and pbi.item_code = i.name and i.is_stock_item = 1""", product_bundle)) + ret = len( + frappe.db.sql( + """select i.name from tabItem i, `tabProduct Bundle Item` pbi + where pbi.parent = %s and pbi.item_code = i.name and i.is_stock_item = 1""", + product_bundle, + ) + ) return ret def validate_sales_mntc_quotation(self): - for d in self.get('items'): + for d in self.get("items"): if d.prevdoc_docname: - res = frappe.db.sql("select name from `tabQuotation` where name=%s and order_type = %s", - (d.prevdoc_docname, self.order_type)) + res = frappe.db.sql( + "select name from `tabQuotation` where name=%s and order_type = %s", + (d.prevdoc_docname, self.order_type), + ) if not res: - frappe.msgprint(_("Quotation {0} not of type {1}") - .format(d.prevdoc_docname, self.order_type)) + frappe.msgprint(_("Quotation {0} not of type {1}").format(d.prevdoc_docname, self.order_type)) def validate_delivery_date(self): - if self.order_type == 'Sales' and not self.skip_delivery_note: + if self.order_type == "Sales" and not self.skip_delivery_note: delivery_date_list = [d.delivery_date for d in self.get("items") if d.delivery_date] max_delivery_date = max(delivery_date_list) if delivery_date_list else None - if (max_delivery_date and not self.delivery_date) or (max_delivery_date and getdate(self.delivery_date) != getdate(max_delivery_date)): + if (max_delivery_date and not self.delivery_date) or ( + max_delivery_date and getdate(self.delivery_date) != getdate(max_delivery_date) + ): self.delivery_date = max_delivery_date if self.delivery_date: for d in self.get("items"): if not d.delivery_date: d.delivery_date = self.delivery_date if getdate(self.transaction_date) > getdate(d.delivery_date): - frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"), - indicator='orange', title=_('Warning')) + frappe.msgprint( + _("Expected Delivery Date should be after Sales Order Date"), + indicator="orange", + title=_("Warning"), + ) else: frappe.throw(_("Please enter Delivery Date")) @@ -127,47 +161,56 @@ class SalesOrder(SellingController): def validate_proj_cust(self): if self.project and self.customer_name: - res = frappe.db.sql("""select name from `tabProject` where name = %s + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or ifnull(customer,'')='')""", - (self.project, self.customer)) + (self.project, self.customer), + ) if not res: - frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) + frappe.throw( + _("Customer {0} does not belong to project {1}").format(self.customer, self.project) + ) def validate_warehouse(self): super(SalesOrder, self).validate_warehouse() for d in self.get("items"): - if (frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 or - (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code))) \ - and not d.warehouse and not cint(d.delivered_by_supplier): - frappe.throw(_("Delivery warehouse required for stock item {0}").format(d.item_code), - WarehouseRequired) + if ( + ( + frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 + or (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code)) + ) + and not d.warehouse + and not cint(d.delivered_by_supplier) + ): + frappe.throw( + _("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired + ) def validate_with_previous_doc(self): - super(SalesOrder, self).validate_with_previous_doc({ - "Quotation": { - "ref_dn_field": "prevdoc_docname", - "compare_fields": [["company", "="]] - } - }) - + super(SalesOrder, self).validate_with_previous_doc( + {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} + ) def update_enquiry_status(self, prevdoc, flag): - enq = frappe.db.sql("select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", prevdoc) + enq = frappe.db.sql( + "select t2.prevdoc_docname from `tabQuotation` t1, `tabQuotation Item` t2 where t2.parent = t1.name and t1.name=%s", + prevdoc, + ) if enq: - frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0])) + frappe.db.sql("update `tabOpportunity` set status = %s where name=%s", (flag, enq[0][0])) def update_prevdoc_status(self, flag=None): for quotation in set(d.prevdoc_docname for d in self.get("items")): if quotation: doc = frappe.get_doc("Quotation", quotation) - if doc.docstatus==2: + if doc.docstatus == 2: frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) def validate_drop_ship(self): - for d in self.get('items'): + for d in self.get("items"): if d.delivered_by_supplier and not d.supplier: frappe.throw(_("Row #{0}: Set Supplier for item {1}").format(d.idx, d.item_code)) @@ -175,41 +218,47 @@ class SalesOrder(SellingController): self.check_credit_limit() self.update_reserved_qty() - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) self.update_project() - self.update_prevdoc_status('submit') + self.update_prevdoc_status("submit") self.update_blanket_order() update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'used') + + update_coupon_code_count(self.coupon_code, "used") def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") super(SalesOrder, self).on_cancel() # Cannot cancel closed SO - if self.status == 'Closed': + if self.status == "Closed": frappe.throw(_("Closed order cannot be cancelled. Unclose to cancel.")) self.check_nextdoc_docstatus() self.update_reserved_qty() self.update_project() - self.update_prevdoc_status('cancel') + self.update_prevdoc_status("cancel") - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") self.update_blanket_order() unlink_inter_company_doc(self.doctype, self.name, self.inter_company_order_reference) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'cancelled') + + update_coupon_code_count(self.coupon_code, "cancelled") def update_project(self): - if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') != "Each Transaction": + if ( + frappe.db.get_single_value("Selling Settings", "sales_update_frequency") != "Each Transaction" + ): return if self.project: @@ -220,71 +269,103 @@ class SalesOrder(SellingController): def check_credit_limit(self): # if bypass credit limit check is set to true (1) at sales order level, # then we need not to check credit limit and vise versa - if not cint(frappe.db.get_value("Customer Credit Limit", - {'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - "bypass_credit_limit_check")): + if not cint( + frappe.db.get_value( + "Customer Credit Limit", + {"parent": self.customer, "parenttype": "Customer", "company": self.company}, + "bypass_credit_limit_check", + ) + ): check_credit_limit(self.customer, self.company) def check_nextdoc_docstatus(self): # Checks Delivery Note - submit_dn = frappe.db.sql_list(""" + submit_dn = frappe.db.sql_list( + """ select t1.name from `tabDelivery Note` t1,`tabDelivery Note Item` t2 - where t1.name = t2.parent and t2.against_sales_order = %s and t1.docstatus = 1""", self.name) + where t1.name = t2.parent and t2.against_sales_order = %s and t1.docstatus = 1""", + self.name, + ) if submit_dn: submit_dn = [get_link_to_form("Delivery Note", dn) for dn in submit_dn] - frappe.throw(_("Delivery Notes {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_dn))) + frappe.throw( + _("Delivery Notes {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_dn) + ) + ) # Checks Sales Invoice - submit_rv = frappe.db.sql_list("""select t1.name + submit_rv = frappe.db.sql_list( + """select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""", - self.name) + self.name, + ) if submit_rv: submit_rv = [get_link_to_form("Sales Invoice", si) for si in submit_rv] - frappe.throw(_("Sales Invoice {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_rv))) + frappe.throw( + _("Sales Invoice {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_rv) + ) + ) - #check maintenance schedule - submit_ms = frappe.db.sql_list(""" + # check maintenance schedule + submit_ms = frappe.db.sql_list( + """ select t1.name from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2 - where t2.parent=t1.name and t2.sales_order = %s and t1.docstatus = 1""", self.name) + where t2.parent=t1.name and t2.sales_order = %s and t1.docstatus = 1""", + self.name, + ) if submit_ms: submit_ms = [get_link_to_form("Maintenance Schedule", ms) for ms in submit_ms] - frappe.throw(_("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_ms))) + frappe.throw( + _("Maintenance Schedule {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_ms) + ) + ) # check maintenance visit - submit_mv = frappe.db.sql_list(""" + submit_mv = frappe.db.sql_list( + """ select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 - where t2.parent=t1.name and t2.prevdoc_docname = %s and t1.docstatus = 1""",self.name) + where t2.parent=t1.name and t2.prevdoc_docname = %s and t1.docstatus = 1""", + self.name, + ) if submit_mv: submit_mv = [get_link_to_form("Maintenance Visit", mv) for mv in submit_mv] - frappe.throw(_("Maintenance Visit {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(submit_mv))) + frappe.throw( + _("Maintenance Visit {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(submit_mv) + ) + ) # check work order - pro_order = frappe.db.sql_list(""" + pro_order = frappe.db.sql_list( + """ select name from `tabWork Order` - where sales_order = %s and docstatus = 1""", self.name) + where sales_order = %s and docstatus = 1""", + self.name, + ) if pro_order: pro_order = [get_link_to_form("Work Order", po) for po in pro_order] - frappe.throw(_("Work Order {0} must be cancelled before cancelling this Sales Order") - .format(", ".join(pro_order))) + frappe.throw( + _("Work Order {0} must be cancelled before cancelling this Sales Order").format( + ", ".join(pro_order) + ) + ) def check_modified_date(self): mod_db = frappe.db.get_value("Sales Order", self.name, "modified") - date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % - ( mod_db, cstr(self.modified))) + date_diff = frappe.db.sql("select TIMEDIFF('%s', '%s')" % (mod_db, cstr(self.modified))) if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(self.doctype, self.name)) @@ -298,10 +379,15 @@ class SalesOrder(SellingController): def update_reserved_qty(self, so_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] + def _valid_for_reserve(item_code, warehouse): - if item_code and warehouse and [item_code, warehouse] not in item_wh_list \ - and frappe.get_cached_value("Item", item_code, "is_stock_item"): - item_wh_list.append([item_code, warehouse]) + if ( + item_code + and warehouse + and [item_code, warehouse] not in item_wh_list + and frappe.get_cached_value("Item", item_code, "is_stock_item") + ): + item_wh_list.append([item_code, warehouse]) for d in self.get("items"): if (not so_item_rows or d.name in so_item_rows) and not d.delivered_by_supplier: @@ -313,9 +399,7 @@ class SalesOrder(SellingController): _valid_for_reserve(d.item_code, d.warehouse) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, { - "reserved_qty": get_reserved_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"reserved_qty": get_reserved_qty(item_code, warehouse)}) def on_update(self): pass @@ -332,13 +416,18 @@ class SalesOrder(SellingController): for item in self.items: if item.supplier: - supplier = frappe.db.get_value("Sales Order Item", {"parent": self.name, "item_code": item.item_code}, - "supplier") + supplier = frappe.db.get_value( + "Sales Order Item", {"parent": self.name, "item_code": item.item_code}, "supplier" + ) if item.ordered_qty > 0.0 and item.supplier != supplier: - exc_list.append(_("Row #{0}: Not allowed to change Supplier as Purchase Order already exists").format(item.idx)) + exc_list.append( + _("Row #{0}: Not allowed to change Supplier as Purchase Order already exists").format( + item.idx + ) + ) if exc_list: - frappe.throw('\n'.join(exc_list)) + frappe.throw("\n".join(exc_list)) def update_delivery_status(self): """Update delivery status from Purchase Order for drop shipping""" @@ -346,13 +435,16 @@ class SalesOrder(SellingController): for item in self.items: if item.delivered_by_supplier: - item_delivered_qty = frappe.db.sql("""select sum(qty) + item_delivered_qty = frappe.db.sql( + """select sum(qty) from `tabPurchase Order Item` poi, `tabPurchase Order` po where poi.sales_order_item = %s and poi.item_code = %s and poi.parent = po.name and po.docstatus = 1 - and po.status = 'Delivered'""", (item.name, item.item_code)) + and po.status = 'Delivered'""", + (item.name, item.item_code), + ) item_delivered_qty = item_delivered_qty[0][0] if item_delivered_qty else 0 item.db_set("delivered_qty", flt(item_delivered_qty), update_modified=False) @@ -361,9 +453,7 @@ class SalesOrder(SellingController): tot_qty += item.qty if tot_qty != 0: - self.db_set("per_delivered", flt(delivered_qty/tot_qty) * 100, - update_modified=False) - + self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False) def set_indicator(self): """Set indicator for portal""" @@ -381,49 +471,62 @@ class SalesOrder(SellingController): @frappe.whitelist() def get_work_order_items(self, for_raw_material_request=0): - '''Returns items with BOM that already do not have a linked work order''' + """Returns items with BOM that already do not have a linked work order""" items = [] item_codes = [i.item_code for i in self.items] - product_bundle_parents = [pb.new_item_code for pb in frappe.get_all("Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"])] + product_bundle_parents = [ + pb.new_item_code + for pb in frappe.get_all( + "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] + ) + ] for table in [self.items, self.packed_items]: for i in table: bom = get_default_bom_item(i.item_code) - stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty + stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty if not for_raw_material_request: - total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order` - where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0]) + total_work_order_qty = flt( + frappe.db.sql( + """select sum(qty) from `tabWork Order` + where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""", + (i.item_code, self.name, i.name), + )[0][0] + ) pending_qty = stock_qty - total_work_order_qty else: pending_qty = stock_qty if pending_qty and i.item_code not in product_bundle_parents: if bom: - items.append(dict( - name= i.name, - item_code= i.item_code, - description= i.description, - bom = bom, - warehouse = i.warehouse, - pending_qty = pending_qty, - required_qty = pending_qty if for_raw_material_request else 0, - sales_order_item = i.name - )) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom, + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) else: - items.append(dict( - name= i.name, - item_code= i.item_code, - description= i.description, - bom = '', - warehouse = i.warehouse, - pending_qty = pending_qty, - required_qty = pending_qty if for_raw_material_request else 0, - sales_order_item = i.name - )) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom="", + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) return items def on_recurring(self, reference_doc, auto_repeat_doc): - def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) @@ -433,15 +536,26 @@ class SalesOrder(SellingController): return delivery_date - self.set("delivery_date", _get_delivery_date(reference_doc.delivery_date, - reference_doc.transaction_date, self.transaction_date )) + self.set( + "delivery_date", + _get_delivery_date( + reference_doc.delivery_date, reference_doc.transaction_date, self.transaction_date + ), + ) for d in self.get("items"): - reference_delivery_date = frappe.db.get_value("Sales Order Item", - {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, "delivery_date") + reference_delivery_date = frappe.db.get_value( + "Sales Order Item", + {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, + "delivery_date", + ) - d.set("delivery_date", _get_delivery_date(reference_delivery_date, - reference_doc.transaction_date, self.transaction_date)) + d.set( + "delivery_date", + _get_delivery_date( + reference_delivery_date, reference_doc.transaction_date, self.transaction_date + ), + ) def validate_serial_no_based_delivery(self): reserved_items = [] @@ -449,32 +563,52 @@ class SalesOrder(SellingController): for item in self.items: if item.ensure_delivery_based_on_produced_serial_no: if item.item_code in normal_items: - frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) + frappe.throw( + _( + "Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No." + ).format(item.item_code) + ) if item.item_code not in reserved_items: if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): - frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code)) + frappe.throw( + _( + "Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No" + ).format(item.item_code) + ) if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}): - frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code)) + frappe.throw( + _("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format( + item.item_code + ) + ) reserved_items.append(item.item_code) else: normal_items.append(item.item_code) - if not item.ensure_delivery_based_on_produced_serial_no and \ - item.item_code in reserved_items: - frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code)) + if not item.ensure_delivery_based_on_produced_serial_no and item.item_code in reserved_items: + frappe.throw( + _( + "Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No." + ).format(item.item_code) + ) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Orders'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Orders"), + } + ) return list_context + @frappe.whitelist() def close_or_unclose_sales_orders(names, status): if not frappe.has_permission("Sales Order", "write"): @@ -485,23 +619,32 @@ def close_or_unclose_sales_orders(names, status): so = frappe.get_doc("Sales Order", name) if so.docstatus == 1: if status == "Closed": - if so.status not in ("Cancelled", "Closed") and (so.per_delivered < 100 or so.per_billed < 100): + if so.status not in ("Cancelled", "Closed") and ( + so.per_delivered < 100 or so.per_billed < 100 + ): so.update_status(status) else: if so.status == "Closed": - so.update_status('Draft') + so.update_status("Draft") so.update_blanket_order() frappe.local.message_log = [] + def get_requested_item_qty(sales_order): - return frappe._dict(frappe.db.sql(""" + return frappe._dict( + frappe.db.sql( + """ select sales_order_item, sum(qty) from `tabMaterial Request Item` where docstatus = 1 and sales_order = %s group by sales_order_item - """, sales_order)) + """, + sales_order, + ) + ) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): @@ -514,55 +657,56 @@ def make_material_request(source_name, target_doc=None): target.qty = qty - requested_item_qty.get(source.name, 0) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Material Request", - "validation": { - "docstatus": ["=", 1] - } - }, - "Packed Item": { - "doctype": "Material Request Item", - "field_map": { - "parent": "sales_order", - "uom": "stock_uom" + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Material Request", "validation": {"docstatus": ["=", 1]}}, + "Packed Item": { + "doctype": "Material Request Item", + "field_map": {"parent": "sales_order", "uom": "stock_uom"}, + "postprocess": update_item, }, - "postprocess": update_item - }, - "Sales Order Item": { - "doctype": "Material Request Item", - "field_map": { - "name": "sales_order_item", - "parent": "sales_order" + "Sales Order Item": { + "doctype": "Material Request Item", + "field_map": {"name": "sales_order_item", "parent": "sales_order"}, + "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) + and doc.stock_qty > requested_item_qty.get(doc.name, 0), + "postprocess": update_item, }, - "condition": lambda doc: not frappe.db.exists('Product Bundle', doc.item_code) and doc.stock_qty > requested_item_qty.get(doc.name, 0), - "postprocess": update_item - } - }, target_doc) + }, + target_doc, + ) return doc + @frappe.whitelist() def make_project(source_name, target_doc=None): def postprocess(source, doc): doc.project_type = "External" doc.project_name = source.name - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Project", - "validation": { - "docstatus": ["=", 1] + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Project", + "validation": {"docstatus": ["=", 1]}, + "field_map": { + "name": "sales_order", + "base_grand_total": "estimated_costing", + }, }, - "field_map":{ - "name" : "sales_order", - "base_grand_total" : "estimated_costing", - } }, - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doc + @frappe.whitelist() def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): def set_missing_values(source, target): @@ -571,13 +715,13 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target.run_method("calculate_taxes_and_totals") if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Delivery Note", 'company_address', target.company_address)) + target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) def update_item(source, target, source_parent): target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate) @@ -588,34 +732,26 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): item_group = get_item_group_defaults(target.item_code, source_parent.company) if item: - target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center") \ - or item.get("buying_cost_center") \ + target.cost_center = ( + frappe.db.get_value("Project", source_parent.project, "cost_center") + or item.get("buying_cost_center") or item_group.get("buying_cost_center") + ) mapper = { - "Sales Order": { - "doctype": "Delivery Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } + "Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, } if not skip_item_mapping: + def condition(doc): # make_mapped_doc sets js `args` into `frappe.flags.args` if frappe.flags.args and frappe.flags.args.delivery_dates: if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates: return False - return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1 mapper["Sales Order Item"] = { "doctype": "Delivery Note Item", @@ -625,20 +761,21 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): "parent": "against_sales_order", }, "postprocess": update_item, - "condition": condition + "condition": condition, } target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values) - target_doc.set_onload('ignore_price_list', True) + target_doc.set_onload("ignore_price_list", True) return target_doc + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): def postprocess(source, target): set_missing_values(source, target) - #Get the advance paid Journal Entries in Sales Invoice Advance + # Get the advance paid Journal Entries in Sales Invoice Advance if target.get("allocate_advances_automatically"): target.set_advances() @@ -649,13 +786,13 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.run_method("calculate_taxes_and_totals") if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) + target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) # set the redeem loyalty points if provided via shopping cart if source.loyalty_points and source.order_type == "Shopping Cart": @@ -664,108 +801,117 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): def update_item(source, target, source_parent): target.amount = flt(source.amount) - flt(source.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) - target.qty = target.amount / flt(source.rate) if (source.rate and source.billed_amt) else source.qty - source.returned_qty + target.qty = ( + target.amount / flt(source.rate) + if (source.rate and source.billed_amt) + else source.qty - source.returned_qty + ) if source_parent.project: target.cost_center = frappe.db.get_value("Project", source_parent.project, "cost_center") if target.item_code: item = get_item_defaults(target.item_code, source_parent.company) item_group = get_item_group_defaults(target.item_code, source_parent.company) - cost_center = item.get("selling_cost_center") \ - or item_group.get("selling_cost_center") + cost_center = item.get("selling_cost_center") or item_group.get("selling_cost_center") if cost_center: target.cost_center = cost_center - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Sales Invoice", - "field_map": { - "party_account_currency": "party_account_currency", - "payment_terms_template": "payment_terms_template" + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Sales Invoice", + "field_map": { + "party_account_currency": "party_account_currency", + "payment_terms_template": "payment_terms_template", + }, + "field_no_map": ["payment_terms_template"], + "validation": {"docstatus": ["=", 1]}, }, - "field_no_map": ["payment_terms_template"], - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Order Item": { - "doctype": "Sales Invoice Item", - "field_map": { - "name": "so_detail", - "parent": "sales_order", + "Sales Order Item": { + "doctype": "Sales Invoice Item", + "field_map": { + "name": "so_detail", + "parent": "sales_order", + }, + "postprocess": update_item, + "condition": lambda doc: doc.qty + and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), }, - "postprocess": update_item, - "condition": lambda doc: doc.qty and (doc.base_amount==0 or abs(doc.billed_amt) < abs(doc.amount)) + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "add_if_empty": True - } - }, target_doc, postprocess, ignore_permissions=ignore_permissions) + target_doc, + postprocess, + ignore_permissions=ignore_permissions, + ) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + automatically_fetch_payment_terms = cint( + frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) if automatically_fetch_payment_terms: doclist.set_payment_schedule() - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist + @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - maint_schedule = frappe.db.sql("""select t1.name + maint_schedule = frappe.db.sql( + """select t1.name from `tabMaintenance Schedule` t1, `tabMaintenance Schedule Item` t2 - where t2.parent=t1.name and t2.sales_order=%s and t1.docstatus=1""", source_name) + where t2.parent=t1.name and t2.sales_order=%s and t1.docstatus=1""", + source_name, + ) if not maint_schedule: - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Maintenance Schedule", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Maintenance Schedule Item", + "field_map": {"parent": "sales_order"}, + }, }, - "Sales Order Item": { - "doctype": "Maintenance Schedule Item", - "field_map": { - "parent": "sales_order" - } - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None): - visit = frappe.db.sql("""select t1.name + visit = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent=t1.name and t2.prevdoc_docname=%s - and t1.docstatus=1 and t1.completion_status='Fully Completed'""", source_name) + and t1.docstatus=1 and t1.completion_status='Fully Completed'""", + source_name, + ) if not visit: - doclist = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Maintenance Visit", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Maintenance Visit", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Maintenance Visit Purpose", + "field_map": {"parent": "prevdoc_docname", "parenttype": "prevdoc_doctype"}, + }, }, - "Sales Order Item": { - "doctype": "Maintenance Visit Purpose", - "field_map": { - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype" - } - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def get_events(start, end, filters=None): """Returns events for Gantt / Calendar view rendering. @@ -775,9 +921,11 @@ def get_events(start, end, filters=None): :param filters: Filters (JSON). """ from frappe.desk.calendar import get_event_conditions + conditions = get_event_conditions("Sales Order", filters) - data = frappe.db.sql(""" + data = frappe.db.sql( + """ select distinct `tabSales Order`.name, `tabSales Order`.customer_name, `tabSales Order`.status, `tabSales Order`.delivery_status, `tabSales Order`.billing_status, @@ -790,16 +938,21 @@ def get_events(start, end, filters=None): and (`tabSales Order Item`.delivery_date between %(start)s and %(end)s) and `tabSales Order`.docstatus < 2 {conditions} - """.format(conditions=conditions), { - "start": start, - "end": end - }, as_dict=True, update={"allDay": 0}) + """.format( + conditions=conditions + ), + {"start": start, "end": end}, + as_dict=True, + update={"allDay": 0}, + ) return data + @frappe.whitelist() def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" - if not selected_items: return + if not selected_items: + return if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) @@ -815,7 +968,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t if default_price_list: target.buying_price_list = default_price_list - if any( item.delivered_by_supplier==1 for item in source.items): + if any(item.delivered_by_supplier == 1 for item in source.items): if source.shipping_address_name: target.shipping_address = source.shipping_address_name target.shipping_address_display = source.shipping_address @@ -838,59 +991,67 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t def update_item(source, target, source_parent): target.schedule_date = source.delivery_date target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) - target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty) target.project = source_parent.project - suppliers = [item.get('supplier') for item in selected_items if item.get('supplier')] - suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order + suppliers = [item.get("supplier") for item in selected_items if item.get("supplier")] + suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order - items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code')] + items_to_map = [item.get("item_code") for item in selected_items if item.get("item_code")] items_to_map = list(set(items_to_map)) if not suppliers: - frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) + frappe.throw( + _("Please set a Supplier against the Items to be considered in the Purchase Order.") + ) purchase_orders = [] for supplier in suppliers: - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address", - "terms" - ], - "validation": { - "docstatus": ["=", 1] - } + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address", + "terms", + ], + "validation": {"docstatus": ["=", 1]}, + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"], + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template", + "discount_percentage", + "discount_amount", + "pricing_rules", + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty + and doc.supplier == supplier + and doc.item_code in items_to_map, + }, }, - "Sales Order Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_item"], - ["parent", "sales_order"], - ["stock_uom", "stock_uom"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["delivery_date", "schedule_date"] - ], - "field_no_map": [ - "rate", - "price_list_rate", - "item_tax_template", - "discount_percentage", - "discount_amount", - "pricing_rules" - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) doc.insert() frappe.db.commit() @@ -898,14 +1059,20 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t return purchase_orders + @frappe.whitelist() def make_purchase_order(source_name, selected_items=None, target_doc=None): - if not selected_items: return + if not selected_items: + return if isinstance(selected_items, string_types): selected_items = json.loads(selected_items) - items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = [ + item.get("item_code") + for item in selected_items + if item.get("item_code") and item.get("item_code") + ] items_to_map = list(set(items_to_map)) def set_missing_values(source, target): @@ -922,86 +1089,89 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): def update_item(source, target, source_parent): target.schedule_date = source.delivery_date target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor)) - target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) + target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty) target.project = source_parent.project def update_item_for_packed_item(source, target, source_parent): target.qty = flt(source.qty) - flt(source.ordered_qty) # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) - doc = get_mapped_doc("Sales Order", source_name, { - "Sales Order": { - "doctype": "Purchase Order", - "field_no_map": [ - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "contact_person", - "taxes_and_charges", - "shipping_address", - "terms" - ], - "validation": { - "docstatus": ["=", 1] - } + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": { + "doctype": "Purchase Order", + "field_no_map": [ + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "contact_person", + "taxes_and_charges", + "shipping_address", + "terms", + ], + "validation": {"docstatus": ["=", 1]}, + }, + "Sales Order Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_item"], + ["parent", "sales_order"], + ["stock_uom", "stock_uom"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["delivery_date", "schedule_date"], + ], + "field_no_map": [ + "rate", + "price_list_rate", + "item_tax_template", + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules", + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty + and doc.item_code in items_to_map + and not is_product_bundle(doc.item_code), + }, + "Packed Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "sales_order_packed_item"], + ["parent", "sales_order"], + ["uom", "uom"], + ["conversion_factor", "conversion_factor"], + ["parent_item", "product_bundle"], + ["rate", "rate"], + ], + "field_no_map": [ + "price_list_rate", + "item_tax_template", + "discount_percentage", + "discount_amount", + "supplier", + "pricing_rules", + ], + "postprocess": update_item_for_packed_item, + "condition": lambda doc: doc.parent_item in items_to_map, + }, }, - "Sales Order Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_item"], - ["parent", "sales_order"], - ["stock_uom", "stock_uom"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["delivery_date", "schedule_date"] - ], - "field_no_map": [ - "rate", - "price_list_rate", - "item_tax_template", - "discount_percentage", - "discount_amount", - "supplier", - "pricing_rules" - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map and not is_product_bundle(doc.item_code) - }, - "Packed Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "sales_order_packed_item"], - ["parent", "sales_order"], - ["uom", "uom"], - ["conversion_factor", "conversion_factor"], - ["parent_item", "product_bundle"], - ["rate", "rate"] - ], - "field_no_map": [ - "price_list_rate", - "item_tax_template", - "discount_percentage", - "discount_amount", - "supplier", - "pricing_rules" - ], - "postprocess": update_item_for_packed_item, - "condition": lambda doc: doc.parent_item in items_to_map - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) set_delivery_date(doc.items, source_name) return doc + def set_delivery_date(items, sales_order): delivery_dates = frappe.get_all( - 'Sales Order Item', - filters = { - 'parent': sales_order - }, - fields = ['delivery_date', 'item_code'] + "Sales Order Item", filters={"parent": sales_order}, fields=["delivery_date", "item_code"] ) delivery_by_item = frappe._dict() @@ -1012,13 +1182,15 @@ def set_delivery_date(items, sales_order): if item.product_bundle: item.schedule_date = delivery_by_item[item.product_bundle] + def is_product_bundle(item_code): - return frappe.db.exists('Product Bundle', item_code) + return frappe.db.exists("Product Bundle", item_code) + @frappe.whitelist() def make_work_orders(items, sales_order, company, project=None): - '''Make Work Orders against the given Sales Order for the given `items`''' - items = json.loads(items).get('items') + """Make Work Orders against the given Sales Order for the given `items`""" + items = json.loads(items).get("items") out = [] for i in items: @@ -1027,18 +1199,20 @@ def make_work_orders(items, sales_order, company, project=None): if not i.get("pending_qty"): frappe.throw(_("Please select Qty against item {0}").format(i.get("item_code"))) - work_order = frappe.get_doc(dict( - doctype='Work Order', - production_item=i['item_code'], - bom_no=i.get('bom'), - qty=i['pending_qty'], - company=company, - sales_order=sales_order, - sales_order_item=i['sales_order_item'], - project=project, - fg_warehouse=i['warehouse'], - description=i['description'] - )).insert() + work_order = frappe.get_doc( + dict( + doctype="Work Order", + production_item=i["item_code"], + bom_no=i.get("bom"), + qty=i["pending_qty"], + company=company, + sales_order=sales_order, + sales_order_item=i["sales_order_item"], + project=project, + fg_warehouse=i["warehouse"], + description=i["description"], + ) + ).insert() work_order.set_work_order_operations() work_order.flags.ignore_mandatory = True work_order.save() @@ -1046,18 +1220,20 @@ def make_work_orders(items, sales_order, company, project=None): return [p.name for p in out] + @frappe.whitelist() def update_status(status, name): so = frappe.get_doc("Sales Order", name) so.update_status(status) + def get_default_bom_item(item_code): - bom = frappe.get_all('BOM', dict(item=item_code, is_active=True), - order_by='is_default desc') + bom = frappe.get_all("BOM", dict(item=item_code, is_active=True), order_by="is_default desc") bom = bom[0].name if bom else None return bom + @frappe.whitelist() def make_raw_material_request(items, company, sales_order, project=None): if not frappe.has_permission("Sales Order", "write"): @@ -1066,43 +1242,49 @@ def make_raw_material_request(items, company, sales_order, project=None): if isinstance(items, string_types): items = frappe._dict(json.loads(items)) - for item in items.get('items'): - item["include_exploded_items"] = items.get('include_exploded_items') - item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty') - item["include_raw_materials_from_sales_order"] = items.get('include_raw_materials_from_sales_order') + for item in items.get("items"): + item["include_exploded_items"] = items.get("include_exploded_items") + item["ignore_existing_ordered_qty"] = items.get("ignore_existing_ordered_qty") + item["include_raw_materials_from_sales_order"] = items.get( + "include_raw_materials_from_sales_order" + ) - items.update({ - 'company': company, - 'sales_order': sales_order - }) + items.update({"company": company, "sales_order": sales_order}) raw_materials = get_items_for_material_requests(items) if not raw_materials: - frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available.")) + frappe.msgprint( + _("Material Request not created, as quantity for Raw Materials already available.") + ) return - material_request = frappe.new_doc('Material Request') - material_request.update(dict( - doctype = 'Material Request', - transaction_date = nowdate(), - company = company, - material_request_type = 'Purchase' - )) + material_request = frappe.new_doc("Material Request") + material_request.update( + dict( + doctype="Material Request", + transaction_date=nowdate(), + company=company, + material_request_type="Purchase", + ) + ) for item in raw_materials: - item_doc = frappe.get_cached_doc('Item', item.get('item_code')) + item_doc = frappe.get_cached_doc("Item", item.get("item_code")) schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) - row = material_request.append('items', { - 'item_code': item.get('item_code'), - 'qty': item.get('quantity'), - 'schedule_date': schedule_date, - 'warehouse': item.get('warehouse'), - 'sales_order': sales_order, - 'project': project - }) + row = material_request.append( + "items", + { + "item_code": item.get("item_code"), + "qty": item.get("quantity"), + "schedule_date": schedule_date, + "warehouse": item.get("warehouse"), + "sales_order": sales_order, + "project": project, + }, + ) if not (strip_html(item.get("description")) and strip_html(item_doc.description)): - row.description = item_doc.item_name or item.get('item_code') + row.description = item_doc.item_name or item.get("item_code") material_request.insert() material_request.flags.ignore_permissions = 1 @@ -1110,53 +1292,56 @@ def make_raw_material_request(items, company, sales_order, project=None): material_request.submit() return material_request + @frappe.whitelist() def make_inter_company_purchase_order(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction + return make_inter_company_transaction("Sales Order", source_name, target_doc) + @frappe.whitelist() def create_pick_list(source_name, target_doc=None): def update_item_quantity(source, target, source_parent): target.qty = flt(source.qty) - flt(source.delivered_qty) target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor) - doc = get_mapped_doc('Sales Order', source_name, { - 'Sales Order': { - 'doctype': 'Pick List', - 'validation': { - 'docstatus': ['=', 1] - } - }, - 'Sales Order Item': { - 'doctype': 'Pick List Item', - 'field_map': { - 'parent': 'sales_order', - 'name': 'sales_order_item' + doc = get_mapped_doc( + "Sales Order", + source_name, + { + "Sales Order": {"doctype": "Pick List", "validation": {"docstatus": ["=", 1]}}, + "Sales Order Item": { + "doctype": "Pick List Item", + "field_map": {"parent": "sales_order", "name": "sales_order_item"}, + "postprocess": update_item_quantity, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, }, - 'postprocess': update_item_quantity, - 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 }, - }, target_doc) + target_doc, + ) - doc.purpose = 'Delivery' + doc.purpose = "Delivery" doc.set_item_locations() return doc + def update_produced_qty_in_so_item(sales_order, sales_order_item): - #for multiple work orders against same sales order item - linked_wo_with_so_item = frappe.db.get_all('Work Order', ['produced_qty'], { - 'sales_order_item': sales_order_item, - 'sales_order': sales_order, - 'docstatus': 1 - }) + # for multiple work orders against same sales order item + linked_wo_with_so_item = frappe.db.get_all( + "Work Order", + ["produced_qty"], + {"sales_order_item": sales_order_item, "sales_order": sales_order, "docstatus": 1}, + ) total_produced_qty = 0 for wo in linked_wo_with_so_item: - total_produced_qty += flt(wo.get('produced_qty')) + total_produced_qty += flt(wo.get("produced_qty")) - if not total_produced_qty and frappe.flags.in_patch: return + if not total_produced_qty and frappe.flags.in_patch: + return - frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) + frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty) diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index abf507a9e54..ace2e29c2b4 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -1,45 +1,27 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'sales_order', - 'non_standard_fieldnames': { - 'Delivery Note': 'against_sales_order', - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name', - 'Auto Repeat': 'reference_document', - 'Maintenance Visit': 'prevdoc_docname' + "fieldname": "sales_order", + "non_standard_fieldnames": { + "Delivery Note": "against_sales_order", + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Payment Request": "reference_name", + "Auto Repeat": "reference_document", + "Maintenance Visit": "prevdoc_docname", }, - 'internal_links': { - 'Quotation': ['items', 'prevdoc_docname'] - }, - 'transactions': [ + "internal_links": {"Quotation": ["items", "prevdoc_docname"]}, + "transactions": [ { - 'label': _('Fulfillment'), - 'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit'] + "label": _("Fulfillment"), + "items": ["Sales Invoice", "Pick List", "Delivery Note", "Maintenance Visit"], }, - { - 'label': _('Purchasing'), - 'items': ['Material Request', 'Purchase Order'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - }, - { - 'label': _('Manufacturing'), - 'items': ['Work Order'] - }, - { - 'label': _('Reference'), - 'items': ['Quotation', 'Auto Repeat'] - }, - { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry'] - }, - ] + {"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]}, + {"label": _("Projects"), "items": ["Project"]}, + {"label": _("Manufacturing"), "items": ["Work Order"]}, + {"label": _("Reference"), "items": ["Quotation", "Auto Repeat"]}, + {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]}, + ], } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 9d093b205e9..8edc9394c10 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -25,18 +25,24 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestSalesOrder(FrappeTestCase): - @classmethod def setUpClass(cls): super().setUpClass() - cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order")) + cls.unlink_setting = int( + frappe.db.get_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order" + ) + ) @classmethod def tearDownClass(cls) -> None: # reset config to previous state - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) + frappe.db.set_value( + "Accounts Settings", + "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", + cls.unlink_setting, + ) super().tearDownClass() def tearDown(self): @@ -83,6 +89,7 @@ class TestSalesOrder(FrappeTestCase): def test_so_billed_amount_against_return_entry(self): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + so = make_sales_order(do_not_submit=True) so.submit() @@ -111,7 +118,7 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(len(si.get("items")), 1) si.insert() - si.set('taxes', []) + si.set("taxes", []) si.save() self.assertEqual(si.payment_schedule[0].payment_amount, 500.0) @@ -179,16 +186,16 @@ class TestSalesOrder(FrappeTestCase): dn1.items[0].so_detail = so.items[0].name dn1.submit() - si1 = create_sales_invoice(is_return=1, return_against=si2.name, qty=-1, update_stock=1, do_not_submit=True) + si1 = create_sales_invoice( + is_return=1, return_against=si2.name, qty=-1, update_stock=1, do_not_submit=True + ) si1.items[0].sales_order = so.name si1.items[0].so_detail = so.items[0].name si1.submit() - so.load_from_db() self.assertEqual(so.get("items")[0].delivered_qty, 5) - def test_reserved_qty_for_partial_delivery(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) existing_reserved_qty = get_reserved_qty() @@ -206,7 +213,7 @@ class TestSalesOrder(FrappeTestCase): # unclose so so.load_from_db() - so.update_status('Draft') + so.update_status("Draft") self.assertEqual(get_reserved_qty(), existing_reserved_qty + 5) dn.cancel() @@ -220,7 +227,7 @@ class TestSalesOrder(FrappeTestCase): def test_reserved_qty_for_over_delivery(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Item", 'over_delivery_receipt_allowance', 50) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) existing_reserved_qty = get_reserved_qty() @@ -237,8 +244,8 @@ class TestSalesOrder(FrappeTestCase): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Item", 'over_delivery_receipt_allowance', 50) - frappe.db.set_value('Item', "_Test Item", 'over_billing_allowance', 20) + frappe.db.set_value("Item", "_Test Item", "over_delivery_receipt_allowance", 50) + frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 20) existing_reserved_qty = get_reserved_qty() @@ -266,7 +273,9 @@ class TestSalesOrder(FrappeTestCase): def test_reserved_qty_for_partial_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry( + item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 + ) existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") @@ -274,14 +283,16 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) dn = create_dn_against_so(so.name) self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 10) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 + ) # close so so.load_from_db() @@ -292,16 +303,18 @@ class TestSalesOrder(FrappeTestCase): # unclose so so.load_from_db() - so.update_status('Draft') + so.update_status("Draft") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 25) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 10) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 10 + ) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) so.load_from_db() so.cancel() @@ -310,17 +323,19 @@ class TestSalesOrder(FrappeTestCase): def test_sales_order_on_hold(self): so = make_sales_order(item_code="_Test Product Bundle Item") - so.db_set('Status', "On Hold") + so.db_set("Status", "On Hold") si = make_sales_invoice(so.name) self.assertRaises(frappe.ValidationError, create_dn_against_so, so.name) self.assertRaises(frappe.ValidationError, si.submit) def test_reserved_qty_for_over_delivery_with_packing_list(self): make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100) - make_stock_entry(item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100) + make_stock_entry( + item="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=10, rate=100 + ) # set over-delivery allowance - frappe.db.set_value('Item', "_Test Product Bundle Item", 'over_delivery_receipt_allowance', 50) + frappe.db.set_value("Item", "_Test Product Bundle Item", "over_delivery_receipt_allowance", 50) existing_reserved_qty_item1 = get_reserved_qty("_Test Item") existing_reserved_qty_item2 = get_reserved_qty("_Test Item Home Desktop 100") @@ -328,22 +343,23 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(item_code="_Test Product Bundle Item") self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) dn = create_dn_against_so(so.name, 15) self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2) + self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2) dn.cancel() self.assertEqual(get_reserved_qty("_Test Item"), existing_reserved_qty_item1 + 50) - self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"), - existing_reserved_qty_item2 + 20) + self.assertEqual( + get_reserved_qty("_Test Item Home Desktop 100"), existing_reserved_qty_item2 + 20 + ) def test_update_child_adding_new_item(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) @@ -354,38 +370,38 @@ class TestSalesOrder(FrappeTestCase): reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") first_item_of_so = so.get("items")[0] - trans_item = json.dumps([ - {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \ - 'qty' : first_item_of_so.qty, 'docname': first_item_of_so.name}, - {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 7} - ]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [ + { + "item_code": first_item_of_so.item_code, + "rate": first_item_of_so.rate, + "qty": first_item_of_so.qty, + "docname": first_item_of_so.name, + }, + {"item_code": "_Test Item 2", "rate": 200, "qty": 7}, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() - self.assertEqual(so.get("items")[-1].item_code, '_Test Item 2') + self.assertEqual(so.get("items")[-1].item_code, "_Test Item 2") self.assertEqual(so.get("items")[-1].rate, 200) self.assertEqual(so.get("items")[-1].qty, 7) self.assertEqual(so.get("items")[-1].amount, 1400) # reserved qty should increase after adding row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 7) + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item + 7) - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(so.status, "To Deliver and Bill") updated_total = so.get("base_total") updated_total_in_words = so.get("base_in_words") - self.assertEqual(updated_total, prev_total+1400) + self.assertEqual(updated_total, prev_total + 1400) self.assertNotEqual(updated_total_in_words, prev_total_in_words) def test_update_child_removing_item(self): - so = make_sales_order(**{ - "item_list": [{ - "item_code": '_Test Item', - "qty": 5, - "rate":1000 - }] - }) + so = make_sales_order(**{"item_list": [{"item_code": "_Test Item", "qty": 5, "rate": 1000}]}) create_dn_against_so(so.name, 2) make_sales_invoice(so.name) @@ -393,64 +409,67 @@ class TestSalesOrder(FrappeTestCase): reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") # add an item so as to try removing items - trans_item = json.dumps([ - {"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name}, - {"item_code": '_Test Item 2', "qty": 2, "rate":500} - ]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [ + {"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.get("items")[0].name}, + {"item_code": "_Test Item 2", "qty": 2, "rate": 500}, + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(len(so.get("items")), 2) # reserved qty should increase after adding row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 2) + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item + 2) # check if delivered items can be removed - trans_item = json.dumps([{ - "item_code": '_Test Item 2', - "qty": 2, - "rate":500, - "docname": so.get("items")[1].name - }]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item 2", "qty": 2, "rate": 500, "docname": so.get("items")[1].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) - #remove last added item - trans_item = json.dumps([{ - "item_code": '_Test Item', - "qty": 5, - "rate":1000, - "docname": so.get("items")[0].name - }]) - update_child_qty_rate('Sales Order', trans_item, so.name) + # remove last added item + trans_item = json.dumps( + [{"item_code": "_Test Item", "qty": 5, "rate": 1000, "docname": so.get("items")[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(len(so.get("items")), 1) # reserved qty should decrease (back to initial) after deleting row - self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item) - - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(get_reserved_qty("_Test Item 2"), reserved_qty_for_second_item) + self.assertEqual(so.status, "To Deliver and Bill") def test_update_child(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) create_dn_against_so(so.name, 4) make_sales_invoice(so.name) existing_reserved_qty = get_reserved_qty() - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": so.items[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(so.get("items")[0].rate, 200) self.assertEqual(so.get("items")[0].qty, 7) self.assertEqual(so.get("items")[0].amount, 1400) - self.assertEqual(so.status, 'To Deliver and Bill') + self.assertEqual(so.status, "To Deliver and Bill") self.assertEqual(get_reserved_qty(), existing_reserved_qty + 3) - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 2, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) def test_update_child_with_precision(self): from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -459,48 +478,60 @@ class TestSalesOrder(FrappeTestCase): precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate")) make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency") - so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664) + so = make_sales_order(item_code="_Test Item", qty=4, rate=200.34664) - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}]) - update_child_qty_rate('Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200.34669, "qty": 4, "docname": so.items[0].name}] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) so.reload() self.assertEqual(so.items[0].rate, 200.34669) make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency") def test_update_child_perm(self): - so = make_sales_order(item_code= "_Test Item", qty=4) + so = make_sales_order(item_code="_Test Item", qty=4) test_user = create_user("test_so_child_perms@example.com", "Accounts User") frappe.set_user(test_user.name) # update qty - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 200, "qty": 7, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) # add new item - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + trans_item = json.dumps([{"item_code": "_Test Item", "rate": 100, "qty": 2}]) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow workflow = make_sales_order_workflow() - so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) - apply_workflow(so, 'Approve') + so = make_sales_order(item_code="_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, "Approve") - user = 'test@example.com' - test_user = frappe.get_doc('User', user) + user = "test@example.com" + test_user = frappe.get_doc("User", user) test_user.add_roles("Sales User", "Test Junior Approver") frappe.set_user(user) # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled - trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}]) - self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + trans_item = json.dumps( + [{"item_code": "_Test Item", "rate": 150, "qty": 2, "docname": so.items[0].name}] + ) + self.assertRaises( + frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name + ) frappe.set_user("Administrator") - user2 = 'test2@example.com' - test_user2 = frappe.get_doc('User', user2) + user2 = "test2@example.com" + test_user2 = frappe.get_doc("User", user2) test_user2.add_roles("Sales User", "Test Approver") frappe.set_user(user2) @@ -519,21 +550,21 @@ class TestSalesOrder(FrappeTestCase): # test Update Items with product bundle if not frappe.db.exists("Item", "_Product Bundle Item"): bundle_item = make_item("_Product Bundle Item", {"is_stock_item": 0}) - bundle_item.append("item_defaults", { - "company": "_Test Company", - "default_warehouse": "_Test Warehouse - _TC"}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) bundle_item.save(ignore_permissions=True) make_item("_Packed Item", {"is_stock_item": 1}) make_product_bundle("_Product Bundle Item", ["_Packed Item"], 2) - so = make_sales_order(item_code = "_Test Item", warehouse=None) + so = make_sales_order(item_code="_Test Item", warehouse=None) # get reserved qty of packed item existing_reserved_qty = get_reserved_qty("_Packed Item") - added_item = json.dumps([{"item_code" : "_Product Bundle Item", "rate" : 200, 'qty' : 2}]) - update_child_qty_rate('Sales Order', added_item, so.name) + added_item = json.dumps([{"item_code": "_Product Bundle Item", "rate": 200, "qty": 2}]) + update_child_qty_rate("Sales Order", added_item, so.name) so.reload() self.assertEqual(so.packed_items[0].qty, 4) @@ -542,15 +573,19 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 4) # test uom and conversion factor change - update_uom_conv_factor = json.dumps([{ - 'item_code': so.get("items")[0].item_code, - 'rate': so.get("items")[0].rate, - 'qty': so.get("items")[0].qty, - 'uom': "_Test UOM 1", - 'conversion_factor': 2, - 'docname': so.get("items")[0].name - }]) - update_child_qty_rate('Sales Order', update_uom_conv_factor, so.name) + update_uom_conv_factor = json.dumps( + [ + { + "item_code": so.get("items")[0].item_code, + "rate": so.get("items")[0].rate, + "qty": so.get("items")[0].qty, + "uom": "_Test UOM 1", + "conversion_factor": 2, + "docname": so.get("items")[0].name, + } + ] + ) + update_child_qty_rate("Sales Order", update_uom_conv_factor, so.name) so.reload() self.assertEqual(so.packed_items[0].qty, 8) @@ -560,61 +595,67 @@ class TestSalesOrder(FrappeTestCase): def test_update_child_with_tax_template(self): """ - Test Action: Create a SO with one item having its tax account head already in the SO. - Add the same item + new item with tax template via Update Items. - Expected result: First Item's tax row is updated. New tax row is added for second Item. + Test Action: Create a SO with one item having its tax account head already in the SO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. """ if not frappe.db.exists("Item", "Test Item with Tax"): - make_item("Test Item with Tax", { - 'is_stock_item': 1, - }) + make_item( + "Test Item with Tax", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): - frappe.get_doc({ - 'doctype': 'Item Tax Template', - 'title': 'Test Update Items Template', - 'company': '_Test Company', - 'taxes': [ - { - 'tax_type': "_Test Account Service Tax - _TC", - 'tax_rate': 10, - } - ] - }).insert() + if not frappe.db.exists("Item Tax Template", {"title": "Test Update Items Template"}): + frappe.get_doc( + { + "doctype": "Item Tax Template", + "title": "Test Update Items Template", + "company": "_Test Company", + "taxes": [ + { + "tax_type": "_Test Account Service Tax - _TC", + "tax_rate": 10, + } + ], + } + ).insert() new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") - new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template - _TC", - "valid_from": nowdate() - }) + new_item_with_tax.append( + "taxes", {"item_tax_template": "Test Update Items Template - _TC", "valid_from": nowdate()} + ) new_item_with_tax.save() tax_template = "_Test Account Excise Duty @ 10 - _TC" - item = "_Test Item Home Desktop 100" - if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item = "_Test Item Home Desktop 100" + if not frappe.db.exists("Item Tax", {"parent": item, "item_tax_template": tax_template}): item_doc = frappe.get_doc("Item", item) - item_doc.append("taxes", { - "item_tax_template": tax_template, - "valid_from": nowdate() - }) + item_doc.append("taxes", {"item_tax_template": tax_template, "valid_from": nowdate()}) item_doc.save() else: # update valid from - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = CURDATE() where parent = %(item)s and item_tax_template = %(tax)s""", - {"item": item, "tax": tax_template}) + {"item": item, "tax": tax_template}, + ) so = make_sales_order(item_code=item, qty=1, do_not_save=1) - so.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 10 - }) + so.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 10, + }, + ) so.insert() so.submit() @@ -624,12 +665,22 @@ class TestSalesOrder(FrappeTestCase): old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse") frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC") - items = json.dumps([ - {'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name}, - {'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO - {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO - ]) - update_child_qty_rate('Sales Order', items, so.name) + items = json.dumps( + [ + {"item_code": item, "rate": 100, "qty": 1, "docname": so.items[0].name}, + { + "item_code": item, + "rate": 200, + "qty": 1, + }, # added item whose tax account head already exists in PO + { + "item_code": new_item_with_tax.name, + "rate": 100, + "qty": 1, + }, # added item whose tax account head is missing in PO + ] + ) + update_child_qty_rate("Sales Order", items, so.name) so.reload() self.assertEqual(so.taxes[0].tax_amount, 40) @@ -639,8 +690,11 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.taxes[1].total, 480) # teardown - frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL - where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + frappe.db.sql( + """UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}, + ) so.cancel() so.delete() new_item_with_tax.delete() @@ -660,8 +714,12 @@ class TestSalesOrder(FrappeTestCase): frappe.set_user(test_user.name) - so = make_sales_order(company="_Test Company 1", customer="_Test Customer 1", - warehouse="_Test Warehouse 2 - _TC1", do_not_save=True) + so = make_sales_order( + company="_Test Company 1", + customer="_Test Customer 1", + warehouse="_Test Warehouse 2 - _TC1", + do_not_save=True, + ) so.conversion_rate = 0.02 so.plc_conversion_rate = 0.02 self.assertRaises(frappe.PermissionError, so.insert) @@ -671,7 +729,9 @@ class TestSalesOrder(FrappeTestCase): frappe.set_user("Administrator") frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name) - frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name) + frappe.permissions.remove_user_permission( + "Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name + ) frappe.permissions.remove_user_permission("Company", "_Test Company 1", test_user_2.name) def test_block_delivery_note_against_cancelled_sales_order(self): @@ -691,10 +751,12 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0}) make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0}) - make_product_bundle("_Test Service Product Bundle", - ["_Test Service Product Bundle Item 1", "_Test Service Product Bundle Item 2"]) + make_product_bundle( + "_Test Service Product Bundle", + ["_Test Service Product Bundle Item 1", "_Test Service Product Bundle Item 2"], + ) - so = make_sales_order(item_code = "_Test Service Product Bundle", warehouse=None) + so = make_sales_order(item_code="_Test Service Product Bundle", warehouse=None) self.assertTrue("_Test Service Product Bundle Item 1" in [d.item_code for d in so.packed_items]) self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items]) @@ -704,38 +766,59 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Mix Product Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Mix Product Bundle Item 2", {"is_stock_item": 0}) - make_product_bundle("_Test Mix Product Bundle", - ["_Test Mix Product Bundle Item 1", "_Test Mix Product Bundle Item 2"]) + make_product_bundle( + "_Test Mix Product Bundle", + ["_Test Mix Product Bundle Item 1", "_Test Mix Product Bundle Item 2"], + ) - self.assertRaises(WarehouseRequired, make_sales_order, item_code = "_Test Mix Product Bundle", warehouse="") + self.assertRaises( + WarehouseRequired, make_sales_order, item_code="_Test Mix Product Bundle", warehouse="" + ) def test_auto_insert_price(self): make_item("_Test Item for Auto Price List", {"is_stock_item": 0}) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) - item_price = frappe.db.get_value("Item Price", {"price_list": "_Test Price List", - "item_code": "_Test Item for Auto Price List"}) + item_price = frappe.db.get_value( + "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} + ) if item_price: frappe.delete_doc("Item Price", item_price) - make_sales_order(item_code = "_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100) - - self.assertEqual(frappe.db.get_value("Item Price", - {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, "price_list_rate"), 100) + make_sales_order( + item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 + ) + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, + "price_list_rate", + ), + 100, + ) # do not update price list frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) - item_price = frappe.db.get_value("Item Price", {"price_list": "_Test Price List", - "item_code": "_Test Item for Auto Price List"}) + item_price = frappe.db.get_value( + "Item Price", {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"} + ) if item_price: frappe.delete_doc("Item Price", item_price) - make_sales_order(item_code = "_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100) + make_sales_order( + item_code="_Test Item for Auto Price List", selling_price_list="_Test Price List", rate=100 + ) - self.assertEqual(frappe.db.get_value("Item Price", - {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, "price_list_rate"), None) + self.assertEqual( + frappe.db.get_value( + "Item Price", + {"price_list": "_Test Price List", "item_code": "_Test Item for Auto Price List"}, + "price_list_rate", + ), + None, + ) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) @@ -747,7 +830,9 @@ class TestSalesOrder(FrappeTestCase): from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items - po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1}) + po_item = make_item( + "_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1} + ) dn_item = make_item("_Test Regular Item", {"is_stock_item": 1}) so_items = [ @@ -757,21 +842,21 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC", "qty": 2, "rate": 300, - "conversion_factor": 1.0 - } + "conversion_factor": 1.0, + }, ] - if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1: + if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item") == 1: make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=2, rate=100) - #create so, po and dn + # create so, po and dn so = make_sales_order(item_list=so_items, do_not_submit=True) so.submit() @@ -784,12 +869,15 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(po.items[0].sales_order, so.name) self.assertEqual(po.items[0].item_code, po_item.item_code) self.assertEqual(dn.items[0].item_code, dn_item.item_code) - #test po_item length + # test po_item length self.assertEqual(len(po.items), 1) # test ordered_qty and reserved_qty for drop ship item - bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) + bin_po_item = frappe.get_all( + "Bin", + filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"], + ) ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 @@ -804,12 +892,15 @@ class TestSalesOrder(FrappeTestCase): po.load_from_db() # test after closing so - so.db_set('status', "Closed") + so.db_set("status", "Closed") so.update_reserved_qty() # test ordered_qty and reserved_qty for drop ship item after closing so - bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, - fields=["ordered_qty", "reserved_qty"]) + bin_po_item = frappe.get_all( + "Bin", + filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"}, + fields=["ordered_qty", "reserved_qty"], + ) ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0 reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0 @@ -832,8 +923,12 @@ class TestSalesOrder(FrappeTestCase): from erpnext.selling.doctype.sales_order.sales_order import update_status as so_update_status # make items - po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1}) - po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1}) + po_item1 = make_item( + "_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1} + ) + po_item2 = make_item( + "_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1} + ) so_items = [ { @@ -842,7 +937,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": po_item2.item_code, @@ -850,8 +945,8 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' - } + "supplier": "_Test Supplier", + }, ] # create so and po @@ -865,7 +960,7 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.customer, po1.customer) self.assertEqual(po1.items[0].sales_order, so.name) self.assertEqual(po1.items[0].item_code, po_item1.item_code) - #test po item length + # test po item length self.assertEqual(len(po1.items), 1) # create po for remaining item @@ -899,7 +994,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", }, { "item_code": "_Test Item for Drop Shipping 2", @@ -907,8 +1002,8 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier 1' - } + "supplier": "_Test Supplier 1", + }, ] # create so and po @@ -918,13 +1013,13 @@ class TestSalesOrder(FrappeTestCase): purchase_orders = make_purchase_order_for_default_supplier(so.name, selected_items=so_items) self.assertEqual(len(purchase_orders), 2) - self.assertEqual(purchase_orders[0].supplier, '_Test Supplier') - self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1') + self.assertEqual(purchase_orders[0].supplier, "_Test Supplier") + self.assertEqual(purchase_orders[1].supplier, "_Test Supplier 1") def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self): """ - Tests if the the Product Bundles in the Items table of Sales Orders are replaced with - their child items(from the Packed Items table) on creating a Purchase Order from it. + Tests if the the Product Bundles in the Items table of Sales Orders are replaced with + their child items(from the Packed Items table) on creating a Purchase Order from it. """ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order @@ -932,8 +1027,7 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) so_items = [ { @@ -942,7 +1036,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", } ] @@ -955,7 +1049,7 @@ class TestSalesOrder(FrappeTestCase): def test_purchase_order_updates_packed_item_ordered_qty(self): """ - Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order + Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order """ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order @@ -963,8 +1057,7 @@ class TestSalesOrder(FrappeTestCase): make_item("_Test Bundle Item 1", {"is_stock_item": 1}) make_item("_Test Bundle Item 2", {"is_stock_item": 1}) - make_product_bundle("_Test Product Bundle", - ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + make_product_bundle("_Test Product Bundle", ["_Test Bundle Item 1", "_Test Bundle Item 2"]) so_items = [ { @@ -973,7 +1066,7 @@ class TestSalesOrder(FrappeTestCase): "qty": 2, "rate": 400, "delivered_by_supplier": 1, - "supplier": '_Test Supplier' + "supplier": "_Test Supplier", } ] @@ -990,145 +1083,163 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.packed_items[1].ordered_qty, 2) def test_reserved_qty_for_closing_so(self): - bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, - fields=["reserved_qty"]) + bin = frappe.get_all( + "Bin", + filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + fields=["reserved_qty"], + ) existing_reserved_qty = bin[0].reserved_qty if bin else 0.0 so = make_sales_order(item_code="_Test Item", qty=1) - self.assertEqual(get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_reserved_qty+1) + self.assertEqual( + get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), + existing_reserved_qty + 1, + ) so.update_status("Closed") - self.assertEqual(get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), existing_reserved_qty) + self.assertEqual( + get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"), + existing_reserved_qty, + ) def test_create_so_with_margin(self): so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True) so.items[0].price_list_rate = price_list_rate = 100 - so.items[0].margin_type = 'Percentage' + so.items[0].margin_type = "Percentage" so.items[0].margin_rate_or_amount = 25 so.save() new_so = frappe.copy_doc(so) new_so.save(ignore_permissions=True) - self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual( + new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) + ) new_so.items[0].margin_rate_or_amount = 25 new_so.payment_schedule = [] new_so.save() new_so.submit() - self.assertEqual(new_so.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual( + new_so.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate) + ) def test_terms_auto_added(self): so = make_sales_order(do_not_save=1) - self.assertFalse(so.get('payment_schedule')) + self.assertFalse(so.get("payment_schedule")) so.insert() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) def test_terms_not_copied(self): so = make_sales_order() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) si = make_sales_invoice(so.name) - self.assertFalse(si.get('payment_schedule')) + self.assertFalse(si.get("payment_schedule")) def test_terms_copied(self): so = make_sales_order(do_not_copy=1, do_not_save=1) - so.payment_terms_template = '_Test Payment Term Template' + so.payment_terms_template = "_Test Payment Term Template" so.insert() so.submit() - self.assertTrue(so.get('payment_schedule')) + self.assertTrue(so.get("payment_schedule")) si = make_sales_invoice(so.name) si.insert() - self.assertTrue(si.get('payment_schedule')) + self.assertTrue(si.get("payment_schedule")) def test_make_work_order(self): # Make a new Sales Order - so = make_sales_order(**{ - "item_list": [{ - "item_code": "_Test FG Item", - "qty": 10, - "rate":100 - }, - { - "item_code": "_Test FG Item", - "qty": 20, - "rate":200 - }] - }) + so = make_sales_order( + **{ + "item_list": [ + {"item_code": "_Test FG Item", "qty": 10, "rate": 100}, + {"item_code": "_Test FG Item", "qty": 20, "rate": 200}, + ] + } + ) # Raise Work Orders - po_items= [] - so_item_name= {} + po_items = [] + so_item_name = {} for item in so.get_work_order_items(): - po_items.append({ - "warehouse": item.get("warehouse"), - "item_code": item.get("item_code"), - "pending_qty": item.get("pending_qty"), - "sales_order_item": item.get("sales_order_item"), - "bom": item.get("bom"), - "description": item.get("description") - }) - so_item_name[item.get("sales_order_item")]= item.get("pending_qty") - make_work_orders(json.dumps({"items":po_items}), so.name, so.company) + po_items.append( + { + "warehouse": item.get("warehouse"), + "item_code": item.get("item_code"), + "pending_qty": item.get("pending_qty"), + "sales_order_item": item.get("sales_order_item"), + "bom": item.get("bom"), + "description": item.get("description"), + } + ) + so_item_name[item.get("sales_order_item")] = item.get("pending_qty") + make_work_orders(json.dumps({"items": po_items}), so.name, so.company) # Check if Work Orders were raised for item in so_item_name: - wo_qty = frappe.db.sql("select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s", (so.name, item)) + wo_qty = frappe.db.sql( + "select sum(qty) from `tabWork Order` where sales_order=%s and sales_order_item=%s", + (so.name, item), + ) self.assertEqual(wo_qty[0][0], so_item_name.get(item)) def test_serial_no_based_delivery(self): frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1) - item = make_item("_Reserved_Serialized_Item", {"is_stock_item": 1, - "maintain_stock": 1, - "has_serial_no": 1, - "serial_no_series": "SI.####", - "valuation_rate": 500, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + item = make_item( + "_Reserved_Serialized_Item", + { + "is_stock_item": 1, + "maintain_stock": 1, + "has_serial_no": 1, + "serial_no_series": "SI.####", + "valuation_rate": 500, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) frappe.db.sql("""delete from `tabSerial No` where item_code=%s""", (item.item_code)) - make_item("_Test Item A", {"maintain_stock": 1, - "valuation_rate": 100, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Item B", {"maintain_stock": 1, - "valuation_rate": 200, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + make_item( + "_Test Item A", + { + "maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Item B", + { + "maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - make_bom(item=item.item_code, rate=1000, - raw_materials = ['_Test Item A', '_Test Item B']) - so = make_sales_order(**{ - "item_list": [{ - "item_code": item.item_code, - "ensure_delivery_based_on_produced_serial_no": 1, - "qty": 1, - "rate":1000 - }] - }) + make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Item A", "_Test Item B"]) + + so = make_sales_order( + **{ + "item_list": [ + { + "item_code": item.item_code, + "ensure_delivery_based_on_produced_serial_no": 1, + "qty": 1, + "rate": 1000, + } + ] + } + ) so.submit() from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - work_order = make_wo_order_test_record(item=item.item_code, - qty=1, do_not_save=True) + + work_order = make_wo_order_test_record(item=item.item_code, qty=1, do_not_save=True) work_order.fg_warehouse = "_Test Warehouse - _TC" work_order.sales_order = so.name work_order.submit() @@ -1137,6 +1248,7 @@ class TestSalesOrder(FrappeTestCase): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_production_stock_entry, ) + se = frappe.get_doc(make_production_stock_entry(work_order.name, "Manufacture", 1)) se.submit() reserved_serial_no = se.get("items")[2].serial_no @@ -1148,7 +1260,7 @@ class TestSalesOrder(FrappeTestCase): item_line = dn.get("items")[0] item_line.serial_no = item_serial_no.name item_line = dn.get("items")[0] - item_line.serial_no = reserved_serial_no + item_line.serial_no = reserved_serial_no dn.submit() dn.load_from_db() dn.cancel() @@ -1171,6 +1283,7 @@ class TestSalesOrder(FrappeTestCase): from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( make_delivery_note as make_delivery_note_from_invoice, ) + dn = make_delivery_note_from_invoice(si.name) dn.save() dn.submit() @@ -1185,8 +1298,10 @@ class TestSalesOrder(FrappeTestCase): def test_advance_payment_entry_unlink_against_sales_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) so = make_sales_order() @@ -1201,7 +1316,7 @@ class TestSalesOrder(FrappeTestCase): pe.save(ignore_permissions=True) pe.submit() - so_doc = frappe.get_doc('Sales Order', so.name) + so_doc = frappe.get_doc("Sales Order", so.name) self.assertRaises(frappe.LinkExistsError, so_doc.cancel) @@ -1212,8 +1327,9 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order() # disable unlinking of payment entry - frappe.db.set_value("Accounts Settings", "Accounts Settings", - "unlink_advance_payment_on_cancelation_of_order", 0) + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0 + ) # create a payment entry against sales order pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") @@ -1233,81 +1349,77 @@ class TestSalesOrder(FrappeTestCase): # Cancel sales order try: - so_doc = frappe.get_doc('Sales Order', so.name) + so_doc = frappe.get_doc("Sales Order", so.name) so_doc.cancel() except Exception: self.fail("Can not cancel sales order with linked cancelled payment entry") def test_request_for_raw_materials(self): - item = make_item("_Test Finished Item", {"is_stock_item": 1, - "maintain_stock": 1, - "valuation_rate": 500, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Raw Item A", {"maintain_stock": 1, - "valuation_rate": 100, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) - make_item("_Test Raw Item B", {"maintain_stock": 1, - "valuation_rate": 200, - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - }] - }) + item = make_item( + "_Test Finished Item", + { + "is_stock_item": 1, + "maintain_stock": 1, + "valuation_rate": 500, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Raw Item A", + { + "maintain_stock": 1, + "valuation_rate": 100, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) + make_item( + "_Test Raw Item B", + { + "maintain_stock": 1, + "valuation_rate": 200, + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + }, + ) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - make_bom(item=item.item_code, rate=1000, - raw_materials = ['_Test Raw Item A', '_Test Raw Item B']) - so = make_sales_order(**{ - "item_list": [{ - "item_code": item.item_code, - "qty": 1, - "rate":1000 - }] - }) + make_bom(item=item.item_code, rate=1000, raw_materials=["_Test Raw Item A", "_Test Raw Item B"]) + + so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]}) so.submit() mr_dict = frappe._dict() items = so.get_work_order_items(1) - mr_dict['items'] = items - mr_dict['include_exploded_items'] = 0 - mr_dict['ignore_existing_ordered_qty'] = 1 + mr_dict["items"] = items + mr_dict["include_exploded_items"] = 0 + mr_dict["ignore_existing_ordered_qty"] = 1 make_raw_material_request(mr_dict, so.company, so.name) - mr = frappe.db.sql("""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1)[0] - mr_doc = frappe.get_doc('Material Request',mr.get('name')) + mr = frappe.db.sql( + """select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1 + )[0] + mr_doc = frappe.get_doc("Material Request", mr.get("name")) self.assertEqual(mr_doc.items[0].sales_order, so.name) def test_so_optional_blanket_order(self): """ - Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. - Second Sales Order should not add on to Blanket Orders Ordered Quantity. + Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. + Second Sales Order should not add on to Blanket Orders Ordered Quantity. """ - bo = make_blanket_order(blanket_order_type = "Selling", quantity = 10, rate = 10) + bo = make_blanket_order(blanket_order_type="Selling", quantity=10, rate=10) - so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) - so_doc = frappe.get_doc('Sales Order', so.get('name')) + so = make_sales_order(item_code="_Test Item", qty=5, against_blanket_order=1) + so_doc = frappe.get_doc("Sales Order", so.get("name")) # To test if the SO has a Blanket Order self.assertTrue(so_doc.items[0].blanket_order) - so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) - so_doc = frappe.get_doc('Sales Order', so.get('name')) + so = make_sales_order(item_code="_Test Item", qty=5, against_blanket_order=0) + so_doc = frappe.get_doc("Sales Order", so.get("name")) # To test if the SO does NOT have a Blanket Order self.assertEqual(so_doc.items[0].blanket_order, None) def test_so_cancellation_when_si_drafted(self): """ - Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state - Expected result: sales order should not get cancelled + Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state + Expected result: sales order should not get cancelled """ so = make_sales_order() so.submit() @@ -1326,7 +1438,7 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() - so.payment_terms_template = 'Test Receivable Template' + so.payment_terms_template = "Test Receivable Template" so.submit() si = create_sales_invoice(qty=10, do_not_save=1) @@ -1348,10 +1460,10 @@ class TestSalesOrder(FrappeTestCase): so.submit() self.assertEqual(so.net_total, 0) - self.assertEqual(so.billing_status, 'Not Billed') + self.assertEqual(so.billing_status, "Not Billed") si = create_sales_invoice(qty=10, do_not_save=1) - si.price_list = '_Test Price List' + si.price_list = "_Test Price List" si.items[0].rate = 0 si.items[0].price_list_rate = 0 si.items[0].sales_order = so.name @@ -1361,7 +1473,7 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(si.net_total, 0) so.load_from_db() - self.assertEqual(so.billing_status, 'Fully Billed') + self.assertEqual(so.billing_status, "Fully Billed") def test_so_back_updated_from_wo_via_mr(self): "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." @@ -1370,7 +1482,7 @@ class TestSalesOrder(FrappeTestCase): ) from erpnext.stock.doctype.material_request.material_request import raise_work_orders - so = make_sales_order(item_list=[{"item_code": "_Test FG Item","qty": 2, "rate":100}]) + so = make_sales_order(item_list=[{"item_code": "_Test FG Item", "qty": 2, "rate": 100}]) mr = make_material_request(so.name) mr.material_request_type = "Manufacture" @@ -1387,17 +1499,18 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(wo.sales_order_item, so.items[0].name) wo.submit() - make_stock_entry(item_code="_Test Item", # Stock RM - target="Work In Progress - _TC", - qty=4, basic_rate=100 + make_stock_entry( + item_code="_Test Item", target="Work In Progress - _TC", qty=4, basic_rate=100 # Stock RM ) - make_stock_entry(item_code="_Test Item Home Desktop 100", # Stock RM + make_stock_entry( + item_code="_Test Item Home Desktop 100", # Stock RM target="Work In Progress - _TC", - qty=4, basic_rate=100 + qty=4, + basic_rate=100, ) se = frappe.get_doc(make_se_from_wo(wo.name, "Manufacture", 2)) - se.submit() # Finish WO + se.submit() # Finish WO mr.reload() wo.reload() @@ -1407,7 +1520,10 @@ class TestSalesOrder(FrappeTestCase): def test_sales_order_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule - shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test") + + shipping_rule = create_shipping_rule( + shipping_rule_type="Selling", shipping_rule_name="Shipping Rule - Sales Invoice Test" + ) sales_order = make_sales_order(do_not_save=True) sales_order.shipping_rule = shipping_rule.name @@ -1427,29 +1543,32 @@ class TestSalesOrder(FrappeTestCase): sales_order.save() self.assertEqual(sales_order.taxes[0].tax_amount, 0) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.automatically_fetch_payment_terms = enable accounts_settings.save() + def compare_payment_schedules(doc, doc1, doc2): - for index, schedule in enumerate(doc1.get('payment_schedule')): + for index, schedule in enumerate(doc1.get("payment_schedule")): doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term) doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date) doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion) doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount) + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) if args.transaction_date: so.transaction_date = args.transaction_date - so.set_warehouse = "" # no need to test set_warehouse permission since it only affects the client + so.set_warehouse = "" # no need to test set_warehouse permission since it only affects the client so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" - so.po_no = args.po_no or '12345' + so.po_no = args.po_no or "12345" if args.selling_price_list: so.selling_price_list = args.selling_price_list @@ -1461,14 +1580,17 @@ def make_sales_order(**args): so.append("items", item) else: - so.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse, - "qty": args.qty or 10, - "uom": args.uom or None, - "rate": args.rate or 100, - "against_blanket_order": args.against_blanket_order - }) + so.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse, + "qty": args.qty or 10, + "uom": args.uom or None, + "rate": args.rate or 100, + "against_blanket_order": args.against_blanket_order, + }, + ) so.delivery_date = add_days(so.transaction_date, 10) @@ -1483,6 +1605,7 @@ def make_sales_order(**args): return so + def create_dn_against_so(so, delivered_qty=0): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -1492,41 +1615,63 @@ def create_dn_against_so(so, delivered_qty=0): dn.submit() return dn + def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - "reserved_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "reserved_qty") + ) + test_dependencies = ["Currency Exchange"] + def make_sales_order_workflow(): - if frappe.db.exists('Workflow', 'SO Test Workflow'): + if frappe.db.exists("Workflow", "SO Test Workflow"): doc = frappe.get_doc("Workflow", "SO Test Workflow") doc.set("is_active", 1) doc.save() return doc - frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) - frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) - frappe.cache().hdel('roles', frappe.session.user) + frappe.get_doc(dict(doctype="Role", role_name="Test Junior Approver")).insert( + ignore_if_duplicate=True + ) + frappe.get_doc(dict(doctype="Role", role_name="Test Approver")).insert(ignore_if_duplicate=True) + frappe.cache().hdel("roles", frappe.session.user) - workflow = frappe.get_doc({ - "doctype": "Workflow", - "workflow_name": "SO Test Workflow", - "document_type": "Sales Order", - "workflow_state_field": "workflow_state", - "is_active": 1, - "send_email_alert": 0, - }) - workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) - workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) - workflow.append('transitions', dict( - state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, - condition = 'doc.grand_total < 200' - )) - workflow.append('transitions', dict( - state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1, - condition = 'doc.grand_total > 200' - )) + workflow = frappe.get_doc( + { + "doctype": "Workflow", + "workflow_name": "SO Test Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + } + ) + workflow.append("states", dict(state="Pending", allow_edit="All")) + workflow.append("states", dict(state="Approved", allow_edit="Test Approver", doc_status=1)) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="Test Junior Approver", + allow_self_approval=1, + condition="doc.grand_total < 200", + ), + ) + workflow.append( + "transitions", + dict( + state="Pending", + action="Approve", + next_state="Approved", + allowed="Test Approver", + allow_self_approval=1, + condition="doc.grand_total > 200", + ), + ) workflow.insert(ignore_permissions=True) return workflow diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 441a6ac9709..83d3f3bc076 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -9,5 +9,6 @@ from frappe.model.document import Document class SalesOrderItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Sales Order Item", ["item_code", "warehouse"]) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index fb86e614b6c..1d1a76fba5e 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -16,23 +16,46 @@ class SellingSettings(Document): self.toggle_editable_rate_for_bundle_items() def validate(self): - for key in ["cust_master_name", "campaign_naming_by", "customer_group", "territory", - "maintain_same_sales_rate", "editable_price_list_rate", "selling_price_list"]: - frappe.db.set_default(key, self.get(key, "")) + for key in [ + "cust_master_name", + "campaign_naming_by", + "customer_group", + "territory", + "maintain_same_sales_rate", + "editable_price_list_rate", + "selling_price_list", + ]: + frappe.db.set_default(key, self.get(key, "")) from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Customer", "customer_name", - self.get("cust_master_name")=="Naming Series", hide_name_field=False) + + set_by_naming_series( + "Customer", + "customer_name", + self.get("cust_master_name") == "Naming Series", + hide_name_field=False, + ) def toggle_hide_tax_id(self): self.hide_tax_id = cint(self.hide_tax_id) # Make property setters to hide tax_id fields for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"): - make_property_setter(doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False + ) def toggle_editable_rate_for_bundle_items(self): 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) + make_property_setter( + "Packed Item", + "rate", + "read_only", + not (editable_bundle_item_rates), + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/selling/doctype/sms_center/sms_center.py b/erpnext/selling/doctype/sms_center/sms_center.py index d192457ee07..cdc7397e1ec 100644 --- a/erpnext/selling/doctype/sms_center/sms_center.py +++ b/erpnext/selling/doctype/sms_center/sms_center.py @@ -12,59 +12,80 @@ from frappe.utils import cstr class SMSCenter(Document): @frappe.whitelist() def create_receiver_list(self): - rec, where_clause = '', '' - if self.send_to == 'All Customer Contact': + rec, where_clause = "", "" + if self.send_to == "All Customer Contact": where_clause = " and dl.link_doctype = 'Customer'" if self.customer: - where_clause += " and dl.link_name = '%s'" % \ - self.customer.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to == 'All Supplier Contact': + where_clause += ( + " and dl.link_name = '%s'" % self.customer.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to == "All Supplier Contact": where_clause = " and dl.link_doctype = 'Supplier'" if self.supplier: - where_clause += " and dl.link_name = '%s'" % \ - self.supplier.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to == 'All Sales Partner Contact': + where_clause += ( + " and dl.link_name = '%s'" % self.supplier.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to == "All Sales Partner Contact": where_clause = " and dl.link_doctype = 'Sales Partner'" if self.sales_partner: - where_clause += "and dl.link_name = '%s'" % \ - self.sales_partner.replace("'", "\'") or " and ifnull(dl.link_name, '') != ''" - if self.send_to in ['All Contact', 'All Customer Contact', 'All Supplier Contact', 'All Sales Partner Contact']: - rec = frappe.db.sql("""select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')), + where_clause += ( + "and dl.link_name = '%s'" % self.sales_partner.replace("'", "'") + or " and ifnull(dl.link_name, '') != ''" + ) + if self.send_to in [ + "All Contact", + "All Customer Contact", + "All Supplier Contact", + "All Sales Partner Contact", + ]: + rec = frappe.db.sql( + """select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')), c.mobile_no from `tabContact` c, `tabDynamic Link` dl where ifnull(c.mobile_no,'')!='' and - c.docstatus != 2 and dl.parent = c.name%s""" % where_clause) + c.docstatus != 2 and dl.parent = c.name%s""" + % where_clause + ) - elif self.send_to == 'All Lead (Open)': - rec = frappe.db.sql("""select lead_name, mobile_no from `tabLead` where - ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'""") + elif self.send_to == "All Lead (Open)": + rec = frappe.db.sql( + """select lead_name, mobile_no from `tabLead` where + ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'""" + ) - elif self.send_to == 'All Employee (Active)': - where_clause = self.department and " and department = '%s'" % \ - self.department.replace("'", "\'") or "" - where_clause += self.branch and " and branch = '%s'" % \ - self.branch.replace("'", "\'") or "" + elif self.send_to == "All Employee (Active)": + where_clause = ( + self.department and " and department = '%s'" % self.department.replace("'", "'") or "" + ) + where_clause += self.branch and " and branch = '%s'" % self.branch.replace("'", "'") or "" - rec = frappe.db.sql("""select employee_name, cell_number from + rec = frappe.db.sql( + """select employee_name, cell_number from `tabEmployee` where status = 'Active' and docstatus < 2 and - ifnull(cell_number,'')!='' %s""" % where_clause) + ifnull(cell_number,'')!='' %s""" + % where_clause + ) - elif self.send_to == 'All Sales Person': - rec = frappe.db.sql("""select sales_person_name, + elif self.send_to == "All Sales Person": + rec = frappe.db.sql( + """select sales_person_name, tabEmployee.cell_number from `tabSales Person` left join tabEmployee on `tabSales Person`.employee = tabEmployee.name - where ifnull(tabEmployee.cell_number,'')!=''""") + where ifnull(tabEmployee.cell_number,'')!=''""" + ) - rec_list = '' + rec_list = "" for d in rec: - rec_list += d[0] + ' - ' + d[1] + '\n' + rec_list += d[0] + " - " + d[1] + "\n" self.receiver_list = rec_list def get_receiver_nos(self): receiver_nos = [] if self.receiver_list: - for d in self.receiver_list.split('\n'): + for d in self.receiver_list.split("\n"): receiver_no = d - if '-' in d: - receiver_no = receiver_no.split('-')[1] + if "-" in d: + receiver_no = receiver_no.split("-")[1] if receiver_no.strip(): receiver_nos.append(cstr(receiver_no).strip()) else: diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 216e35a3903..4efb1a03f7e 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -20,31 +20,46 @@ def search_by_term(search_term, warehouse, price_list): barcode = result.get("barcode") or "" if result: - item_info = frappe.db.get_value("Item", item_code, - ["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"], - as_dict=1) + item_info = frappe.db.get_value( + "Item", + item_code, + [ + "name as item_code", + "item_name", + "description", + "stock_uom", + "image as item_image", + "is_stock_item", + ], + as_dict=1, + ) item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) - price_list_rate, currency = frappe.db.get_value('Item Price', { - 'price_list': price_list, - 'item_code': item_code - }, ["price_list_rate", "currency"]) or [None, None] + price_list_rate, currency = frappe.db.get_value( + "Item Price", + {"price_list": price_list, "item_code": item_code}, + ["price_list_rate", "currency"], + ) or [None, None] - item_info.update({ - 'serial_no': serial_no, - 'batch_no': batch_no, - 'barcode': barcode, - 'price_list_rate': price_list_rate, - 'currency': currency, - 'actual_qty': item_stock_qty - }) + item_info.update( + { + "serial_no": serial_no, + "batch_no": batch_no, + "barcode": barcode, + "price_list_rate": price_list_rate, + "currency": currency, + "actual_qty": item_stock_qty, + } + ) + + return {"items": [item_info]} - return {'items': [item_info]} @frappe.whitelist() def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""): warehouse, hide_unavailable_items = frappe.db.get_value( - 'POS Profile', pos_profile, ['warehouse', 'hide_unavailable_items']) + "POS Profile", pos_profile, ["warehouse", "hide_unavailable_items"] + ) result = [] @@ -53,20 +68,23 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te if result: return result - if not frappe.db.exists('Item Group', item_group): - item_group = get_root_of('Item Group') + if not frappe.db.exists("Item Group", item_group): + item_group = get_root_of("Item Group") condition = get_conditions(search_term) condition += get_item_group_condition(pos_profile) - lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) + lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) bin_join_selection, bin_join_condition = "", "" if hide_unavailable_items: bin_join_selection = ", `tabBin` bin" - bin_join_condition = "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" + bin_join_condition = ( + "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" + ) - items_data = frappe.db.sql(""" + items_data = frappe.db.sql( + """ SELECT item.name AS item_code, item.item_name, @@ -87,22 +105,26 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ORDER BY item.name asc LIMIT - {start}, {page_length}""" - .format( + {start}, {page_length}""".format( start=start, page_length=page_length, lft=lft, rgt=rgt, condition=condition, bin_join_selection=bin_join_selection, - bin_join_condition=bin_join_condition - ), {'warehouse': warehouse}, as_dict=1) + bin_join_condition=bin_join_condition, + ), + {"warehouse": warehouse}, + as_dict=1, + ) if items_data: items = [d.item_code for d in items_data] - item_prices_data = frappe.get_all("Item Price", - fields = ["item_code", "price_list_rate", "currency"], - filters = {'price_list': price_list, 'item_code': ['in', items]}) + item_prices_data = frappe.get_all( + "Item Price", + fields=["item_code", "price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": ["in", items]}, + ) item_prices = {} for d in item_prices_data: @@ -115,163 +137,190 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te row = {} row.update(item) - row.update({ - 'price_list_rate': item_price.get('price_list_rate'), - 'currency': item_price.get('currency'), - 'actual_qty': item_stock_qty, - }) + row.update( + { + "price_list_rate": item_price.get("price_list_rate"), + "currency": item_price.get("currency"), + "actual_qty": item_stock_qty, + } + ) result.append(row) - return {'items': result} + return {"items": result} + @frappe.whitelist() def search_for_serial_or_batch_or_barcode_number(search_value): # search barcode no - barcode_data = frappe.db.get_value('Item Barcode', {'barcode': search_value}, ['barcode', 'parent as item_code'], as_dict=True) + barcode_data = frappe.db.get_value( + "Item Barcode", {"barcode": search_value}, ["barcode", "parent as item_code"], as_dict=True + ) if barcode_data: return barcode_data # search serial no - serial_no_data = frappe.db.get_value('Serial No', search_value, ['name as serial_no', 'item_code'], as_dict=True) + serial_no_data = frappe.db.get_value( + "Serial No", search_value, ["name as serial_no", "item_code"], as_dict=True + ) if serial_no_data: return serial_no_data # search batch no - batch_no_data = frappe.db.get_value('Batch', search_value, ['name as batch_no', 'item as item_code'], as_dict=True) + batch_no_data = frappe.db.get_value( + "Batch", search_value, ["name as batch_no", "item as item_code"], as_dict=True + ) if batch_no_data: return batch_no_data return {} + def get_conditions(search_term): condition = "(" condition += """item.name like {search_term} - or item.item_name like {search_term}""".format(search_term=frappe.db.escape('%' + search_term + '%')) + or item.item_name like {search_term}""".format( + search_term=frappe.db.escape("%" + search_term + "%") + ) condition += add_search_fields_condition(search_term) condition += ")" return condition + def add_search_fields_condition(search_term): - condition = '' - search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname']) + condition = "" + search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"]) if search_fields: for field in search_fields: - condition += " or item.`{0}` like {1}".format(field['fieldname'], frappe.db.escape('%' + search_term + '%')) + condition += " or item.`{0}` like {1}".format( + field["fieldname"], frappe.db.escape("%" + search_term + "%") + ) return condition + def get_item_group_condition(pos_profile): cond = "and 1=1" item_groups = get_item_groups(pos_profile) if item_groups: - cond = "and item.item_group in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups))) return cond % tuple(item_groups) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_group_query(doctype, txt, searchfield, start, page_len, filters): item_groups = [] cond = "1=1" - pos_profile= filters.get('pos_profile') + pos_profile = filters.get("pos_profile") if pos_profile: item_groups = get_item_groups(pos_profile) if item_groups: - cond = "name in (%s)"%(', '.join(['%s']*len(item_groups))) + cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups))) cond = cond % tuple(item_groups) - return frappe.db.sql(""" select distinct name from `tabItem Group` - where {condition} and (name like %(txt)s) limit {start}, {page_len}""" - .format(condition = cond, start=start, page_len= page_len), - {'txt': '%%%s%%' % txt}) + return frappe.db.sql( + """ select distinct name from `tabItem Group` + where {condition} and (name like %(txt)s) limit {start}, {page_len}""".format( + condition=cond, start=start, page_len=page_len + ), + {"txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() def check_opening_entry(user): - open_vouchers = frappe.db.get_all("POS Opening Entry", - filters = { - "user": user, - "pos_closing_entry": ["in", ["", None]], - "docstatus": 1 - }, - fields = ["name", "company", "pos_profile", "period_start_date"], - order_by = "period_start_date desc" + open_vouchers = frappe.db.get_all( + "POS Opening Entry", + filters={"user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1}, + fields=["name", "company", "pos_profile", "period_start_date"], + order_by="period_start_date desc", ) return open_vouchers + @frappe.whitelist() def create_opening_voucher(pos_profile, company, balance_details): balance_details = json.loads(balance_details) - new_pos_opening = frappe.get_doc({ - 'doctype': 'POS Opening Entry', - "period_start_date": frappe.utils.get_datetime(), - "posting_date": frappe.utils.getdate(), - "user": frappe.session.user, - "pos_profile": pos_profile, - "company": company, - }) + new_pos_opening = frappe.get_doc( + { + "doctype": "POS Opening Entry", + "period_start_date": frappe.utils.get_datetime(), + "posting_date": frappe.utils.getdate(), + "user": frappe.session.user, + "pos_profile": pos_profile, + "company": company, + } + ) new_pos_opening.set("balance_details", balance_details) new_pos_opening.submit() return new_pos_opening.as_dict() + @frappe.whitelist() def get_past_order_list(search_term, status, limit=20): - fields = ['name', 'grand_total', 'currency', 'customer', 'posting_time', 'posting_date'] + fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"] invoice_list = [] if search_term and status: - invoices_by_customer = frappe.db.get_all('POS Invoice', filters={ - 'customer': ['like', '%{}%'.format(search_term)], - 'status': status - }, fields=fields) - invoices_by_name = frappe.db.get_all('POS Invoice', filters={ - 'name': ['like', '%{}%'.format(search_term)], - 'status': status - }, fields=fields) + invoices_by_customer = frappe.db.get_all( + "POS Invoice", + filters={"customer": ["like", "%{}%".format(search_term)], "status": status}, + fields=fields, + ) + invoices_by_name = frappe.db.get_all( + "POS Invoice", + filters={"name": ["like", "%{}%".format(search_term)], "status": status}, + fields=fields, + ) invoice_list = invoices_by_customer + invoices_by_name elif status: - invoice_list = frappe.db.get_all('POS Invoice', filters={ - 'status': status - }, fields=fields) + invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields) return invoice_list + @frappe.whitelist() def set_customer_info(fieldname, customer, value=""): - if fieldname == 'loyalty_program': - frappe.db.set_value('Customer', customer, 'loyalty_program', value) + if fieldname == "loyalty_program": + frappe.db.set_value("Customer", customer, "loyalty_program", value) - contact = frappe.get_cached_value('Customer', customer, 'customer_primary_contact') + contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact") if not contact: - contact = frappe.db.sql(""" + contact = frappe.db.sql( + """ SELECT parent FROM `tabDynamic Link` WHERE parenttype = 'Contact' AND parentfield = 'links' AND link_doctype = 'Customer' AND link_name = %s - """, (customer), as_dict=1) - contact = contact[0].get('parent') if contact else None + """, + (customer), + as_dict=1, + ) + contact = contact[0].get("parent") if contact else None if not contact: - new_contact = frappe.new_doc('Contact') + new_contact = frappe.new_doc("Contact") new_contact.is_primary_contact = 1 new_contact.first_name = customer - new_contact.set('links', [{'link_doctype': 'Customer', 'link_name': customer}]) + new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}]) new_contact.save() contact = new_contact.name - frappe.db.set_value('Customer', customer, 'customer_primary_contact', contact) + frappe.db.set_value("Customer", customer, "customer_primary_contact", contact) - contact_doc = frappe.get_doc('Contact', contact) - if fieldname == 'email_id': - contact_doc.set('email_ids', [{ 'email_id': value, 'is_primary': 1}]) - frappe.db.set_value('Customer', customer, 'email_id', value) - elif fieldname == 'mobile_no': - contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}]) - frappe.db.set_value('Customer', customer, 'mobile_no', value) + contact_doc = frappe.get_doc("Contact", contact) + if fieldname == "email_id": + contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}]) + frappe.db.set_value("Customer", customer, "email_id", value) + elif fieldname == "mobile_no": + contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) + frappe.db.set_value("Customer", customer, "mobile_no", value) contact_doc.save() diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index a75108e4032..c626f5b05fc 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -16,86 +16,153 @@ def validate_filters(from_date, to_date, company): if not company: frappe.throw(_("Please Select a Company")) + @frappe.whitelist() def get_funnel_data(from_date, to_date, company): validate_filters(from_date, to_date, company) - active_leads = frappe.db.sql("""select count(*) from `tabLead` + active_leads = frappe.db.sql( + """select count(*) from `tabLead` where (date(`creation`) between %s and %s) - and company=%s""", (from_date, to_date, company))[0][0] + and company=%s""", + (from_date, to_date, company), + )[0][0] - opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` + opportunities = frappe.db.sql( + """select count(*) from `tabOpportunity` where (date(`creation`) between %s and %s) - and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0] + and opportunity_from='Lead' and company=%s""", + (from_date, to_date, company), + )[0][0] - quotations = frappe.db.sql("""select count(*) from `tabQuotation` + quotations = frappe.db.sql( + """select count(*) from `tabQuotation` where docstatus = 1 and (date(`creation`) between %s and %s) - and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] + and (opportunity!="" or quotation_to="Lead") and company=%s""", + (from_date, to_date, company), + )[0][0] - converted = frappe.db.sql("""select count(*) from `tabCustomer` + converted = frappe.db.sql( + """select count(*) from `tabCustomer` JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name WHERE (date(`tabCustomer`.creation) between %s and %s) - and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] - + and `tabLead`.company=%s""", + (from_date, to_date, company), + )[0][0] return [ - { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" }, - { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, - { "title": _("Quotations"), "value": quotations, "color": "#006685" }, - { "title": _("Converted"), "value": converted, "color": "#00AD65" } + {"title": _("Active Leads"), "value": active_leads, "color": "#B03B46"}, + {"title": _("Opportunities"), "value": opportunities, "color": "#F09C00"}, + {"title": _("Quotations"), "value": quotations, "color": "#006685"}, + {"title": _("Converted"), "value": converted, "color": "#00AD65"}, ] + @frappe.whitelist() def get_opp_by_lead_source(from_date, to_date, company): validate_filters(from_date, to_date, company) - opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability', 'source']) + opportunities = frappe.get_all( + "Opportunity", + filters=[ + ["status", "in", ["Open", "Quotation", "Replied"]], + ["company", "=", company], + ["transaction_date", "Between", [from_date, to_date]], + ], + fields=["currency", "sales_stage", "opportunity_amount", "probability", "source"], + ) if opportunities: - default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') + default_currency = frappe.get_cached_value("Global Defaults", "None", "default_currency") - cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] + cp_opportunities = [ + dict( + x, + **{ + "compound_amount": ( + convert(x["opportunity_amount"], x["currency"], default_currency, to_date) + * x["probability"] + / 100 + ) + } + ) + for x in opportunities + ] - df = pd.DataFrame(cp_opportunities).groupby(['source', 'sales_stage'], as_index=False).agg({'compound_amount': 'sum'}) + df = ( + pd.DataFrame(cp_opportunities) + .groupby(["source", "sales_stage"], as_index=False) + .agg({"compound_amount": "sum"}) + ) result = {} - result['labels'] = list(set(df.source.values)) - result['datasets'] = [] + result["labels"] = list(set(df.source.values)) + result["datasets"] = [] for s in set(df.sales_stage.values): - result['datasets'].append({'name': s, 'values': [0]*len(result['labels']), 'chartType': 'bar'}) + result["datasets"].append( + {"name": s, "values": [0] * len(result["labels"]), "chartType": "bar"} + ) for row in df.itertuples(): - source_index = result['labels'].index(row.source) + source_index = result["labels"].index(row.source) - for dataset in result['datasets']: - if dataset['name'] == row.sales_stage: - dataset['values'][source_index] = row.compound_amount + for dataset in result["datasets"]: + if dataset["name"] == row.sales_stage: + dataset["values"][source_index] = row.compound_amount return result else: - return 'empty' + return "empty" + @frappe.whitelist() def get_pipeline_data(from_date, to_date, company): validate_filters(from_date, to_date, company) - opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability']) + opportunities = frappe.get_all( + "Opportunity", + filters=[ + ["status", "in", ["Open", "Quotation", "Replied"]], + ["company", "=", company], + ["transaction_date", "Between", [from_date, to_date]], + ], + fields=["currency", "sales_stage", "opportunity_amount", "probability"], + ) if opportunities: - default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency') + default_currency = frappe.get_cached_value("Global Defaults", "None", "default_currency") - cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities] + cp_opportunities = [ + dict( + x, + **{ + "compound_amount": ( + convert(x["opportunity_amount"], x["currency"], default_currency, to_date) + * x["probability"] + / 100 + ) + } + ) + for x in opportunities + ] - df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict() + df = ( + pd.DataFrame(cp_opportunities) + .groupby(["sales_stage"], as_index=True) + .agg({"compound_amount": "sum"}) + .to_dict() + ) result = {} - result['labels'] = df['compound_amount'].keys() - result['datasets'] = [] - result['datasets'].append({'name': _("Total Amount"), 'values': df['compound_amount'].values(), 'chartType': 'bar'}) + result["labels"] = df["compound_amount"].keys() + result["datasets"] = [] + result["datasets"].append( + {"name": _("Total Amount"), "values": df["compound_amount"].values(), "chartType": "bar"} + ) return result else: - return 'empty' + return "empty" diff --git a/erpnext/selling/report/address_and_contacts/address_and_contacts.py b/erpnext/selling/report/address_and_contacts/address_and_contacts.py index 319e78f4889..5832d445423 100644 --- a/erpnext/selling/report/address_and_contacts/address_and_contacts.py +++ b/erpnext/selling/report/address_and_contacts/address_and_contacts.py @@ -7,20 +7,30 @@ from six import iteritems from six.moves import range field_map = { - "Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ], - "Address": [ "address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address" ] + "Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"], + "Address": [ + "address_line1", + "address_line2", + "city", + "state", + "pincode", + "country", + "is_primary_address", + ], } + def execute(filters=None): columns, data = get_columns(filters), get_data(filters) return columns, data + def get_columns(filters): party_type = filters.get("party_type") party_type_value = get_party_group(party_type) return [ "{party_type}:Link/{party_type}".format(party_type=party_type), - "{party_value_type}::150".format(party_value_type = frappe.unscrub(str(party_type_value))), + "{party_value_type}::150".format(party_value_type=frappe.unscrub(str(party_type_value))), "Address Line 1", "Address Line 2", "City", @@ -33,9 +43,10 @@ def get_columns(filters): "Phone", "Mobile No", "Email Id", - "Is Primary Contact:Check" + "Is Primary Contact:Check", ] + def get_data(filters): party_type = filters.get("party_type") party = filters.get("party_name") @@ -43,6 +54,7 @@ def get_data(filters): return get_party_addresses_and_contact(party_type, party, party_group) + def get_party_addresses_and_contact(party_type, party, party_group): data = [] filters = None @@ -52,9 +64,11 @@ def get_party_addresses_and_contact(party_type, party, party_group): return [] if party: - filters = { "name": party } + filters = {"name": party} - fetch_party_list = frappe.get_list(party_type, filters=filters, fields=["name", party_group], as_list=True) + fetch_party_list = frappe.get_list( + party_type, filters=filters, fields=["name", party_group], as_list=True + ) party_list = [d[0] for d in fetch_party_list] party_groups = {} for d in fetch_party_list: @@ -68,7 +82,7 @@ def get_party_addresses_and_contact(party_type, party, party_group): for party, details in iteritems(party_details): addresses = details.get("address", []) - contacts = details.get("contact", []) + contacts = details.get("contact", []) if not any([addresses, contacts]): result = [party] result.append(party_groups[party]) @@ -91,10 +105,11 @@ def get_party_addresses_and_contact(party_type, party, party_group): data.append(result) return data + def get_party_details(party_type, party_list, doctype, party_details): - filters = [ + filters = [ ["Dynamic Link", "link_doctype", "=", party_type], - ["Dynamic Link", "link_name", "in", party_list] + ["Dynamic Link", "link_name", "in", party_list], ] fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) @@ -105,15 +120,18 @@ def get_party_details(party_type, party_list, doctype, party_details): return party_details + def add_blank_columns_for(doctype): return ["" for field in field_map.get(doctype, [])] + def get_party_group(party_type): - if not party_type: return + if not party_type: + return group = { "Customer": "customer_group", "Supplier": "supplier_group", - "Sales Partner": "partner_type" + "Sales Partner": "partner_type", } return group[party_type] diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index e702a51d0e7..5e763bb4364 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -7,7 +7,8 @@ from frappe.utils import flt def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() iwq_map = get_item_warehouse_quantity_map() @@ -20,32 +21,49 @@ def execute(filters=None): for wh, item_qty in warehouse.items(): total += 1 if item_map.get(sbom): - row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, - item_map.get(sbom).stock_uom, wh] + row = [ + sbom, + item_map.get(sbom).item_name, + item_map.get(sbom).description, + item_map.get(sbom).stock_uom, + wh, + ] available_qty = item_qty total_qty += flt(available_qty) row += [available_qty] if available_qty: data.append(row) - if (total == len(warehouse)): + if total == len(warehouse): row = ["", "", "Total", "", "", total_qty] data.append(row) return columns, data + def get_columns(): - columns = ["Item Code:Link/Item:100", "Item Name::100", "Description::120", \ - "UOM:Link/UOM:80", "Warehouse:Link/Warehouse:100", "Quantity::100"] + columns = [ + "Item Code:Link/Item:100", + "Item Name::100", + "Description::120", + "UOM:Link/UOM:80", + "Warehouse:Link/Warehouse:100", + "Quantity::100", + ] return columns + def get_item_details(): item_map = {} - for item in frappe.db.sql("""SELECT name, item_name, description, stock_uom - from `tabItem`""", as_dict=1): + for item in frappe.db.sql( + """SELECT name, item_name, description, stock_uom + from `tabItem`""", + as_dict=1, + ): item_map.setdefault(item.name, item) return item_map + def get_item_warehouse_quantity_map(): query = """SELECT parent, warehouse, MIN(qty) AS qty FROM (SELECT b.parent, bi.item_code, bi.warehouse, diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 2426cbb0b55..33badc37f8a 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -12,178 +12,177 @@ from frappe.utils import cint, cstr, getdate def execute(filters=None): common_columns = [ { - 'label': _('New Customers'), - 'fieldname': 'new_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 + "label": _("New Customers"), + "fieldname": "new_customers", + "fieldtype": "Int", + "default": 0, + "width": 125, }, { - 'label': _('Repeat Customers'), - 'fieldname': 'repeat_customers', - 'fieldtype': 'Int', - 'default': 0, - 'width': 125 + "label": _("Repeat Customers"), + "fieldname": "repeat_customers", + "fieldtype": "Int", + "default": 0, + "width": 125, + }, + {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "default": 0, "width": 100}, + { + "label": _("New Customer Revenue"), + "fieldname": "new_customer_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, { - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Int', - 'default': 0, - 'width': 100 + "label": _("Repeat Customer Revenue"), + "fieldname": "repeat_customer_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, { - 'label': _('New Customer Revenue'), - 'fieldname': 'new_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 + "label": _("Total Revenue"), + "fieldname": "total_revenue", + "fieldtype": "Currency", + "default": 0.0, + "width": 175, }, - { - 'label': _('Repeat Customer Revenue'), - 'fieldname': 'repeat_customer_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - }, - { - 'label': _('Total Revenue'), - 'fieldname': 'total_revenue', - 'fieldtype': 'Currency', - 'default': 0.0, - 'width': 175 - } ] - if filters.get('view_type') == 'Monthly': + if filters.get("view_type") == "Monthly": return get_data_by_time(filters, common_columns) else: return get_data_by_territory(filters, common_columns) + def get_data_by_time(filters, common_columns): # key yyyy-mm columns = [ - { - 'label': _('Year'), - 'fieldname': 'year', - 'fieldtype': 'Data', - 'width': 100 - }, - { - 'label': _('Month'), - 'fieldname': 'month', - 'fieldtype': 'Data', - 'width': 100 - }, + {"label": _("Year"), "fieldname": "year", "fieldtype": "Data", "width": 100}, + {"label": _("Month"), "fieldname": "month", "fieldtype": "Data", "width": 100}, ] columns += common_columns customers_in = get_customer_stats(filters) # time series - from_year, from_month, temp = filters.get('from_date').split('-') - to_year, to_month, temp = filters.get('to_date').split('-') + from_year, from_month, temp = filters.get("from_date").split("-") + to_year, to_month, temp = filters.get("to_date").split("-") - from_year, from_month, to_year, to_month = \ - cint(from_year), cint(from_month), cint(to_year), cint(to_month) + from_year, from_month, to_year, to_month = ( + cint(from_year), + cint(from_month), + cint(to_year), + cint(to_month), + ) out = [] - for year in range(from_year, to_year+1): - for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): - key = '{year}-{month:02d}'.format(year=year, month=month) + for year in range(from_year, to_year + 1): + for month in range( + from_month if year == from_year else 1, (to_month + 1) if year == to_year else 13 + ): + key = "{year}-{month:02d}".format(year=year, month=month) data = customers_in.get(key) - new = data['new'] if data else [0, 0.0] - repeat = data['repeat'] if data else [0, 0.0] - out.append({ - 'year': cstr(year), - 'month': calendar.month_name[month], - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1] - }) + new = data["new"] if data else [0, 0.0] + repeat = data["repeat"] if data else [0, 0.0] + out.append( + { + "year": cstr(year), + "month": calendar.month_name[month], + "new_customers": new[0], + "repeat_customers": repeat[0], + "total": new[0] + repeat[0], + "new_customer_revenue": new[1], + "repeat_customer_revenue": repeat[1], + "total_revenue": new[1] + repeat[1], + } + ) return columns, out + def get_data_by_territory(filters, common_columns): - columns = [{ - 'label': 'Territory', - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 150 - }] + columns = [ + { + "label": "Territory", + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 150, + } + ] columns += common_columns customers_in = get_customer_stats(filters, tree_view=True) territory_dict = {} - for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): - territory_dict.update({ - t.name: { - 'parent': t.parent_territory, - 'is_group': t.is_group - } - }) + for t in frappe.db.sql( + """SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft""", as_dict=1 + ): + territory_dict.update({t.name: {"parent": t.parent_territory, "is_group": t.is_group}}) depth_map = frappe._dict() for name, info in territory_dict.items(): - default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 + default = depth_map.get(info["parent"]) + 1 if info["parent"] else 0 depth_map.setdefault(name, default) data = [] for name, indent in depth_map.items(): condition = customers_in.get(name) - new = customers_in[name]['new'] if condition else [0, 0.0] - repeat = customers_in[name]['repeat'] if condition else [0, 0.0] + new = customers_in[name]["new"] if condition else [0, 0.0] + repeat = customers_in[name]["repeat"] if condition else [0, 0.0] temp = { - 'territory': name, - 'parent_territory': territory_dict[name]['parent'], - 'indent': indent, - 'new_customers': new[0], - 'repeat_customers': repeat[0], - 'total': new[0] + repeat[0], - 'new_customer_revenue': new[1], - 'repeat_customer_revenue': repeat[1], - 'total_revenue': new[1] + repeat[1], - 'bold': 0 if indent else 1 + "territory": name, + "parent_territory": territory_dict[name]["parent"], + "indent": indent, + "new_customers": new[0], + "repeat_customers": repeat[0], + "total": new[0] + repeat[0], + "new_customer_revenue": new[1], + "repeat_customer_revenue": repeat[1], + "total_revenue": new[1] + repeat[1], + "bold": 0 if indent else 1, } data.append(temp) - loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) + loop_data = sorted(data, key=lambda k: k["indent"], reverse=True) for ld in loop_data: - if ld['parent_territory']: - parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] + if ld["parent_territory"]: + parent_data = [x for x in data if x["territory"] == ld["parent_territory"]][0] for key in parent_data.keys(): - if key not in ['indent', 'territory', 'parent_territory', 'bold']: + if key not in ["indent", "territory", "parent_territory", "bold"]: parent_data[key] += ld[key] return columns, data, None, None, None, 1 + def get_customer_stats(filters, tree_view=False): - """ Calculates number of new and repeated customers and revenue. """ - company_condition = '' - if filters.get('company'): - company_condition = ' and company=%(company)s' + """Calculates number of new and repeated customers and revenue.""" + company_condition = "" + if filters.get("company"): + company_condition = " and company=%(company)s" customers = [] customers_in = {} - for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + for si in frappe.db.sql( + """select territory, posting_date, customer, base_grand_total from `tabSales Invoice` where docstatus=1 and posting_date <= %(to_date)s - {company_condition} order by posting_date'''.format(company_condition=company_condition), - filters, as_dict=1): + {company_condition} order by posting_date""".format( + company_condition=company_condition + ), + filters, + as_dict=1, + ): - key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') - new_or_repeat = 'new' if si.customer not in customers else 'repeat' - customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) + key = si.territory if tree_view else si.posting_date.strftime("%Y-%m") + new_or_repeat = "new" if si.customer not in customers else "repeat" + customers_in.setdefault(key, {"new": [0, 0.0], "repeat": [0, 0.0]}) # if filters.from_date <= si.posting_date.strftime('%Y-%m-%d'): if getdate(filters.from_date) <= getdate(si.posting_date): - customers_in[key][new_or_repeat][0] += 1 - customers_in[key][new_or_repeat][1] += si.base_grand_total - if new_or_repeat == 'new': + customers_in[key][new_or_repeat][0] += 1 + customers_in[key][new_or_repeat][1] += si.base_grand_total + if new_or_repeat == "new": customers.append(si.customer) return customers_in diff --git a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py index dd49f1355d2..1c10a374b6f 100644 --- a/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py +++ b/erpnext/selling/report/customer_credit_balance/customer_credit_balance.py @@ -10,8 +10,9 @@ from erpnext.selling.doctype.customer.customer import get_credit_limit, get_cust def execute(filters=None): - if not filters: filters = {} - #Check if customer id is according to naming series or customer name + if not filters: + filters = {} + # Check if customer id is according to naming series or customer name customer_naming_type = frappe.db.get_value("Selling Settings", None, "cust_master_name") columns = get_columns(customer_naming_type) @@ -22,8 +23,9 @@ def execute(filters=None): for d in customer_list: row = [] - outstanding_amt = get_customer_outstanding(d.name, filters.get("company"), - ignore_outstanding_sales_order=d.bypass_credit_limit_check) + outstanding_amt = get_customer_outstanding( + d.name, filters.get("company"), ignore_outstanding_sales_order=d.bypass_credit_limit_check + ) credit_limit = get_credit_limit(d.name, filters.get("company")) @@ -31,15 +33,24 @@ def execute(filters=None): if customer_naming_type == "Naming Series": row = [ - d.name, d.customer_name, credit_limit, - outstanding_amt, bal, d.bypass_credit_limit_check, - d.is_frozen, d.disabled + d.name, + d.customer_name, + credit_limit, + outstanding_amt, + bal, + d.bypass_credit_limit_check, + d.is_frozen, + d.disabled, ] else: row = [ - d.name, credit_limit, outstanding_amt, bal, - d.bypass_credit_limit_check, d.is_frozen, - d.disabled + d.name, + credit_limit, + outstanding_amt, + bal, + d.bypass_credit_limit_check, + d.is_frozen, + d.disabled, ] if credit_limit: @@ -47,6 +58,7 @@ def execute(filters=None): return columns, data + def get_columns(customer_naming_type): columns = [ _("Customer") + ":Link/Customer:120", @@ -63,6 +75,7 @@ def get_columns(customer_naming_type): return columns + def get_details(filters): sql_query = """SELECT diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py index e5f93543209..a58f40362ba 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py @@ -30,32 +30,23 @@ def get_columns(filters=None): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 150 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Selling Rate"), - "fieldname": "selling_rate", - "fieldtype": "Currency" + "width": 150, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Selling Rate"), "fieldname": "selling_rate", "fieldtype": "Currency"}, { "label": _("Available Stock"), "fieldname": "available_stock", "fieldtype": "Float", - "width": 150 + "width": 150, }, { "label": _("Price List"), "fieldname": "price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 - } + "width": 120, + }, ] @@ -64,30 +55,33 @@ def get_data(filters=None): customer_details = get_customer_details(filters) items = get_selling_items(filters) - item_stock_map = frappe.get_all("Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code") + item_stock_map = frappe.get_all( + "Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code" + ) item_stock_map = {item.item_code: item.available for item in item_stock_map} for item in items: price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0 available_stock = item_stock_map.get(item.item_code) - data.append({ - "item_code": item.item_code, - "item_name": item.item_name, - "selling_rate": price_list_rate, - "price_list": customer_details.get("price_list"), - "available_stock": available_stock, - }) + data.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "selling_rate": price_list_rate, + "price_list": customer_details.get("price_list"), + "available_stock": available_stock, + } + ) return data def get_customer_details(filters): customer_details = get_party_details(party=filters.get("customer"), party_type="Customer") - customer_details.update({ - "company": get_default_company(), - "price_list": customer_details.get("selling_price_list") - }) + customer_details.update( + {"company": get_default_company(), "price_list": customer_details.get("selling_price_list")} + ) return customer_details @@ -98,6 +92,8 @@ def get_selling_items(filters): else: item_filters = {"is_sales_item": 1, "disabled": 0} - items = frappe.get_all("Item", filters=item_filters, fields=["item_code", "item_name"], order_by="item_name") + items = frappe.get_all( + "Item", filters=item_filters, fields=["item_code", "item_name"], order_by="item_name" + ) return items diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index d97e1c6dcb0..1b337fc495e 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -8,7 +8,8 @@ from frappe.utils import cint def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} days_since_last_order = filters.get("days_since_last_order") doctype = filters.get("doctype") @@ -22,10 +23,11 @@ def execute(filters=None): data = [] for cust in customers: if cint(cust[8]) >= cint(days_since_last_order): - cust.insert(7,get_last_sales_amt(cust[0], doctype)) + cust.insert(7, get_last_sales_amt(cust[0], doctype)) data.append(cust) return columns, data + def get_sales_details(doctype): cond = """sum(so.base_net_total) as 'total_order_considered', max(so.posting_date) as 'last_order_date', @@ -37,7 +39,8 @@ def get_sales_details(doctype): max(so.transaction_date) as 'last_order_date', DATEDIFF(CURDATE(), max(so.transaction_date)) as 'days_since_last_order'""" - return frappe.db.sql("""select + return frappe.db.sql( + """select cust.name, cust.customer_name, cust.territory, @@ -47,18 +50,29 @@ def get_sales_details(doctype): from `tabCustomer` cust, `tab{1}` so where cust.name = so.customer and so.docstatus = 1 group by cust.name - order by 'days_since_last_order' desc """.format(cond, doctype), as_list=1) + order by 'days_since_last_order' desc """.format( + cond, doctype + ), + as_list=1, + ) + def get_last_sales_amt(customer, doctype): cond = "posting_date" - if doctype =="Sales Order": + if doctype == "Sales Order": cond = "transaction_date" - res = frappe.db.sql("""select base_net_total from `tab{0}` + res = frappe.db.sql( + """select base_net_total from `tab{0}` where customer = %s and docstatus = 1 order by {1} desc - limit 1""".format(doctype, cond), customer) + limit 1""".format( + doctype, cond + ), + customer, + ) return res and res[0][0] or 0 + def get_columns(): return [ _("Customer") + ":Link/Customer:120", @@ -70,5 +84,5 @@ def get_columns(): _("Total Order Considered") + ":Currency:160", _("Last Order Amount") + ":Currency:160", _("Last Order Date") + ":Date:160", - _("Days Since Last Order") + "::160" + _("Days Since Last Order") + "::160", ] diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index f0684f71cd7..12ca7b3ff83 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -20,6 +20,7 @@ def execute(filters=None): return columns, data, None, chart_data + def get_columns(filters): return [ { @@ -27,120 +28,85 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "item_code", "options": "Item", - "width": 120 - }, - { - "label": _("Item Name"), - "fieldtype": "Data", - "fieldname": "item_name", - "width": 140 + "width": 120, }, + {"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": 140}, { "label": _("Item Group"), "fieldtype": "Link", "fieldname": "item_group", "options": "Item Group", - "width": 120 - }, - { - "label": _("Description"), - "fieldtype": "Data", - "fieldname": "description", - "width": 150 - }, - { - "label": _("Quantity"), - "fieldtype": "Float", - "fieldname": "quantity", - "width": 150 - }, - { - "label": _("UOM"), - "fieldtype": "Link", - "fieldname": "uom", - "options": "UOM", - "width": 100 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "options": "Currency", - "width": 120 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "options": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150}, + {"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150}, + {"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120}, + {"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120}, { "label": _("Sales Order"), "fieldtype": "Link", "fieldname": "sales_order", "options": "Sales Order", - "width": 100 + "width": 100, }, { "label": _("Transaction Date"), "fieldtype": "Date", "fieldname": "transaction_date", - "width": 90 + "width": 90, }, { "label": _("Customer"), "fieldtype": "Link", "fieldname": "customer", "options": "Customer", - "width": 100 - }, - { - "label": _("Customer Name"), - "fieldtype": "Data", - "fieldname": "customer_name", - "width": 140 + "width": 100, }, + {"label": _("Customer Name"), "fieldtype": "Data", "fieldname": "customer_name", "width": 140}, { "label": _("Customer Group"), "fieldtype": "Link", "fieldname": "customer_group", "options": "Customer Group", - "width": 120 + "width": 120, }, { "label": _("Territory"), "fieldtype": "Link", "fieldname": "territory", "options": "Territory", - "width": 100 + "width": 100, }, { "label": _("Project"), "fieldtype": "Link", "fieldname": "project", "options": "Project", - "width": 100 + "width": 100, }, { "label": _("Delivered Quantity"), "fieldtype": "Float", "fieldname": "delivered_quantity", - "width": 150 + "width": 150, }, { "label": _("Billed Amount"), "fieldtype": "currency", "fieldname": "billed_amount", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldtype": "Link", "fieldname": "company", "options": "Company", - "width": 100 - } + "width": 100, + }, ] + def get_data(filters): data = [] @@ -156,59 +122,60 @@ def get_data(filters): customer_record = customer_details.get(record.customer) item_record = item_details.get(record.item_code) row = { - "item_code": record.get('item_code'), - "item_name": item_record.get('item_name'), - "item_group": item_record.get('item_group'), - "description": record.get('description'), - "quantity": record.get('qty'), - "uom": record.get('uom'), - "rate": record.get('base_rate'), - "amount": record.get('base_amount'), - "sales_order": record.get('name'), - "transaction_date": record.get('transaction_date'), - "customer": record.get('customer'), - "customer_name": customer_record.get('customer_name'), - "customer_group": customer_record.get('customer_group'), - "territory": record.get('territory'), - "project": record.get('project'), - "delivered_quantity": flt(record.get('delivered_qty')), - "billed_amount": flt(record.get('billed_amt')), - "company": record.get('company') + "item_code": record.get("item_code"), + "item_name": item_record.get("item_name"), + "item_group": item_record.get("item_group"), + "description": record.get("description"), + "quantity": record.get("qty"), + "uom": record.get("uom"), + "rate": record.get("base_rate"), + "amount": record.get("base_amount"), + "sales_order": record.get("name"), + "transaction_date": record.get("transaction_date"), + "customer": record.get("customer"), + "customer_name": customer_record.get("customer_name"), + "customer_group": customer_record.get("customer_group"), + "territory": record.get("territory"), + "project": record.get("project"), + "delivered_quantity": flt(record.get("delivered_qty")), + "billed_amount": flt(record.get("billed_amt")), + "company": record.get("company"), } data.append(row) return data + def get_conditions(filters): - conditions = '' - if filters.get('item_group'): - conditions += "AND so_item.item_group = %s" %frappe.db.escape(filters.item_group) + conditions = "" + if filters.get("item_group"): + conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group) - if filters.get('from_date'): - conditions += "AND so.transaction_date >= '%s'" %filters.from_date + if filters.get("from_date"): + conditions += "AND so.transaction_date >= '%s'" % filters.from_date - if filters.get('to_date'): - conditions += "AND so.transaction_date <= '%s'" %filters.to_date + if filters.get("to_date"): + conditions += "AND so.transaction_date <= '%s'" % filters.to_date if filters.get("item_code"): - conditions += "AND so_item.item_code = %s" %frappe.db.escape(filters.item_code) + conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code) if filters.get("customer"): - conditions += "AND so.customer = %s" %frappe.db.escape(filters.customer) + conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer) return conditions + def get_customer_details(): - details = frappe.get_all("Customer", - fields=["name", "customer_name", "customer_group"]) + details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"]) customer_details = {} for d in details: - customer_details.setdefault(d.name, frappe._dict({ - "customer_name": d.customer_name, - "customer_group": d.customer_group - })) + customer_details.setdefault( + d.name, frappe._dict({"customer_name": d.customer_name, "customer_group": d.customer_group}) + ) return customer_details + def get_item_details(): details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"]) item_details = {} @@ -218,10 +185,12 @@ def get_item_details(): ) return item_details + def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so_item.item_code, so_item.description, so_item.qty, so_item.uom, so_item.base_rate, so_item.base_amount, @@ -234,7 +203,13 @@ def get_sales_order_details(company_list, filters): so.name = so_item.parent AND so.company in ({0}) AND so.docstatus = 1 {1} - """.format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1) + """.format( + ",".join(["%s"] * len(company_list)), conditions + ), + tuple(company_list), + as_dict=1, + ) + def get_chart_data(data): item_wise_sales_map = {} @@ -248,21 +223,19 @@ def get_chart_data(data): item_wise_sales_map[item_key] = flt(item_wise_sales_map[item_key]) + flt(row.get("amount")) - item_wise_sales_map = { item: value for item, value in (sorted(item_wise_sales_map.items(), key = lambda i: i[1], reverse=True))} + item_wise_sales_map = { + item: value + for item, value in (sorted(item_wise_sales_map.items(), key=lambda i: i[1], reverse=True)) + } for key in item_wise_sales_map: labels.append(key) datapoints.append(item_wise_sales_map[key]) return { - "data" : { - "labels" : labels[:30], # show max of 30 items in chart - "datasets" : [ - { - "name" : _(" Total Sales Amount"), - "values" : datapoints[:30] - } - ] + "data": { + "labels": labels[:30], # show max of 30 items in chart + "datasets": [{"name": _(" Total Sales Amount"), "values": datapoints[:30]}], }, - "type" : "bar" + "type": "bar", } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index e6a56eea310..7f797f67eee 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -62,12 +62,7 @@ def get_columns(): "fieldname": "status", "fieldtype": "Data", }, - { - "label": _("Currency"), - "fieldname": "currency", - "fieldtype": "Currency", - "hidden": 1 - } + {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "hidden": 1}, ] return columns @@ -156,7 +151,7 @@ def set_payment_terms_statuses(sales_orders, invoices, filters): """ for so in sales_orders: - so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + so.currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") so.invoices = "" for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]: if so.base_payment_amount - so.paid_amount > 0: @@ -182,8 +177,14 @@ def prepare_chart(s_orders): "data": { "labels": [term.payment_term for term in s_orders], "datasets": [ - {"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],}, - {"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],}, + { + "name": "Payment Amount", + "values": [x.base_payment_amount for x in s_orders], + }, + { + "name": "Paid Amount", + "values": [x.paid_amount for x in s_orders], + }, ], }, "type": "bar", diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index f7f8a5dbce3..89940a6e872 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -94,7 +94,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "currency": "INR", "base_payment_amount": 500000.0, "paid_amount": 500000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, { "name": so.name, @@ -107,25 +107,29 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "currency": "INR", "base_payment_amount": 500000.0, "paid_amount": 100000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, ] self.assertEqual(data, expected_value) def create_exchange_rate(self, date): # make an entry in Currency Exchange list. serves as a static exchange rate - if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}): + if frappe.db.exists( + {"doctype": "Currency Exchange", "date": date, "from_currency": "USD", "to_currency": "INR"} + ): return else: - doc = frappe.get_doc({ - 'doctype': "Currency Exchange", - 'date': date, - 'from_currency': 'USD', - 'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'), - 'exchange_rate': 70, - 'for_buying': True, - 'for_selling': True - }) + doc = frappe.get_doc( + { + "doctype": "Currency Exchange", + "date": date, + "from_currency": "USD", + "to_currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), + "exchange_rate": 70, + "for_buying": True, + "for_selling": True, + } + ) doc.insert() def test_alternate_currency(self): @@ -176,10 +180,10 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 6, 30), "invoice_portion": 50.0, - "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), "base_payment_amount": 3500000.0, "paid_amount": 3500000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, { "name": so.name, @@ -189,10 +193,10 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "description": "_Test 50-50", "due_date": datetime.date(2021, 7, 15), "invoice_portion": 50.0, - "currency": frappe.get_cached_value("Company", '_Test Company','default_currency'), + "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), "base_payment_amount": 3500000.0, "paid_amount": 700000.0, - "invoices": ","+sinv.name, + "invoices": "," + sinv.name, }, ] self.assertEqual(data, expected_value) diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py index 01421e8fd0e..cc1055c787d 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py @@ -12,6 +12,7 @@ def execute(filters=None): data = get_data() return columns, data + def get_columns(): columns = [ { @@ -19,80 +20,37 @@ def get_columns(): "options": "Item", "fieldname": "item_code", "fieldtype": "Link", - "width": 200 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 140 + "width": 200, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 140}, { "label": _("S.O. No."), "options": "Sales Order", "fieldname": "sales_order_no", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 140 + "width": 140, }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 140}, { "label": _("Material Request"), "fieldname": "material_request", "fieldtype": "Data", - "width": 140 + "width": 140, }, - { - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Data", - "width": 140 - }, - { - "label": _("SO Qty"), - "fieldname": "so_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Requested Qty"), - "fieldname": "requested_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Pending Qty"), - "fieldname": "pending_qty", - "fieldtype": "Float", - "width": 140 - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Data", - "width": 140 - } + {"label": _("Customer"), "fieldname": "customer", "fieldtype": "Data", "width": 140}, + {"label": _("Territory"), "fieldname": "territory", "fieldtype": "Data", "width": 140}, + {"label": _("SO Qty"), "fieldname": "so_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Requested Qty"), "fieldname": "requested_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Pending Qty"), "fieldname": "pending_qty", "fieldtype": "Float", "width": 140}, + {"label": _("Company"), "fieldname": "company", "fieldtype": "Data", "width": 140}, ] return columns + def get_data(): - sales_order_entry = frappe.db.sql(""" + sales_order_entry = frappe.db.sql( + """ SELECT so_item.item_code, so_item.item_name, @@ -110,88 +68,94 @@ def get_data(): and so.status not in ("Closed","Completed","Cancelled") GROUP BY so.name,so_item.item_code - """, as_dict = 1) + """, + as_dict=1, + ) sales_orders = [row.name for row in sales_order_entry] - mr_records = frappe.get_all("Material Request Item", + mr_records = frappe.get_all( + "Material Request Item", {"sales_order": ("in", sales_orders), "docstatus": 1}, - ["parent", "qty", "sales_order", "item_code"]) + ["parent", "qty", "sales_order", "item_code"], + ) bundled_item_map = get_packed_items(sales_orders) - item_with_product_bundle = get_items_with_product_bundle([row.item_code for row in sales_order_entry]) + item_with_product_bundle = get_items_with_product_bundle( + [row.item_code for row in sales_order_entry] + ) materials_request_dict = {} for record in mr_records: key = (record.sales_order, record.item_code) if key not in materials_request_dict: - materials_request_dict.setdefault(key, { - 'qty': 0, - 'material_requests': [record.parent] - }) + materials_request_dict.setdefault(key, {"qty": 0, "material_requests": [record.parent]}) details = materials_request_dict.get(key) - details['qty'] += record.qty + details["qty"] += record.qty - if record.parent not in details.get('material_requests'): - details['material_requests'].append(record.parent) + if record.parent not in details.get("material_requests"): + details["material_requests"].append(record.parent) pending_so = [] for so in sales_order_entry: if so.item_code not in item_with_product_bundle: material_requests_against_so = materials_request_dict.get((so.name, so.item_code)) or {} # check for pending sales order - if flt(so.total_qty) > flt(material_requests_against_so.get('qty')): + if flt(so.total_qty) > flt(material_requests_against_so.get("qty")): so_record = { "item_code": so.item_code, "item_name": so.item_name, "description": so.description, "sales_order_no": so.name, "date": so.transaction_date, - "material_request": ','.join(material_requests_against_so.get('material_requests', [])), + "material_request": ",".join(material_requests_against_so.get("material_requests", [])), "customer": so.customer, "territory": so.territory, "so_qty": so.total_qty, - "requested_qty": material_requests_against_so.get('qty'), - "pending_qty": so.total_qty - flt(material_requests_against_so.get('qty')), - "company": so.company + "requested_qty": material_requests_against_so.get("qty"), + "pending_qty": so.total_qty - flt(material_requests_against_so.get("qty")), + "company": so.company, } pending_so.append(so_record) else: for item in bundled_item_map.get((so.name, so.item_code), []): material_requests_against_so = materials_request_dict.get((so.name, item.item_code)) or {} - if flt(item.qty) > flt(material_requests_against_so.get('qty')): + if flt(item.qty) > flt(material_requests_against_so.get("qty")): so_record = { "item_code": item.item_code, "item_name": item.item_name, "description": item.description, "sales_order_no": so.name, "date": so.transaction_date, - "material_request": ','.join(material_requests_against_so.get('material_requests', [])), + "material_request": ",".join(material_requests_against_so.get("material_requests", [])), "customer": so.customer, "territory": so.territory, "so_qty": item.qty, - "requested_qty": material_requests_against_so.get('qty', 0), - "pending_qty": item.qty - flt(material_requests_against_so.get('qty', 0)), - "company": so.company + "requested_qty": material_requests_against_so.get("qty", 0), + "pending_qty": item.qty - flt(material_requests_against_so.get("qty", 0)), + "company": so.company, } pending_so.append(so_record) - return pending_so + def get_items_with_product_bundle(item_list): - bundled_items = frappe.get_all("Product Bundle", filters = [ - ("new_item_code", "IN", item_list) - ], fields = ["new_item_code"]) + bundled_items = frappe.get_all( + "Product Bundle", filters=[("new_item_code", "IN", item_list)], fields=["new_item_code"] + ) return [d.new_item_code for d in bundled_items] + def get_packed_items(sales_order_list): - packed_items = frappe.get_all("Packed Item", filters = [ - ("parent", "IN", sales_order_list) - ], fields = ["parent_item", "item_code", "qty", "item_name", "description", "parent"]) + packed_items = frappe.get_all( + "Packed Item", + filters=[("parent", "IN", sales_order_list)], + fields=["parent_item", "item_code", "qty", "item_name", "description", "parent"], + ) bundled_item_map = frappe._dict() for d in packed_items: diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py index 16162acc8f3..e4ad5c622b8 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py @@ -13,18 +13,18 @@ from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_ite class TestPendingSOItemsForPurchaseRequest(FrappeTestCase): - def test_result_for_partial_material_request(self): - so = make_sales_order() - mr=make_material_request(so.name) - mr.items[0].qty = 4 - mr.schedule_date = add_months(nowdate(),1) - mr.submit() - report = execute() - l = len(report[1]) - self.assertEqual((so.items[0].qty - mr.items[0].qty), report[1][l-1]['pending_qty']) + def test_result_for_partial_material_request(self): + so = make_sales_order() + mr = make_material_request(so.name) + mr.items[0].qty = 4 + mr.schedule_date = add_months(nowdate(), 1) + mr.submit() + report = execute() + l = len(report[1]) + self.assertEqual((so.items[0].qty - mr.items[0].qty), report[1][l - 1]["pending_qty"]) - def test_result_for_so_item(self): - so = make_sales_order() - report = execute() - l = len(report[1]) - self.assertEqual(so.items[0].qty, report[1][l-1]['pending_qty']) + def test_result_for_so_item(self): + so = make_sales_order() + report = execute() + l = len(report[1]) + self.assertEqual(so.items[0].qty, report[1][l - 1]["pending_qty"]) diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.py b/erpnext/selling/report/quotation_trends/quotation_trends.py index 047b09081af..dfcec22cca2 100644 --- a/erpnext/selling/report/quotation_trends/quotation_trends.py +++ b/erpnext/selling/report/quotation_trends/quotation_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Quotation") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, conditions, filters): if not (data and conditions): return [] @@ -29,32 +31,27 @@ def get_chart_data(data, conditions, filters): # fetch only periodic columns as labels columns = conditions.get("columns")[start:-2][1::2] - labels = [column.split(':')[0] for column in columns] + labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start-1]: + if not row[start - 1]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[1::2] for i in range(len(row)): datapoints[i] += row[i] return { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : _("{0}").format(filters.get("period")) + _(" Quoted Amount"), - "values" : datapoints - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("{0}").format(filters.get("period")) + _(" Quoted Amount"), "values": datapoints} + ], }, - "type" : "line", - "lineOptions": { - "regionFill": 1 - } + "type": "line", + "lineOptions": {"regionFill": 1}, } diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index a380f842ebf..f1aada3e185 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -13,12 +13,29 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): return Analytics(filters).run() + class Analytics(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) - self.date_field = 'transaction_date' \ - if self.filters.doc_type in ['Sales Order', 'Purchase Order'] else 'posting_date' - self.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + self.date_field = ( + "transaction_date" + if self.filters.doc_type in ["Sales Order", "Purchase Order"] + else "posting_date" + ) + self.months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] self.get_period_date_ranges() def run(self): @@ -35,52 +52,52 @@ class Analytics(object): return self.columns, self.data, None, self.chart, None, skip_total_row def get_columns(self): - self.columns = [{ + self.columns = [ + { "label": _(self.filters.tree_type), "options": self.filters.tree_type if self.filters.tree_type != "Order Type" else "", "fieldname": "entity", "fieldtype": "Link" if self.filters.tree_type != "Order Type" else "Data", - "width": 140 if self.filters.tree_type != "Order Type" else 200 - }] + "width": 140 if self.filters.tree_type != "Order Type" else 200, + } + ] if self.filters.tree_type in ["Customer", "Supplier", "Item"]: - self.columns.append({ - "label": _(self.filters.tree_type + " Name"), - "fieldname": "entity_name", - "fieldtype": "Data", - "width": 140 - }) + self.columns.append( + { + "label": _(self.filters.tree_type + " Name"), + "fieldname": "entity_name", + "fieldtype": "Data", + "width": 140, + } + ) if self.filters.tree_type == "Item": - self.columns.append({ - "label": _("UOM"), - "fieldname": 'stock_uom', - "fieldtype": "Link", - "options": "UOM", - "width": 100 - }) + self.columns.append( + { + "label": _("UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, + } + ) for end_date in self.periodic_daterange: period = self.get_period(end_date) - self.columns.append({ - "label": _(period), - "fieldname": scrub(period), - "fieldtype": "Float", - "width": 120 - }) + self.columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) - self.columns.append({ - "label": _("Total"), - "fieldname": "total", - "fieldtype": "Float", - "width": 120 - }) + self.columns.append( + {"label": _("Total"), "fieldname": "total", "fieldtype": "Float", "width": 120} + ) def get_data(self): if self.filters.tree_type in ["Customer", "Supplier"]: self.get_sales_transactions_based_on_customers_or_suppliers() self.get_rows() - elif self.filters.tree_type == 'Item': + elif self.filters.tree_type == "Item": self.get_sales_transactions_based_on_items() self.get_rows() @@ -88,7 +105,7 @@ class Analytics(object): self.get_sales_transactions_based_on_customer_or_territory_group() self.get_rows_by_group() - elif self.filters.tree_type == 'Item Group': + elif self.filters.tree_type == "Item Group": self.get_sales_transactions_based_on_item_group() self.get_rows_by_group() @@ -104,40 +121,45 @@ class Analytics(object): self.get_rows() def get_sales_transactions_based_on_order_type(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total" else: value_field = "total_qty" - self.entries = frappe.db.sql(""" select s.order_type as entity, s.{value_field} as value_field, s.{date_field} + self.entries = frappe.db.sql( + """ select s.order_type as entity, s.{value_field} as value_field, s.{date_field} from `tab{doctype}` s where s.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s and ifnull(s.order_type, '') != '' order by s.order_type - """ - .format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.get_teams() def get_sales_transactions_based_on_customers_or_suppliers(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" - if self.filters.tree_type == 'Customer': + if self.filters.tree_type == "Customer": entity = "customer as entity" entity_name = "customer_name as entity_name" else: entity = "supplier as entity" entity_name = "supplier_name as entity_name" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity, entity_name, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) self.entity_names = {} @@ -146,80 +168,91 @@ class Analytics(object): def get_sales_transactions_based_on_items(self): - if self.filters["value_quantity"] == 'Value': - value_field = 'base_amount' + if self.filters["value_quantity"] == "Value": + value_field = "base_amount" else: - value_field = 'stock_qty' + value_field = "stock_qty" - self.entries = frappe.db.sql(""" + self.entries = frappe.db.sql( + """ select i.item_code as entity, i.item_name as entity_name, i.stock_uom, i.{value_field} as value_field, s.{date_field} from `tab{doctype} Item` i , `tab{doctype}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - """ - .format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.entity_names = {} for d in self.entries: self.entity_names.setdefault(d.entity, d.entity_name) def get_sales_transactions_based_on_customer_or_territory_group(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" - if self.filters.tree_type == 'Customer Group': - entity_field = 'customer_group as entity' - elif self.filters.tree_type == 'Supplier Group': + if self.filters.tree_type == "Customer Group": + entity_field = "customer_group as entity" + elif self.filters.tree_type == "Supplier Group": entity_field = "supplier as entity" self.get_supplier_parent_child_map() else: entity_field = "territory as entity" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity_field, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) self.get_groups() def get_sales_transactions_based_on_item_group(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_amount" else: value_field = "qty" - self.entries = frappe.db.sql(""" + self.entries = frappe.db.sql( + """ select i.item_group as entity, i.{value_field} as value_field, s.{date_field} from `tab{doctype} Item` i , `tab{doctype}` s where s.name = i.parent and i.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - """.format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), as_dict=1) + """.format( + date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type + ), + (self.filters.company, self.filters.from_date, self.filters.to_date), + as_dict=1, + ) self.get_groups() def get_sales_transactions_based_on_project(self): - if self.filters["value_quantity"] == 'Value': + if self.filters["value_quantity"] == "Value": value_field = "base_net_total as value_field" else: value_field = "total_qty as value_field" entity = "project as entity" - self.entries = frappe.get_all(self.filters.doc_type, + self.entries = frappe.get_all( + self.filters.doc_type, fields=[entity, value_field, self.date_field], filters={ "docstatus": 1, "company": self.filters.company, "project": ["!=", ""], - self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) - } + self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), + }, ) def get_rows(self): @@ -229,7 +262,7 @@ class Analytics(object): for entity, period_data in iteritems(self.entity_periodic_data): row = { "entity": entity, - "entity_name": self.entity_names.get(entity) if hasattr(self, 'entity_names') else None + "entity_name": self.entity_names.get(entity) if hasattr(self, "entity_names") else None, } total = 0 for end_date in self.periodic_daterange: @@ -250,10 +283,7 @@ class Analytics(object): out = [] for d in reversed(self.group_entries): - row = { - "entity": d.name, - "indent": self.depth_map.get(d.name) - } + row = {"entity": d.name, "indent": self.depth_map.get(d.name)} total = 0 for end_date in self.periodic_daterange: period = self.get_period(end_date) @@ -280,14 +310,14 @@ class Analytics(object): self.entity_periodic_data[d.entity][period] += flt(d.value_field) if self.filters.tree_type == "Item": - self.entity_periodic_data[d.entity]['stock_uom'] = d.stock_uom + self.entity_periodic_data[d.entity]["stock_uom"] = d.stock_uom def get_period(self, posting_date): - if self.filters.range == 'Weekly': + if self.filters.range == "Weekly": period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) - elif self.filters.range == 'Monthly': + elif self.filters.range == "Monthly": period = str(self.months[posting_date.month - 1]) + " " + str(posting_date.year) - elif self.filters.range == 'Quarterly': + elif self.filters.range == "Quarterly": period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) else: year = get_fiscal_year(posting_date, company=self.filters.company) @@ -296,16 +326,14 @@ class Analytics(object): def get_period_date_ranges(self): from dateutil.relativedelta import MO, relativedelta + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) - increment = { - "Monthly": 1, - "Quarterly": 3, - "Half-Yearly": 6, - "Yearly": 12 - }.get(self.filters.range, 1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( + self.filters.range, 1 + ) - if self.filters.range in ['Monthly', 'Quarterly']: + if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) elif self.filters.range == "Yearly": from_date = get_fiscal_year(from_date)[1] @@ -330,19 +358,23 @@ class Analytics(object): def get_groups(self): if self.filters.tree_type == "Territory": - parent = 'parent_territory' + parent = "parent_territory" if self.filters.tree_type == "Customer Group": - parent = 'parent_customer_group' + parent = "parent_customer_group" if self.filters.tree_type == "Item Group": - parent = 'parent_item_group' + parent = "parent_item_group" if self.filters.tree_type == "Supplier Group": - parent = 'parent_supplier_group' + parent = "parent_supplier_group" self.depth_map = frappe._dict() - self.group_entries = frappe.db.sql("""select name, lft, rgt , {parent} as parent - from `tab{tree}` order by lft""" - .format(tree=self.filters.tree_type, parent=parent), as_dict=1) + self.group_entries = frappe.db.sql( + """select name, lft, rgt , {parent} as parent + from `tab{tree}` order by lft""".format( + tree=self.filters.tree_type, parent=parent + ), + as_dict=1, + ) for d in self.group_entries: if d.parent: @@ -353,11 +385,15 @@ class Analytics(object): def get_teams(self): self.depth_map = frappe._dict() - self.group_entries = frappe.db.sql(""" select * from (select "Order Types" as name, 0 as lft, + self.group_entries = frappe.db.sql( + """ select * from (select "Order Types" as name, 0 as lft, 2 as rgt, '' as parent union select distinct order_type as name, 1 as lft, 1 as rgt, "Order Types" as parent from `tab{doctype}` where ifnull(order_type, '') != '') as b order by lft, name - """ - .format(doctype=self.filters.doc_type), as_dict=1) + """.format( + doctype=self.filters.doc_type + ), + as_dict=1, + ) for d in self.group_entries: if d.parent: @@ -366,21 +402,17 @@ class Analytics(object): self.depth_map.setdefault(d.name, 0) def get_supplier_parent_child_map(self): - self.parent_child_map = frappe._dict(frappe.db.sql(""" select name, supplier_group from `tabSupplier`""")) + self.parent_child_map = frappe._dict( + frappe.db.sql(""" select name, supplier_group from `tabSupplier`""") + ) def get_chart_data(self): length = len(self.columns) if self.filters.tree_type in ["Customer", "Supplier"]: - labels = [d.get("label") for d in self.columns[2:length - 1]] + labels = [d.get("label") for d in self.columns[2 : length - 1]] elif self.filters.tree_type == "Item": - labels = [d.get("label") for d in self.columns[3:length - 1]] + labels = [d.get("label") for d in self.columns[3 : length - 1]] else: - labels = [d.get("label") for d in self.columns[1:length - 1]] - self.chart = { - "data": { - 'labels': labels, - 'datasets': [] - }, - "type": "line" - } + labels = [d.get("label") for d in self.columns[1 : length - 1]] + self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index 564f48fef3b..15f06d9c9b8 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -19,16 +19,15 @@ class TestAnalytics(FrappeTestCase): self.compare_result_for_customer_group() self.compare_result_for_customer_based_on_quantity() - def compare_result_for_customer(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Value' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Value", } report = execute(filters) @@ -49,7 +48,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 2000.0, "mar_2018": 0.0, - "total":2000.0 + "total": 2000.0, }, { "entity": "_Test Customer 2", @@ -66,7 +65,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total":2500.0 + "total": 2500.0, }, { "entity": "_Test Customer 3", @@ -83,21 +82,21 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total": 3000.0 - } + "total": 3000.0, + }, ] - result = sorted(report[1], key=lambda k: k['entity']) + result = sorted(report[1], key=lambda k: k["entity"]) self.assertEqual(expected_data, result) def compare_result_for_customer_group(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer Group', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Value' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer Group", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Value", } report = execute(filters) @@ -117,19 +116,19 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 2000.0, "mar_2018": 0.0, - "total":7500.0 + "total": 7500.0, } self.assertEqual(expected_first_row, report[1][0]) def compare_result_for_customer_based_on_quantity(self): filters = { - 'doc_type': 'Sales Order', - 'range': 'Monthly', - 'to_date': '2018-03-31', - 'tree_type': 'Customer', - 'company': '_Test Company 2', - 'from_date': '2017-04-01', - 'value_quantity': 'Quantity' + "doc_type": "Sales Order", + "range": "Monthly", + "to_date": "2018-03-31", + "tree_type": "Customer", + "company": "_Test Company 2", + "from_date": "2017-04-01", + "value_quantity": "Quantity", } report = execute(filters) @@ -150,7 +149,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 20.0, "mar_2018": 0.0, - "total":20.0 + "total": 20.0, }, { "entity": "_Test Customer 2", @@ -167,7 +166,7 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total":25.0 + "total": 25.0, }, { "entity": "_Test Customer 3", @@ -184,47 +183,66 @@ class TestAnalytics(FrappeTestCase): "jan_2018": 0.0, "feb_2018": 0.0, "mar_2018": 0.0, - "total": 30.0 - } + "total": 30.0, + }, ] - result = sorted(report[1], key=lambda k: k['entity']) + result = sorted(report[1], key=lambda k: k["entity"]) self.assertEqual(expected_data, result) + def create_sales_orders(): frappe.set_user("Administrator") - make_sales_order(company="_Test Company 2", qty=10, - customer = "_Test Customer 1", - transaction_date = '2018-02-10', - warehouse = 'Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 1", + transaction_date="2018-02-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=10, customer = "_Test Customer 1", - transaction_date = '2018-02-15', - warehouse = 'Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 1", + transaction_date="2018-02-15", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company = "_Test Company 2", - qty=10, customer = "_Test Customer 2", - transaction_date = '2017-10-10', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 2", + transaction_date="2017-10-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=15, customer = "_Test Customer 2", - transaction_date='2017-09-23', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=15, + customer="_Test Customer 2", + transaction_date="2017-09-23", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=20, customer = "_Test Customer 3", - transaction_date='2017-06-15', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=20, + customer="_Test Customer 3", + transaction_date="2017-06-15", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) - make_sales_order(company="_Test Company 2", - qty=10, customer = "_Test Customer 3", - transaction_date='2017-07-10', - warehouse='Finished Goods - _TC2', - currency = 'EUR') + make_sales_order( + company="_Test Company 2", + qty=10, + customer="_Test Customer 3", + transaction_date="2017-07-10", + warehouse="Finished Goods - _TC2", + currency="EUR", + ) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 001095588ba..465bb2d7452 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -26,6 +26,7 @@ def execute(filters=None): return columns, data, None, chart_data + def validate_filters(filters): from_date, to_date = filters.get("from_date"), filters.get("to_date") @@ -34,6 +35,7 @@ def validate_filters(filters): elif date_diff(to_date, from_date) < 0: frappe.throw(_("To Date cannot be before From Date.")) + def get_conditions(filters): conditions = "" if filters.get("from_date") and filters.get("to_date"): @@ -50,8 +52,10 @@ def get_conditions(filters): return conditions + def get_data(conditions, filters): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT so.transaction_date as date, soi.delivery_date as delivery_date, @@ -81,10 +85,16 @@ def get_data(conditions, filters): {conditions} GROUP BY soi.name ORDER BY so.transaction_date ASC, soi.item_code ASC - """.format(conditions=conditions), filters, as_dict=1) + """.format( + conditions=conditions + ), + filters, + as_dict=1, + ) return data + def prepare_data(data, filters): completed, pending = 0, 0 @@ -114,8 +124,17 @@ def prepare_data(data, filters): so_row["delay"] = min(so_row["delay"], row["delay"]) # sum numeric columns - fields = ["qty", "delivered_qty", "pending_qty", "billed_qty", "qty_to_bill", "amount", - "delivered_qty_amount", "billed_amount", "pending_amount"] + fields = [ + "qty", + "delivered_qty", + "pending_qty", + "billed_qty", + "qty_to_bill", + "amount", + "delivered_qty_amount", + "billed_amount", + "pending_amount", + ] for field in fields: so_row[field] = flt(row[field]) + flt(so_row[field]) @@ -129,160 +148,142 @@ def prepare_data(data, filters): return data, chart_data + def prepare_chart_data(pending, completed): labels = ["Amount to Bill", "Billed Amount"] return { - "data" : { - "labels": labels, - "datasets": [ - {"values": [pending, completed]} - ] - }, - "type": 'donut', - "height": 300 + "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, + "type": "donut", + "height": 300, } + def get_columns(filters): columns = [ - { - "label":_("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 90 - }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 90}, { "label": _("Sales Order"), "fieldname": "sales_order", "fieldtype": "Link", "options": "Sales Order", - "width": 160 - }, - { - "label":_("Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 130 + "width": 160, }, + {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 130}, { "label": _("Customer"), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", - "width": 130 - }] - - if not filters.get("group_by_so"): - columns.append({ - "label":_("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }) - columns.append({ - "label":_("Description"), - "fieldname": "description", - "fieldtype": "Small Text", - "width": 100 - }) - - columns.extend([ - { - "label": _("Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Delivered Qty"), - "fieldname": "delivered_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Qty to Deliver"), - "fieldname": "pending_qty", - "fieldtype": "Float", - "width": 120, - "convertible": "qty" - }, - { - "label": _("Billed Qty"), - "fieldname": "billed_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Qty to Bill"), - "fieldname": "qty_to_bill", - "fieldtype": "Float", - "width": 80, - "convertible": "qty" - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Billed Amount"), - "fieldname": "billed_amount", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label": _("Pending Amount"), - "fieldname": "pending_amount", - "fieldtype": "Currency", "width": 130, - "options": "Company:company:default_currency", - "convertible": "rate" }, - { - "label": _("Amount Delivered"), - "fieldname": "delivered_qty_amount", - "fieldtype": "Currency", - "width": 100, - "options": "Company:company:default_currency", - "convertible": "rate" - }, - { - "label":_("Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 120 - }, - { - "label": _("Delay (in Days)"), - "fieldname": "delay", - "fieldtype": "Data", - "width": 100 - } - ]) + ] + if not filters.get("group_by_so"): - columns.append({ - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100 - }) - columns.append({ + columns.append( + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + } + ) + columns.append( + {"label": _("Description"), "fieldname": "description", "fieldtype": "Small Text", "width": 100} + ) + + columns.extend( + [ + { + "label": _("Qty"), + "fieldname": "qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Delivered Qty"), + "fieldname": "delivered_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Qty to Deliver"), + "fieldname": "pending_qty", + "fieldtype": "Float", + "width": 120, + "convertible": "qty", + }, + { + "label": _("Billed Qty"), + "fieldname": "billed_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Qty to Bill"), + "fieldname": "qty_to_bill", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Billed Amount"), + "fieldname": "billed_amount", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Pending Amount"), + "fieldname": "pending_amount", + "fieldtype": "Currency", + "width": 130, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Amount Delivered"), + "fieldname": "delivered_qty_amount", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + {"label": _("Delivery Date"), "fieldname": "delivery_date", "fieldtype": "Date", "width": 120}, + {"label": _("Delay (in Days)"), "fieldname": "delay", "fieldtype": "Data", "width": 100}, + ] + ) + if not filters.get("group_by_so"): + columns.append( + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + } + ) + columns.append( + { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 100 - }) - + "width": 100, + } + ) return columns diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.py b/erpnext/selling/report/sales_order_trends/sales_order_trends.py index 5a711712620..93707bd46d9 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.py +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Sales Order") data = get_data(filters, conditions) @@ -16,6 +17,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, conditions, filters): if not (data and conditions): return [] @@ -28,32 +30,27 @@ def get_chart_data(data, conditions, filters): # fetch only periodic columns as labels columns = conditions.get("columns")[start:-2][1::2] - labels = [column.split(':')[0] for column in columns] + labels = [column.split(":")[0] for column in columns] datapoints = [0] * len(labels) for row in data: # If group by filter, don't add first row of group (it's already summed) - if not row[start-1]: + if not row[start - 1]: continue # Remove None values and compute only periodic data row = [x if x else 0 for x in row[start:-2]] - row = row[1::2] + row = row[1::2] for i in range(len(row)): datapoints[i] += row[i] return { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : _("{0}").format(filters.get("period")) + _(" Sales Value"), - "values" : datapoints - } - ] + "data": { + "labels": labels, + "datasets": [ + {"name": _("{0}").format(filters.get("period")) + _(" Sales Value"), "values": datapoints} + ], }, - "type" : "line", - "lineOptions": { - "regionFill": 1 - } + "type": "line", + "lineOptions": {"regionFill": 1}, } diff --git a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py index b775907bd53..cf9ea219c1b 100644 --- a/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py +++ b/erpnext/selling/report/sales_partner_commission_summary/sales_partner_commission_summary.py @@ -7,80 +7,73 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) data = get_entries(filters) return columns, data + def get_columns(filters): if not filters.get("doctype"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doctype"]), "options": filters["doctype"], "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Partner"), "options": "Sales Partner", "fieldname": "sales_partner", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": _("Total Commission"), "fieldname": "total_commission", "fieldtype": "Currency", - "width": 120 - } + "width": 120, + }, ] return columns + def get_entries(filters): - date_field = ("transaction_date" if filters.get('doctype') == "Sales Order" - else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" conditions = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT name, customer, territory, {0} as posting_date, base_net_total as amount, sales_partner, commission_rate, total_commission @@ -89,10 +82,16 @@ def get_entries(filters): WHERE {2} and docstatus = 1 and sales_partner is not null and sales_partner != '' order by name desc, sales_partner - """.format(date_field, filters.get('doctype'), conditions), filters, as_dict=1) + """.format( + date_field, filters.get("doctype"), conditions + ), + filters, + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = "1=1" diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index a647eb4fea2..f34f3e34e2c 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -14,8 +14,15 @@ from erpnext.accounts.utils import get_fiscal_year def get_data_column(filters, partner_doctype): data = [] - period_list = get_period_list(filters.fiscal_year, filters.fiscal_year, '', '', - 'Fiscal Year', filters.period, company=filters.company) + period_list = get_period_list( + filters.fiscal_year, + filters.fiscal_year, + "", + "", + "Fiscal Year", + filters.period, + company=filters.company, + ) rows = get_data(filters, period_list, partner_doctype) columns = get_columns(filters, period_list, partner_doctype) @@ -24,20 +31,19 @@ def get_data_column(filters, partner_doctype): return columns, data for key, value in rows.items(): - value.update({ - frappe.scrub(partner_doctype): key[0], - 'item_group': key[1] - }) + value.update({frappe.scrub(partner_doctype): key[0], "item_group": key[1]}) data.append(value) return columns, data + def get_data(filters, period_list, partner_doctype): sales_field = frappe.scrub(partner_doctype) sales_users_data = get_parents_data(filters, partner_doctype) - if not sales_users_data: return + if not sales_users_data: + return sales_users, item_groups = [], [] for d in sales_users_data: @@ -47,99 +53,110 @@ def get_data(filters, period_list, partner_doctype): if d.item_group not in item_groups: item_groups.append(d.item_group) - date_field = ("transaction_date" - if filters.get('doctype') == "Sales Order" else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" actual_data = get_actual_data(filters, item_groups, sales_users, date_field, sales_field) - return prepare_data(filters, sales_users_data, - actual_data, date_field, period_list, sales_field) + return prepare_data(filters, sales_users_data, actual_data, date_field, period_list, sales_field) + def get_columns(filters, period_list, partner_doctype): fieldtype, options = "Currency", "currency" - if filters.get("target_on") == 'Quantity': + if filters.get("target_on") == "Quantity": fieldtype, options = "Float", "" - columns = [{ - "fieldname": frappe.scrub(partner_doctype), - "label": _(partner_doctype), - "fieldtype": "Link", - "options": partner_doctype, - "width": 150 - }, { - "fieldname": "item_group", - "label": _("Item Group"), - "fieldtype": "Link", - "options": "Item Group", - "width": 150 - }] + columns = [ + { + "fieldname": frappe.scrub(partner_doctype), + "label": _(partner_doctype), + "fieldtype": "Link", + "options": partner_doctype, + "width": 150, + }, + { + "fieldname": "item_group", + "label": _("Item Group"), + "fieldtype": "Link", + "options": "Item Group", + "width": 150, + }, + ] for period in period_list: - target_key = 'target_{}'.format(period.key) - variance_key = 'variance_{}'.format(period.key) + target_key = "target_{}".format(period.key) + variance_key = "variance_{}".format(period.key) - columns.extend([{ - "fieldname": target_key, - "label": _("Target ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": period.key, - "label": _("Achieved ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": variance_key, - "label": _("Variance ({})").format(period.label), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }]) + columns.extend( + [ + { + "fieldname": target_key, + "label": _("Target ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": period.key, + "label": _("Achieved ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": variance_key, + "label": _("Variance ({})").format(period.label), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + ] + ) - columns.extend([{ - "fieldname": "total_target", - "label": _("Total Target"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": "total_achieved", - "label": _("Total Achieved"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }, { - "fieldname": "total_variance", - "label": _("Total Variance"), - "fieldtype": fieldtype, - "options": options, - "width": 150 - }]) + columns.extend( + [ + { + "fieldname": "total_target", + "label": _("Total Target"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": "total_achieved", + "label": _("Total Achieved"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + { + "fieldname": "total_variance", + "label": _("Total Variance"), + "fieldtype": fieldtype, + "options": options, + "width": 150, + }, + ] + ) return columns + def prepare_data(filters, sales_users_data, actual_data, date_field, period_list, sales_field): rows = {} - target_qty_amt_field = ("target_qty" - if filters.get("target_on") == 'Quantity' else "target_amount") + target_qty_amt_field = "target_qty" if filters.get("target_on") == "Quantity" else "target_amount" - qty_or_amount_field = ("stock_qty" - if filters.get("target_on") == 'Quantity' else "base_net_amount") + qty_or_amount_field = "stock_qty" if filters.get("target_on") == "Quantity" else "base_net_amount" for d in sales_users_data: key = (d.parent, d.item_group) - dist_data = get_periodwise_distribution_data(d.distribution_id, period_list, filters.get("period")) + dist_data = get_periodwise_distribution_data( + d.distribution_id, period_list, filters.get("period") + ) if key not in rows: - rows.setdefault(key,{ - 'total_target': 0, - 'total_achieved': 0, - 'total_variance': 0 - }) + rows.setdefault(key, {"total_target": 0, "total_achieved": 0, "total_variance": 0}) details = rows[key] for period in period_list: @@ -147,15 +164,19 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list if p_key not in details: details[p_key] = 0 - target_key = 'target_{}'.format(p_key) - variance_key = 'variance_{}'.format(p_key) + target_key = "target_{}".format(p_key) + variance_key = "variance_{}".format(p_key) details[target_key] = (d.get(target_qty_amt_field) * dist_data.get(p_key)) / 100 details[variance_key] = 0 details["total_target"] += details[target_key] for r in actual_data: - if (r.get(sales_field) == d.parent and r.item_group == d.item_group and - period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): + if ( + r.get(sales_field) == d.parent + and r.item_group == d.item_group + and period.from_date <= r.get(date_field) + and r.get(date_field) <= period.to_date + ): details[p_key] += r.get(qty_or_amount_field, 0) details[variance_key] = details.get(p_key) - details.get(target_key) @@ -164,24 +185,28 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list return rows + def get_actual_data(filters, item_groups, sales_users_or_territory_data, date_field, sales_field): fiscal_year = get_fiscal_year(fiscal_year=filters.get("fiscal_year"), as_dict=1) dates = [fiscal_year.year_start_date, fiscal_year.year_end_date] select_field = "`tab{0}`.{1}".format(filters.get("doctype"), sales_field) - child_table = "`tab{0}`".format(filters.get("doctype") + ' Item') + child_table = "`tab{0}`".format(filters.get("doctype") + " Item") - if sales_field == 'sales_person': + if sales_field == "sales_person": select_field = "`tabSales Team`.sales_person" - child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + ' Item') + child_table = "`tab{0}`, `tabSales Team`".format(filters.get("doctype") + " Item") cond = """`tabSales Team`.parent = `tab{0}`.name and - `tabSales Team`.sales_person in ({1}) """.format(filters.get("doctype"), - ','.join(['%s'] * len(sales_users_or_territory_data))) + `tabSales Team`.sales_person in ({1}) """.format( + filters.get("doctype"), ",".join(["%s"] * len(sales_users_or_territory_data)) + ) else: - cond = "`tab{0}`.{1} in ({2})".format(filters.get("doctype"), sales_field, - ','.join(['%s'] * len(sales_users_or_territory_data))) + cond = "`tab{0}`.{1} in ({2})".format( + filters.get("doctype"), sales_field, ",".join(["%s"] * len(sales_users_or_territory_data)) + ) - return frappe.db.sql(""" SELECT `tab{child_doc}`.item_group, + return frappe.db.sql( + """ SELECT `tab{child_doc}`.item_group, `tab{child_doc}`.stock_qty, `tab{child_doc}`.base_net_amount, {select_field}, `tab{parent_doc}`.{date_field} FROM `tab{parent_doc}`, {child_table} @@ -189,26 +214,30 @@ def get_actual_data(filters, item_groups, sales_users_or_territory_data, date_fi `tab{child_doc}`.parent = `tab{parent_doc}`.name and `tab{parent_doc}`.docstatus = 1 and {cond} and `tab{child_doc}`.item_group in ({item_groups}) - and `tab{parent_doc}`.{date_field} between %s and %s""" - .format( - cond = cond, - date_field = date_field, - select_field = select_field, - child_table = child_table, - parent_doc = filters.get("doctype"), - child_doc = filters.get("doctype") + ' Item', - item_groups = ','.join(['%s'] * len(item_groups)) - ), tuple(sales_users_or_territory_data + item_groups + dates), as_dict=1) + and `tab{parent_doc}`.{date_field} between %s and %s""".format( + cond=cond, + date_field=date_field, + select_field=select_field, + child_table=child_table, + parent_doc=filters.get("doctype"), + child_doc=filters.get("doctype") + " Item", + item_groups=",".join(["%s"] * len(item_groups)), + ), + tuple(sales_users_or_territory_data + item_groups + dates), + as_dict=1, + ) + def get_parents_data(filters, partner_doctype): - filters_dict = {'parenttype': partner_doctype} + filters_dict = {"parenttype": partner_doctype} - target_qty_amt_field = ("target_qty" - if filters.get("target_on") == 'Quantity' else "target_amount") + target_qty_amt_field = "target_qty" if filters.get("target_on") == "Quantity" else "target_amount" if filters.get("fiscal_year"): filters_dict["fiscal_year"] = filters.get("fiscal_year") - return frappe.get_all('Target Detail', - filters = filters_dict, - fields = ["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"]) + return frappe.get_all( + "Target Detail", + filters=filters_dict, + fields=["parent", "item_group", target_qty_amt_field, "fiscal_year", "distribution_id"], + ) diff --git a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py index c64555bf2d3..2049520eadc 100644 --- a/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py +++ b/erpnext/selling/report/sales_partner_transaction_summary/sales_partner_transaction_summary.py @@ -7,120 +7,98 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) data = get_entries(filters) return columns, data + def get_columns(filters): if not filters.get("doctype"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doctype"]), "options": filters["doctype"], "fieldname": "name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 100 + "width": 100, }, { "label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 100, }, { "label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", - "width": 100 - }, - { - "label": _("Quantity"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 120 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "fieldtype": "Currency", - "width": 120 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Partner"), "options": "Sales Partner", "fieldname": "sales_partner", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Commission"), - "fieldname": "commission", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120}, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 120 - } + "width": 120, + }, ] return columns + def get_entries(filters): - date_field = ("transaction_date" if filters.get('doctype') == "Sales Order" - else "posting_date") + date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date" conditions = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency, dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount, @@ -132,11 +110,16 @@ def get_entries(filters): {cond} and dt.name = dt_item.parent and dt.docstatus = 1 and dt.sales_partner is not null and dt.sales_partner != '' order by dt.name desc, dt.sales_partner - """.format(date_field=date_field, doctype=filters.get('doctype'), - cond=conditions), filters, as_dict=1) + """.format( + date_field=date_field, doctype=filters.get("doctype"), cond=conditions + ), + filters, + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = "1=1" @@ -150,18 +133,19 @@ def get_conditions(filters, date_field): if filters.get("to_date"): conditions += " and dt.{0} <= %(to_date)s".format(date_field) - if not filters.get('show_return_entries'): + if not filters.get("show_return_entries"): conditions += " and dt_item.qty > 0.0" - if filters.get('brand'): + if filters.get("brand"): conditions += " and dt_item.brand = %(brand)s" - if filters.get('item_group'): - lft, rgt = frappe.get_cached_value('Item Group', - filters.get('item_group'), ['lft', 'rgt']) + if filters.get("item_group"): + lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"]) conditions += """ and dt_item.item_group in (select name from - `tabItem Group` where lft >= %s and rgt <= %s)""" % (lft, rgt) - + `tabItem Group` where lft >= %s and rgt <= %s)""" % ( + lft, + rgt, + ) return conditions diff --git a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py index 1542e31feff..a8df5308036 100644 --- a/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py +++ b/erpnext/selling/report/sales_person_commission_summary/sales_person_commission_summary.py @@ -7,102 +7,101 @@ from frappe import _, msgprint def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) entries = get_entries(filters) data = [] for d in entries: - data.append([ - d.name, d.customer, d.territory, d.posting_date, - d.base_net_amount, d.sales_person, d.allocated_percentage, d.commission_rate, d.allocated_amount,d.incentives - ]) + data.append( + [ + d.name, + d.customer, + d.territory, + d.posting_date, + d.base_net_amount, + d.sales_person, + d.allocated_percentage, + d.commission_rate, + d.allocated_amount, + d.incentives, + ] + ) if data: - total_row = [""]*len(data[0]) + total_row = [""] * len(data[0]) data.append(total_row) return columns, data + def get_columns(filters): if not filters.get("doc_type"): msgprint(_("Please select the document type first"), raise_exception=1) - columns =[ + columns = [ { "label": _(filters["doc_type"]), "options": filters["doc_type"], - "fieldname": filters['doc_type'], + "fieldname": filters["doc_type"], "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 100 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 120 + "width": 100, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, { "label": _("Sales Person"), "options": "Sales Person", "fieldname": "sales_person", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Contribution %"), "fieldname": "contribution_percentage", "fieldtype": "Data", - "width": 110 + "width": 110, }, { "label": _("Commission Rate %"), "fieldname": "commission_rate", "fieldtype": "Data", - "width": 100 + "width": 100, }, { "label": _("Contribution Amount"), "fieldname": "contribution_amount", "fieldtype": "Currency", - "width": 120 + "width": 120, }, - { - "label": _("Incentives"), - "fieldname": "incentives", - "fieldtype": "Currency", - "width": 120 - } + {"label": _("Incentives"), "fieldname": "incentives", "fieldtype": "Currency", "width": 120}, ] return columns + def get_entries(filters): date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" conditions, values = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ select dt.name, dt.customer, dt.territory, dt.%s as posting_date,dt.base_net_total as base_net_amount, st.commission_rate,st.sales_person, st.allocated_percentage, st.allocated_amount, st.incentives @@ -111,11 +110,15 @@ def get_entries(filters): where st.parent = dt.name and st.parenttype = %s and dt.docstatus = 1 %s order by dt.name desc,st.sales_person - """ %(date_field, filters["doc_type"], '%s', conditions), - tuple([filters["doc_type"]] + values), as_dict=1) + """ + % (date_field, filters["doc_type"], "%s", conditions), + tuple([filters["doc_type"]] + values), + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = [""] values = [] diff --git a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py index c621be88295..cb6e8a1102f 100644 --- a/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py +++ b/erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py @@ -9,7 +9,8 @@ from erpnext import get_company_currency def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) entries = get_entries(filters) @@ -19,19 +20,33 @@ def execute(filters=None): company_currency = get_company_currency(filters.get("company")) for d in entries: - if d.stock_qty > 0 or filters.get('show_return_entries', 0): - data.append([ - d.name, d.customer, d.territory, d.warehouse, d.posting_date, d.item_code, - item_details.get(d.item_code, {}).get("item_group"), item_details.get(d.item_code, {}).get("brand"), - d.stock_qty, d.base_net_amount, d.sales_person, d.allocated_percentage, d.contribution_amt, company_currency - ]) + if d.stock_qty > 0 or filters.get("show_return_entries", 0): + data.append( + [ + d.name, + d.customer, + d.territory, + d.warehouse, + d.posting_date, + d.item_code, + item_details.get(d.item_code, {}).get("item_group"), + item_details.get(d.item_code, {}).get("brand"), + d.stock_qty, + d.base_net_amount, + d.sales_person, + d.allocated_percentage, + d.contribution_amt, + company_currency, + ] + ) if data: - total_row = [""]*len(data[0]) + total_row = [""] * len(data[0]) data.append(total_row) return columns, data + def get_columns(filters): if not filters.get("doc_type"): msgprint(_("Please select the document type first"), raise_exception=1) @@ -40,102 +55,88 @@ def get_columns(filters): { "label": _(filters["doc_type"]), "options": filters["doc_type"], - "fieldname": frappe.scrub(filters['doc_type']), + "fieldname": frappe.scrub(filters["doc_type"]), "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Customer"), "options": "Customer", "fieldname": "customer", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Territory"), "options": "Territory", "fieldname": "territory", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Warehouse"), "options": "Warehouse", "fieldname": "warehouse", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 140 + "width": 140, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140}, { "label": _("Item Code"), "options": "Item", "fieldname": "item_code", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Item Group"), "options": "Item Group", "fieldname": "item_group", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Brand"), "options": "Brand", "fieldname": "brand", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 140 + "width": 140, }, + {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140}, { "label": _("Amount"), "options": "currency", "fieldname": "amount", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Sales Person"), "options": "Sales Person", "fieldname": "sales_person", "fieldtype": "Link", - "width": 140 - }, - { - "label": _("Contribution %"), - "fieldname": "contribution", - "fieldtype": "Float", - "width": 140 + "width": 140, }, + {"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140}, { "label": _("Contribution Amount"), "options": "currency", "fieldname": "contribution_amt", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { - "label":_("Currency"), + "label": _("Currency"), "options": "Currency", - "fieldname":"currency", - "fieldtype":"Link", - "hidden" : 1 - } + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + }, ] return columns + def get_entries(filters): date_field = filters["doc_type"] == "Sales Order" and "transaction_date" or "posting_date" if filters["doc_type"] == "Sales Order": @@ -144,7 +145,8 @@ def get_entries(filters): qty_field = "qty" conditions, values = get_conditions(filters, date_field) - entries = frappe.db.sql(""" + entries = frappe.db.sql( + """ SELECT dt.name, dt.customer, dt.territory, dt.%s as posting_date, dt_item.item_code, st.sales_person, st.allocated_percentage, dt_item.warehouse, @@ -165,11 +167,24 @@ def get_entries(filters): WHERE st.parent = dt.name and dt.name = dt_item.parent and st.parenttype = %s and dt.docstatus = 1 %s order by st.sales_person, dt.name desc - """ %(date_field, qty_field, qty_field, qty_field, filters["doc_type"], filters["doc_type"], '%s', conditions), - tuple([filters["doc_type"]] + values), as_dict=1) + """ + % ( + date_field, + qty_field, + qty_field, + qty_field, + filters["doc_type"], + filters["doc_type"], + "%s", + conditions, + ), + tuple([filters["doc_type"]] + values), + as_dict=1, + ) return entries + def get_conditions(filters, date_field): conditions = [""] values = [] @@ -181,7 +196,11 @@ def get_conditions(filters, date_field): if filters.get("sales_person"): lft, rgt = frappe.get_value("Sales Person", filters.get("sales_person"), ["lft", "rgt"]) - conditions.append("exists(select name from `tabSales Person` where lft >= {0} and rgt <= {1} and name=st.sales_person)".format(lft, rgt)) + conditions.append( + "exists(select name from `tabSales Person` where lft >= {0} and rgt <= {1} and name=st.sales_person)".format( + lft, rgt + ) + ) if filters.get("from_date"): conditions.append("dt.{0}>=%s".format(date_field)) @@ -193,23 +212,29 @@ def get_conditions(filters, date_field): items = get_items(filters) if items: - conditions.append("dt_item.item_code in (%s)" % ', '.join(['%s']*len(items))) + conditions.append("dt_item.item_code in (%s)" % ", ".join(["%s"] * len(items))) values += items return " and ".join(conditions), values + def get_items(filters): - if filters.get("item_group"): key = "item_group" - elif filters.get("brand"): key = "brand" - else: key = "" + if filters.get("item_group"): + key = "item_group" + elif filters.get("brand"): + key = "brand" + else: + key = "" items = [] if key: - items = frappe.db.sql_list("""select name from tabItem where %s = %s""" % - (key, '%s'), (filters[key])) + items = frappe.db.sql_list( + """select name from tabItem where %s = %s""" % (key, "%s"), (filters[key]) + ) return items + def get_item_details(): item_details = {} for d in frappe.db.sql("""SELECT `name`, `item_group`, `brand` FROM `tabItem`""", as_dict=1): diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index b7b4d3aa4ca..5dfc1db0976 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -23,38 +23,39 @@ def get_columns(): "fieldname": "territory", "fieldtype": "Link", "options": "Territory", - "width": 150 + "width": 150, }, { "label": _("Opportunity Amount"), "fieldname": "opportunity_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Quotation Amount"), "fieldname": "quotation_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Order Amount"), "fieldname": "order_amount", "fieldtype": "Currency", "options": currency, - "width": 150 + "width": 150, }, { "label": _("Billing Amount"), "fieldname": "billing_amount", "fieldtype": "Currency", "options": currency, - "width": 150 - } + "width": 150, + }, ] + def get_data(filters=None): data = [] @@ -84,26 +85,32 @@ def get_data(filters=None): if territory_orders: t_order_names = [t.name for t in territory_orders] - territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] + territory_invoices = ( + list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) + if t_order_names and sales_invoices + else [] + ) territory_data = { "territory": territory.name, "opportunity_amount": _get_total(territory_opportunities, "opportunity_amount"), "quotation_amount": _get_total(territory_quotations), "order_amount": _get_total(territory_orders), - "billing_amount": _get_total(territory_invoices) + "billing_amount": _get_total(territory_invoices), } data.append(territory_data) return data + def get_opportunities(filters): conditions = "" - if filters.get('transaction_date'): + if filters.get("transaction_date"): conditions = " WHERE transaction_date between {0} and {1}".format( - frappe.db.escape(filters['transaction_date'][0]), - frappe.db.escape(filters['transaction_date'][1])) + frappe.db.escape(filters["transaction_date"][0]), + frappe.db.escape(filters["transaction_date"][1]), + ) if filters.company: if conditions: @@ -112,11 +119,17 @@ def get_opportunities(filters): conditions += " WHERE" conditions += " company = %(company)s" - - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT name, territory, opportunity_amount FROM `tabOpportunity` {0} - """.format(conditions), filters, as_dict=1) #nosec + """.format( + conditions + ), + filters, + as_dict=1, + ) # nosec + def get_quotations(opportunities): if not opportunities: @@ -124,11 +137,18 @@ def get_quotations(opportunities): opportunity_names = [o.name for o in opportunities] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `name`,`base_grand_total`, `opportunity` FROM `tabQuotation` WHERE docstatus=1 AND opportunity in ({0}) - """.format(', '.join(["%s"]*len(opportunity_names))), tuple(opportunity_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(opportunity_names)) + ), + tuple(opportunity_names), + as_dict=1, + ) # nosec + def get_sales_orders(quotations): if not quotations: @@ -136,11 +156,18 @@ def get_sales_orders(quotations): quotation_names = [q.name for q in quotations] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT so.`name`, so.`base_grand_total`, soi.prevdoc_docname as quotation FROM `tabSales Order` so, `tabSales Order Item` soi WHERE so.docstatus=1 AND so.name = soi.parent AND soi.prevdoc_docname in ({0}) - """.format(', '.join(["%s"]*len(quotation_names))), tuple(quotation_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(quotation_names)) + ), + tuple(quotation_names), + as_dict=1, + ) # nosec + def get_sales_invoice(sales_orders): if not sales_orders: @@ -148,11 +175,18 @@ def get_sales_invoice(sales_orders): so_names = [so.name for so in sales_orders] - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT si.name, si.base_grand_total, sii.sales_order FROM `tabSales Invoice` si, `tabSales Invoice Item` sii WHERE si.docstatus=1 AND si.name = sii.parent AND sii.sales_order in ({0}) - """.format(', '.join(["%s"]*len(so_names))), tuple(so_names), as_dict=1) #nosec + """.format( + ", ".join(["%s"] * len(so_names)) + ), + tuple(so_names), + as_dict=1, + ) # nosec + def _get_total(doclist, amount_field="base_grand_total"): if not doclist: diff --git a/erpnext/setup/default_energy_point_rules.py b/erpnext/setup/default_energy_point_rules.py index cfff75e525c..b7fe19758c1 100644 --- a/erpnext/setup/default_energy_point_rules.py +++ b/erpnext/setup/default_energy_point_rules.py @@ -1,57 +1,48 @@ - from frappe import _ doctype_rule_map = { - 'Item': { - 'points': 5, - 'for_doc_event': 'New' + "Item": {"points": 5, "for_doc_event": "New"}, + "Customer": {"points": 5, "for_doc_event": "New"}, + "Supplier": {"points": 5, "for_doc_event": "New"}, + "Lead": {"points": 2, "for_doc_event": "New"}, + "Opportunity": { + "points": 10, + "for_doc_event": "Custom", + "condition": 'doc.status=="Converted"', + "rule_name": _("On Converting Opportunity"), + "user_field": "converted_by", }, - 'Customer': { - 'points': 5, - 'for_doc_event': 'New' + "Sales Order": { + "points": 10, + "for_doc_event": "Submit", + "rule_name": _("On Sales Order Submission"), + "user_field": "modified_by", }, - 'Supplier': { - 'points': 5, - 'for_doc_event': 'New' + "Purchase Order": { + "points": 10, + "for_doc_event": "Submit", + "rule_name": _("On Purchase Order Submission"), + "user_field": "modified_by", }, - 'Lead': { - 'points': 2, - 'for_doc_event': 'New' + "Task": { + "points": 5, + "condition": 'doc.status == "Completed"', + "rule_name": _("On Task Completion"), + "user_field": "completed_by", }, - 'Opportunity': { - 'points': 10, - 'for_doc_event': 'Custom', - 'condition': 'doc.status=="Converted"', - 'rule_name': _('On Converting Opportunity'), - 'user_field': 'converted_by' - }, - 'Sales Order': { - 'points': 10, - 'for_doc_event': 'Submit', - 'rule_name': _('On Sales Order Submission'), - 'user_field': 'modified_by' - }, - 'Purchase Order': { - 'points': 10, - 'for_doc_event': 'Submit', - 'rule_name': _('On Purchase Order Submission'), - 'user_field': 'modified_by' - }, - 'Task': { - 'points': 5, - 'condition': 'doc.status == "Completed"', - 'rule_name': _('On Task Completion'), - 'user_field': 'completed_by' - } } + def get_default_energy_point_rules(): - return [{ - 'doctype': 'Energy Point Rule', - 'reference_doctype': doctype, - 'for_doc_event': rule.get('for_doc_event') or 'Custom', - 'condition': rule.get('condition'), - 'rule_name': rule.get('rule_name') or _('On {0} Creation').format(doctype), - 'points': rule.get('points'), - 'user_field': rule.get('user_field') or 'owner' - } for doctype, rule in doctype_rule_map.items()] + return [ + { + "doctype": "Energy Point Rule", + "reference_doctype": doctype, + "for_doc_event": rule.get("for_doc_event") or "Custom", + "condition": rule.get("condition"), + "rule_name": rule.get("rule_name") or _("On {0} Creation").format(doctype), + "points": rule.get("points"), + "user_field": rule.get("user_field") or "owner", + } + for doctype, rule in doctype_rule_map.items() + ] diff --git a/erpnext/setup/default_success_action.py b/erpnext/setup/default_success_action.py index 338fb43f249..2b9e75c3265 100644 --- a/erpnext/setup/default_success_action.py +++ b/erpnext/setup/default_success_action.py @@ -1,26 +1,31 @@ - from frappe import _ doctype_list = [ - 'Purchase Receipt', - 'Purchase Invoice', - 'Quotation', - 'Sales Order', - 'Delivery Note', - 'Sales Invoice' + "Purchase Receipt", + "Purchase Invoice", + "Quotation", + "Sales Order", + "Delivery Note", + "Sales Invoice", ] + def get_message(doctype): return _("{0} has been submitted successfully").format(_(doctype)) + def get_first_success_message(doctype): return get_message(doctype) + def get_default_success_action(): - return [{ - 'doctype': 'Success Action', - 'ref_doctype': doctype, - 'message': get_message(doctype), - 'first_success_message': get_first_success_message(doctype), - 'next_actions': 'new\nprint\nemail' - } for doctype in doctype_list] + return [ + { + "doctype": "Success Action", + "ref_doctype": doctype, + "message": get_message(doctype), + "first_success_message": get_first_success_message(doctype), + "next_actions": "new\nprint\nemail", + } + for doctype in doctype_list + ] diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 2a0d785520a..309658d2601 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -12,90 +12,121 @@ from erpnext.utilities.transaction_base import TransactionBase class AuthorizationControl(TransactionBase): def get_appr_user_role(self, det, doctype_name, total, based_on, condition, item, company): amt_list, appr_users, appr_roles = [], [], [] - users, roles = '','' + users, roles = "", "" if det: for x in det: amt_list.append(flt(x[0])) max_amount = max(amt_list) - app_dtl = frappe.db.sql("""select approving_user, approving_role from `tabAuthorization Rule` + app_dtl = frappe.db.sql( + """select approving_user, approving_role from `tabAuthorization Rule` where transaction = %s and (value = %s or value > %s) - and docstatus != 2 and based_on = %s and company = %s %s""" % - ('%s', '%s', '%s', '%s', '%s', condition), - (doctype_name, flt(max_amount), total, based_on, company)) + and docstatus != 2 and based_on = %s and company = %s %s""" + % ("%s", "%s", "%s", "%s", "%s", condition), + (doctype_name, flt(max_amount), total, based_on, company), + ) if not app_dtl: - app_dtl = frappe.db.sql("""select approving_user, approving_role from `tabAuthorization Rule` + app_dtl = frappe.db.sql( + """select approving_user, approving_role from `tabAuthorization Rule` where transaction = %s and (value = %s or value > %s) and docstatus != 2 - and based_on = %s and ifnull(company,'') = '' %s""" % - ('%s', '%s', '%s', '%s', condition), (doctype_name, flt(max_amount), total, based_on)) + and based_on = %s and ifnull(company,'') = '' %s""" + % ("%s", "%s", "%s", "%s", condition), + (doctype_name, flt(max_amount), total, based_on), + ) for d in app_dtl: - if(d[0]): appr_users.append(d[0]) - if(d[1]): appr_roles.append(d[1]) + if d[0]: + appr_users.append(d[0]) + if d[1]: + appr_roles.append(d[1]) - if not has_common(appr_roles, frappe.get_roles()) and not has_common(appr_users, [session['user']]): + if not has_common(appr_roles, frappe.get_roles()) and not has_common( + appr_users, [session["user"]] + ): frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on))) frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users))) - def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item = ''): + def validate_auth_rule(self, doctype_name, total, based_on, cond, company, item=""): chk = 1 - add_cond1,add_cond2 = '','' - if based_on == 'Itemwise Discount': + add_cond1, add_cond2 = "", "" + if based_on == "Itemwise Discount": add_cond1 += " and master_name = " + frappe.db.escape(cstr(item)) - itemwise_exists = frappe.db.sql("""select value from `tabAuthorization Rule` + itemwise_exists = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s - and based_on = %s and company = %s and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', '%s', cond, add_cond1), (doctype_name, total, based_on, company)) + and based_on = %s and company = %s and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", "%s", cond, add_cond1), + (doctype_name, total, based_on, company), + ) if not itemwise_exists: - itemwise_exists = frappe.db.sql("""select value from `tabAuthorization Rule` + itemwise_exists = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', cond, add_cond1), (doctype_name, total, based_on)) + and ifnull(company,'') = '' and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", cond, add_cond1), + (doctype_name, total, based_on), + ) if itemwise_exists: - self.get_appr_user_role(itemwise_exists, doctype_name, total, based_on, cond+add_cond1, item,company) + self.get_appr_user_role( + itemwise_exists, doctype_name, total, based_on, cond + add_cond1, item, company + ) chk = 0 if chk == 1: - if based_on == 'Itemwise Discount': + if based_on == "Itemwise Discount": add_cond2 += " and ifnull(master_name,'') = ''" - appr = frappe.db.sql("""select value from `tabAuthorization Rule` + appr = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and company = %s and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', '%s', cond, add_cond2), (doctype_name, total, based_on, company)) + and company = %s and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", "%s", cond, add_cond2), + (doctype_name, total, based_on, company), + ) if not appr: - appr = frappe.db.sql("""select value from `tabAuthorization Rule` + appr = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction = %s and value <= %s and based_on = %s - and ifnull(company,'') = '' and docstatus != 2 %s %s""" % - ('%s', '%s', '%s', cond, add_cond2), (doctype_name, total, based_on)) + and ifnull(company,'') = '' and docstatus != 2 %s %s""" + % ("%s", "%s", "%s", cond, add_cond2), + (doctype_name, total, based_on), + ) - self.get_appr_user_role(appr, doctype_name, total, based_on, cond+add_cond2, item, company) + self.get_appr_user_role(appr, doctype_name, total, based_on, cond + add_cond2, item, company) def bifurcate_based_on_type(self, doctype_name, total, av_dis, based_on, doc_obj, val, company): - add_cond = '' + add_cond = "" auth_value = av_dis - if val == 1: add_cond += " and system_user = {}".format(frappe.db.escape(session['user'])) - elif val == 2: add_cond += " and system_role IN %s" % ("('"+"','".join(frappe.get_roles())+"')") - else: add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''" + if val == 1: + add_cond += " and system_user = {}".format(frappe.db.escape(session["user"])) + elif val == 2: + add_cond += " and system_role IN %s" % ("('" + "','".join(frappe.get_roles()) + "')") + else: + add_cond += " and ifnull(system_user,'') = '' and ifnull(system_role,'') = ''" - if based_on == 'Grand Total': auth_value = total - elif based_on == 'Customerwise Discount': + if based_on == "Grand Total": + auth_value = total + elif based_on == "Customerwise Discount": if doc_obj: - if doc_obj.doctype == 'Sales Invoice': customer = doc_obj.customer - else: customer = doc_obj.customer_name + if doc_obj.doctype == "Sales Invoice": + customer = doc_obj.customer + else: + customer = doc_obj.customer_name add_cond = " and master_name = {}".format(frappe.db.escape(customer)) - if based_on == 'Itemwise Discount': + if based_on == "Itemwise Discount": if doc_obj: for t in doc_obj.get("items"): - self.validate_auth_rule(doctype_name, t.discount_percentage, based_on, add_cond, company,t.item_code ) + self.validate_auth_rule( + doctype_name, t.discount_percentage, based_on, add_cond, company, t.item_code + ) else: self.validate_auth_rule(doctype_name, auth_value, based_on, add_cond, company) - def validate_approving_authority(self, doctype_name,company, total, doc_obj = ''): + def validate_approving_authority(self, doctype_name, company, total, doc_obj=""): if not frappe.db.count("Authorization Rule"): return @@ -109,56 +140,85 @@ class AuthorizationControl(TransactionBase): if doc_obj.get("discount_amount"): base_rate -= flt(doc_obj.discount_amount) - if price_list_rate: av_dis = 100 - flt(base_rate * 100 / price_list_rate) + if price_list_rate: + av_dis = 100 - flt(base_rate * 100 / price_list_rate) - final_based_on = ['Grand Total','Average Discount','Customerwise Discount','Itemwise Discount'] + final_based_on = [ + "Grand Total", + "Average Discount", + "Customerwise Discount", + "Itemwise Discount", + ] # Check for authorization set for individual user - based_on = [x[0] for x in frappe.db.sql("""select distinct based_on from `tabAuthorization Rule` + based_on = [ + x[0] + for x in frappe.db.sql( + """select distinct based_on from `tabAuthorization Rule` where transaction = %s and system_user = %s and (company = %s or ifnull(company,'')='') and docstatus != 2""", - (doctype_name, session['user'], company))] + (doctype_name, session["user"], company), + ) + ] for d in based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, d, doc_obj, 1, company) # Remove user specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != 'Itemwise Discount': final_based_on.remove(r) + if r in final_based_on and r != "Itemwise Discount": + final_based_on.remove(r) # Check for authorization set on particular roles - based_on = [x[0] for x in frappe.db.sql("""select based_on + based_on = [ + x[0] + for x in frappe.db.sql( + """select based_on from `tabAuthorization Rule` where transaction = %s and system_role IN (%s) and based_on IN (%s) and (company = %s or ifnull(company,'')='') and docstatus != 2 - """ % ('%s', "'"+"','".join(frappe.get_roles())+"'", "'"+"','".join(final_based_on)+"'", '%s'), (doctype_name, company))] + """ + % ( + "%s", + "'" + "','".join(frappe.get_roles()) + "'", + "'" + "','".join(final_based_on) + "'", + "%s", + ), + (doctype_name, company), + ) + ] for d in based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, d, doc_obj, 2, company) # Remove role specific rules from global authorization rules for r in based_on: - if r in final_based_on and r != 'Itemwise Discount': final_based_on.remove(r) + if r in final_based_on and r != "Itemwise Discount": + final_based_on.remove(r) # Check for global authorization for g in final_based_on: self.bifurcate_based_on_type(doctype_name, total, av_dis, g, doc_obj, 0, company) - def get_value_based_rule(self,doctype_name,employee,total_claimed_amount,company): - val_lst =[] - val = frappe.db.sql("""select value from `tabAuthorization Rule` + def get_value_based_rule(self, doctype_name, employee, total_claimed_amount, company): + val_lst = [] + val = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)< %s and company = %s and docstatus!=2""", - (doctype_name,employee,employee,total_claimed_amount,company)) + (doctype_name, employee, employee, total_claimed_amount, company), + ) if not val: - val = frappe.db.sql("""select value from `tabAuthorization Rule` + val = frappe.db.sql( + """select value from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)< %s and ifnull(company,'') = '' and docstatus!=2""", - (doctype_name, employee, employee, total_claimed_amount)) + (doctype_name, employee, employee, total_claimed_amount), + ) if val: val_lst = [y[0] for y in val] @@ -166,64 +226,83 @@ class AuthorizationControl(TransactionBase): val_lst.append(0) max_val = max(val_lst) - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and company = %s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)= %s and docstatus!=2""", - (doctype_name,company,employee,employee,flt(max_val)), as_dict=1) + (doctype_name, company, employee, employee, flt(max_val)), + as_dict=1, + ) if not rule: - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and ifnull(company,'') = '' and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(value,0)= %s and docstatus!=2""", - (doctype_name,employee,employee,flt(max_val)), as_dict=1) + (doctype_name, employee, employee, flt(max_val)), + as_dict=1, + ) return rule # related to payroll module only - def get_approver_name(self, doctype_name, total, doc_obj=''): - app_user=[] - app_specific_user =[] - rule ={} + def get_approver_name(self, doctype_name, total, doc_obj=""): + app_user = [] + app_specific_user = [] + rule = {} if doc_obj: - if doctype_name == 'Expense Claim': - rule = self.get_value_based_rule(doctype_name, doc_obj.employee, - doc_obj.total_claimed_amount, doc_obj.company) - elif doctype_name == 'Appraisal': - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + if doctype_name == "Expense Claim": + rule = self.get_value_based_rule( + doctype_name, doc_obj.employee, doc_obj.total_claimed_amount, doc_obj.company + ) + elif doctype_name == "Appraisal": + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and company = %s and docstatus!=2""", - (doctype_name,doc_obj.employee, doc_obj.employee, doc_obj.company),as_dict=1) + (doctype_name, doc_obj.employee, doc_obj.employee, doc_obj.company), + as_dict=1, + ) if not rule: - rule = frappe.db.sql("""select name, to_emp, to_designation, approving_role, approving_user + rule = frappe.db.sql( + """select name, to_emp, to_designation, approving_role, approving_user from `tabAuthorization Rule` where transaction=%s and (to_emp=%s or to_designation IN (select designation from `tabEmployee` where name=%s)) and ifnull(company,'') = '' and docstatus!=2""", - (doctype_name,doc_obj.employee, doc_obj.employee), as_dict=1) + (doctype_name, doc_obj.employee, doc_obj.employee), + as_dict=1, + ) if rule: for m in rule: - if m['to_emp'] or m['to_designation']: - if m['approving_user']: - app_specific_user.append(m['approving_user']) - elif m['approving_role']: - user_lst = [z[0] for z in frappe.db.sql("""select distinct t1.name + if m["to_emp"] or m["to_designation"]: + if m["approving_user"]: + app_specific_user.append(m["approving_user"]) + elif m["approving_role"]: + user_lst = [ + z[0] + for z in frappe.db.sql( + """select distinct t1.name from `tabUser` t1, `tabHas Role` t2 where t2.role=%s and t2.parent=t1.name and t1.name !='Administrator' - and t1.name != 'Guest' and t1.docstatus !=2""", m['approving_role'])] + and t1.name != 'Guest' and t1.docstatus !=2""", + m["approving_role"], + ) + ] for x in user_lst: if not x in app_user: app_user.append(x) - if len(app_specific_user) >0: + if len(app_specific_user) > 0: return app_specific_user else: return app_user diff --git a/erpnext/setup/doctype/authorization_rule/authorization_rule.py b/erpnext/setup/doctype/authorization_rule/authorization_rule.py index e07de3b2934..faecd5ae069 100644 --- a/erpnext/setup/doctype/authorization_rule/authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/authorization_rule.py @@ -10,40 +10,58 @@ from frappe.utils import cstr, flt class AuthorizationRule(Document): def check_duplicate_entry(self): - exists = frappe.db.sql("""select name, docstatus from `tabAuthorization Rule` + exists = frappe.db.sql( + """select name, docstatus from `tabAuthorization Rule` where transaction = %s and based_on = %s and system_user = %s and system_role = %s and approving_user = %s and approving_role = %s and to_emp =%s and to_designation=%s and name != %s""", - (self.transaction, self.based_on, cstr(self.system_user), - cstr(self.system_role), cstr(self.approving_user), - cstr(self.approving_role), cstr(self.to_emp), - cstr(self.to_designation), self.name)) - auth_exists = exists and exists[0][0] or '' + ( + self.transaction, + self.based_on, + cstr(self.system_user), + cstr(self.system_role), + cstr(self.approving_user), + cstr(self.approving_role), + cstr(self.to_emp), + cstr(self.to_designation), + self.name, + ), + ) + auth_exists = exists and exists[0][0] or "" if auth_exists: frappe.throw(_("Duplicate Entry. Please check Authorization Rule {0}").format(auth_exists)) - def validate_rule(self): - if self.transaction != 'Appraisal': + if self.transaction != "Appraisal": if not self.approving_role and not self.approving_user: frappe.throw(_("Please enter Approving Role or Approving User")) elif self.system_user and self.system_user == self.approving_user: frappe.throw(_("Approving User cannot be same as user the rule is Applicable To")) elif self.system_role and self.system_role == self.approving_role: frappe.throw(_("Approving Role cannot be same as role the rule is Applicable To")) - elif self.transaction in ['Purchase Order', 'Purchase Receipt', \ - 'Purchase Invoice', 'Stock Entry'] and self.based_on \ - in ['Average Discount', 'Customerwise Discount', 'Itemwise Discount']: - frappe.throw(_("Cannot set authorization on basis of Discount for {0}").format(self.transaction)) - elif self.based_on == 'Average Discount' and flt(self.value) > 100.00: + elif self.transaction in [ + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + "Stock Entry", + ] and self.based_on in [ + "Average Discount", + "Customerwise Discount", + "Itemwise Discount", + ]: + frappe.throw( + _("Cannot set authorization on basis of Discount for {0}").format(self.transaction) + ) + elif self.based_on == "Average Discount" and flt(self.value) > 100.00: frappe.throw(_("Discount must be less than 100")) - elif self.based_on == 'Customerwise Discount' and not self.master_name: + elif self.based_on == "Customerwise Discount" and not self.master_name: frappe.throw(_("Customer required for 'Customerwise Discount'")) else: - if self.transaction == 'Appraisal': + if self.transaction == "Appraisal": self.based_on = "Not Applicable" def validate(self): self.check_duplicate_entry() self.validate_rule() - if not self.value: self.value = 0.0 + if not self.value: + self.value = 0.0 diff --git a/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py b/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py index 7d3d5d4c4d3..55c1bbb79b1 100644 --- a/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py +++ b/erpnext/setup/doctype/authorization_rule/test_authorization_rule.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Authorization Rule') + class TestAuthorizationRule(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/brand/brand.py b/erpnext/setup/doctype/brand/brand.py index 9b91b456c34..1bb6fc9f16c 100644 --- a/erpnext/setup/doctype/brand/brand.py +++ b/erpnext/setup/doctype/brand/brand.py @@ -11,6 +11,7 @@ from frappe.model.document import Document class Brand(Document): pass + def get_brand_defaults(item, company): item = frappe.get_cached_doc("Item", item) if item.brand: diff --git a/erpnext/setup/doctype/brand/test_brand.py b/erpnext/setup/doctype/brand/test_brand.py index 1c71448cb8d..2e030b09a31 100644 --- a/erpnext/setup/doctype/brand/test_brand.py +++ b/erpnext/setup/doctype/brand/test_brand.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Brand') +test_records = frappe.get_test_records("Brand") diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 3347935234c..ee39d3a4ac5 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -21,7 +21,7 @@ from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_ch class Company(NestedSet): - nsm_parent_field = 'parent_company' + nsm_parent_field = "parent_company" def onload(self): load_address_and_contact(self, "company") @@ -29,12 +29,24 @@ class Company(NestedSet): @frappe.whitelist() def check_if_transactions_exist(self): exists = False - for doctype in ["Sales Invoice", "Delivery Note", "Sales Order", "Quotation", - "Purchase Invoice", "Purchase Receipt", "Purchase Order", "Supplier Quotation"]: - if frappe.db.sql("""select name from `tab%s` where company=%s and docstatus=1 - limit 1""" % (doctype, "%s"), self.name): - exists = True - break + for doctype in [ + "Sales Invoice", + "Delivery Note", + "Sales Order", + "Quotation", + "Purchase Invoice", + "Purchase Receipt", + "Purchase Order", + "Supplier Quotation", + ]: + if frappe.db.sql( + """select name from `tab%s` where company=%s and docstatus=1 + limit 1""" + % (doctype, "%s"), + self.name, + ): + exists = True + break return exists @@ -56,7 +68,7 @@ class Company(NestedSet): def validate_abbr(self): if not self.abbr: - self.abbr = ''.join(c[0] for c in self.company_name.split()).upper() + self.abbr = "".join(c[0] for c in self.company_name.split()).upper() self.abbr = self.abbr.strip() @@ -66,7 +78,9 @@ class Company(NestedSet): if not self.abbr.strip(): frappe.throw(_("Abbreviation is mandatory")) - if frappe.db.sql("select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr)): + if frappe.db.sql( + "select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr) + ): frappe.throw(_("Abbreviation already used for another company")) @frappe.whitelist() @@ -75,37 +89,57 @@ class Company(NestedSet): def validate_default_accounts(self): accounts = [ - ["Default Bank Account", "default_bank_account"], ["Default Cash Account", "default_cash_account"], - ["Default Receivable Account", "default_receivable_account"], ["Default Payable Account", "default_payable_account"], - ["Default Expense Account", "default_expense_account"], ["Default Income Account", "default_income_account"], - ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], ["Stock Adjustment Account", "stock_adjustment_account"], - ["Expense Included In Valuation Account", "expenses_included_in_valuation"], ["Default Payroll Payable Account", "default_payroll_payable_account"] + ["Default Bank Account", "default_bank_account"], + ["Default Cash Account", "default_cash_account"], + ["Default Receivable Account", "default_receivable_account"], + ["Default Payable Account", "default_payable_account"], + ["Default Expense Account", "default_expense_account"], + ["Default Income Account", "default_income_account"], + ["Stock Received But Not Billed Account", "stock_received_but_not_billed"], + ["Stock Adjustment Account", "stock_adjustment_account"], + ["Expense Included In Valuation Account", "expenses_included_in_valuation"], + ["Default Payroll Payable Account", "default_payroll_payable_account"], ] for account in accounts: if self.get(account[1]): for_company = frappe.db.get_value("Account", self.get(account[1]), "company") if for_company != self.name: - frappe.throw(_("Account {0} does not belong to company: {1}").format(self.get(account[1]), self.name)) + frappe.throw( + _("Account {0} does not belong to company: {1}").format(self.get(account[1]), self.name) + ) if get_account_currency(self.get(account[1])) != self.default_currency: - error_message = _("{0} currency must be same as company's default currency. Please select another account.") \ - .format(frappe.bold(account[0])) + error_message = _( + "{0} currency must be same as company's default currency. Please select another account." + ).format(frappe.bold(account[0])) frappe.throw(error_message) def validate_currency(self): if self.is_new(): return - self.previous_default_currency = frappe.get_cached_value('Company', self.name, "default_currency") - if self.default_currency and self.previous_default_currency and \ - self.default_currency != self.previous_default_currency and \ - self.check_if_transactions_exist(): - frappe.throw(_("Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency.")) + self.previous_default_currency = frappe.get_cached_value( + "Company", self.name, "default_currency" + ) + if ( + self.default_currency + and self.previous_default_currency + and self.default_currency != self.previous_default_currency + and self.check_if_transactions_exist() + ): + frappe.throw( + _( + "Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency." + ) + ) def on_update(self): NestedSet.on_update(self) - if not frappe.db.sql("""select name from tabAccount - where company=%s and docstatus<2 limit 1""", self.name): + if not frappe.db.sql( + """select name from tabAccount + where company=%s and docstatus<2 limit 1""", + self.name, + ): if not frappe.local.flags.ignore_chart_of_accounts: frappe.flags.country_change = True self.create_default_accounts() @@ -120,7 +154,8 @@ class Company(NestedSet): if not frappe.db.get_value("Department", {"company": self.name}): from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures - install_post_company_fixtures(frappe._dict({'company_name': self.name})) + + install_post_company_fixtures(frappe._dict({"company_name": self.name})) if not frappe.local.flags.ignore_chart_of_accounts: self.set_default_accounts() @@ -130,12 +165,15 @@ class Company(NestedSet): if self.default_currency: frappe.db.set_value("Currency", self.default_currency, "enabled", 1) - if hasattr(frappe.local, 'enable_perpetual_inventory') and \ - self.name in frappe.local.enable_perpetual_inventory: + if ( + hasattr(frappe.local, "enable_perpetual_inventory") + and self.name in frappe.local.enable_perpetual_inventory + ): frappe.local.enable_perpetual_inventory[self.name] = self.enable_perpetual_inventory if frappe.flags.parent_company_changed: from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Company", "parent_company") frappe.clear_cache() @@ -146,31 +184,48 @@ class Company(NestedSet): {"warehouse_name": _("Stores"), "is_group": 0}, {"warehouse_name": _("Work In Progress"), "is_group": 0}, {"warehouse_name": _("Finished Goods"), "is_group": 0}, - {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}]: + {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}, + ]: - if not frappe.db.exists("Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr)): - warehouse = frappe.get_doc({ - "doctype":"Warehouse", - "warehouse_name": wh_detail["warehouse_name"], - "is_group": wh_detail["is_group"], - "company": self.name, - "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) \ - if not wh_detail["is_group"] else "", - "warehouse_type" : wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None - }) + if not frappe.db.exists( + "Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr) + ): + warehouse = frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": wh_detail["warehouse_name"], + "is_group": wh_detail["is_group"], + "company": self.name, + "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) + if not wh_detail["is_group"] + else "", + "warehouse_type": wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None, + } + ) warehouse.flags.ignore_permissions = True warehouse.flags.ignore_mandatory = True warehouse.insert() def create_default_accounts(self): from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts + frappe.local.flags.ignore_root_company_validation = True create_charts(self.name, self.chart_of_accounts, self.existing_company) - frappe.db.set(self, "default_receivable_account", frappe.db.get_value("Account", - {"company": self.name, "account_type": "Receivable", "is_group": 0})) - frappe.db.set(self, "default_payable_account", frappe.db.get_value("Account", - {"company": self.name, "account_type": "Payable", "is_group": 0})) + frappe.db.set( + self, + "default_receivable_account", + frappe.db.get_value( + "Account", {"company": self.name, "account_type": "Receivable", "is_group": 0} + ), + ) + frappe.db.set( + self, + "default_payable_account", + frappe.db.get_value( + "Account", {"company": self.name, "account_type": "Payable", "is_group": 0} + ), + ) def validate_coa_input(self): if self.create_chart_of_accounts_based_on == "Existing Company": @@ -187,34 +242,46 @@ class Company(NestedSet): def validate_perpetual_inventory(self): if not self.get("__islocal"): if cint(self.enable_perpetual_inventory) == 1 and not self.default_inventory_account: - frappe.msgprint(_("Set default inventory account for perpetual inventory"), - alert=True, indicator='orange') + frappe.msgprint( + _("Set default inventory account for perpetual inventory"), alert=True, indicator="orange" + ) def validate_provisional_account_for_non_stock_items(self): if not self.get("__islocal"): - if cint(self.enable_provisional_accounting_for_non_stock_items) == 1 and not self.default_provisional_account: - frappe.throw(_("Set default {0} account for non stock items").format( - frappe.bold('Provisional Account'))) + if ( + cint(self.enable_provisional_accounting_for_non_stock_items) == 1 + and not self.default_provisional_account + ): + frappe.throw( + _("Set default {0} account for non stock items").format(frappe.bold("Provisional Account")) + ) - make_property_setter("Purchase Receipt", "provisional_expense_account", "hidden", - not self.enable_provisional_accounting_for_non_stock_items, "Check", validate_fields_for_doctype=False) + make_property_setter( + "Purchase Receipt", + "provisional_expense_account", + "hidden", + not self.enable_provisional_accounting_for_non_stock_items, + "Check", + validate_fields_for_doctype=False, + ) def check_country_change(self): frappe.flags.country_change = False - if not self.is_new() and \ - self.country != frappe.get_cached_value('Company', self.name, 'country'): + if not self.is_new() and self.country != frappe.get_cached_value( + "Company", self.name, "country" + ): frappe.flags.country_change = True def set_chart_of_accounts(self): - ''' If parent company is set, chart of accounts will be based on that company ''' + """If parent company is set, chart of accounts will be based on that company""" if self.parent_company: self.create_chart_of_accounts_based_on = "Existing Company" self.existing_company = self.parent_company def validate_parent_company(self): if self.parent_company: - is_group = frappe.get_value('Company', self.parent_company, 'is_group') + is_group = frappe.get_value("Company", self.parent_company, "is_group") if not is_group: frappe.throw(_("Parent Company must be a group company")) @@ -228,29 +295,33 @@ class Company(NestedSet): "depreciation_expense_account": "Depreciation", "capital_work_in_progress_account": "Capital Work in Progress", "asset_received_but_not_billed": "Asset Received But Not Billed", - "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation" + "expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation", } if self.enable_perpetual_inventory: - default_accounts.update({ - "stock_received_but_not_billed": "Stock Received But Not Billed", - "default_inventory_account": "Stock", - "stock_adjustment_account": "Stock Adjustment", - "expenses_included_in_valuation": "Expenses Included In Valuation", - "default_expense_account": "Cost of Goods Sold" - }) + default_accounts.update( + { + "stock_received_but_not_billed": "Stock Received But Not Billed", + "default_inventory_account": "Stock", + "stock_adjustment_account": "Stock Adjustment", + "expenses_included_in_valuation": "Expenses Included In Valuation", + "default_expense_account": "Cost of Goods Sold", + } + ) if self.update_default_account: for default_account in default_accounts: self._set_default_account(default_account, default_accounts.get(default_account)) if not self.default_income_account: - income_account = frappe.db.get_value("Account", - {"account_name": _("Sales"), "company": self.name, "is_group": 0}) + income_account = frappe.db.get_value( + "Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0} + ) if not income_account: - income_account = frappe.db.get_value("Account", - {"account_name": _("Sales Account"), "company": self.name}) + income_account = frappe.db.get_value( + "Account", {"account_name": _("Sales Account"), "company": self.name} + ) self.db_set("default_income_account", income_account) @@ -258,32 +329,38 @@ class Company(NestedSet): self.db_set("default_payable_account", self.default_payable_account) if not self.default_payroll_payable_account: - payroll_payable_account = frappe.db.get_value("Account", - {"account_name": _("Payroll Payable"), "company": self.name, "is_group": 0}) + payroll_payable_account = frappe.db.get_value( + "Account", {"account_name": _("Payroll Payable"), "company": self.name, "is_group": 0} + ) self.db_set("default_payroll_payable_account", payroll_payable_account) if not self.default_employee_advance_account: - employe_advance_account = frappe.db.get_value("Account", - {"account_name": _("Employee Advances"), "company": self.name, "is_group": 0}) + employe_advance_account = frappe.db.get_value( + "Account", {"account_name": _("Employee Advances"), "company": self.name, "is_group": 0} + ) self.db_set("default_employee_advance_account", employe_advance_account) if not self.write_off_account: - write_off_acct = frappe.db.get_value("Account", - {"account_name": _("Write Off"), "company": self.name, "is_group": 0}) + write_off_acct = frappe.db.get_value( + "Account", {"account_name": _("Write Off"), "company": self.name, "is_group": 0} + ) self.db_set("write_off_account", write_off_acct) if not self.exchange_gain_loss_account: - exchange_gain_loss_acct = frappe.db.get_value("Account", - {"account_name": _("Exchange Gain/Loss"), "company": self.name, "is_group": 0}) + exchange_gain_loss_acct = frappe.db.get_value( + "Account", {"account_name": _("Exchange Gain/Loss"), "company": self.name, "is_group": 0} + ) self.db_set("exchange_gain_loss_account", exchange_gain_loss_acct) if not self.disposal_account: - disposal_acct = frappe.db.get_value("Account", - {"account_name": _("Gain/Loss on Asset Disposal"), "company": self.name, "is_group": 0}) + disposal_acct = frappe.db.get_value( + "Account", + {"account_name": _("Gain/Loss on Asset Disposal"), "company": self.name, "is_group": 0}, + ) self.db_set("disposal_account", disposal_acct) @@ -291,35 +368,39 @@ class Company(NestedSet): if self.get(fieldname): return - account = frappe.db.get_value("Account", {"account_type": account_type, "is_group": 0, "company": self.name}) + account = frappe.db.get_value( + "Account", {"account_type": account_type, "is_group": 0, "company": self.name} + ) if account: self.db_set(fieldname, account) def set_mode_of_payment_account(self): - cash = frappe.db.get_value('Mode of Payment', {'type': 'Cash'}, 'name') - if cash and self.default_cash_account \ - and not frappe.db.get_value('Mode of Payment Account', {'company': self.name, 'parent': cash}): - mode_of_payment = frappe.get_doc('Mode of Payment', cash, for_update=True) - mode_of_payment.append('accounts', { - 'company': self.name, - 'default_account': self.default_cash_account - }) + cash = frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name") + if ( + cash + and self.default_cash_account + and not frappe.db.get_value("Mode of Payment Account", {"company": self.name, "parent": cash}) + ): + mode_of_payment = frappe.get_doc("Mode of Payment", cash, for_update=True) + mode_of_payment.append( + "accounts", {"company": self.name, "default_account": self.default_cash_account} + ) mode_of_payment.save(ignore_permissions=True) def create_default_cost_center(self): cc_list = [ { - 'cost_center_name': self.name, - 'company':self.name, - 'is_group': 1, - 'parent_cost_center':None + "cost_center_name": self.name, + "company": self.name, + "is_group": 1, + "parent_cost_center": None, }, { - 'cost_center_name':_('Main'), - 'company':self.name, - 'is_group':0, - 'parent_cost_center':self.name + ' - ' + self.abbr + "cost_center_name": _("Main"), + "company": self.name, + "is_group": 0, + "parent_cost_center": self.name + " - " + self.abbr, }, ] for cc in cc_list: @@ -338,26 +419,32 @@ class Company(NestedSet): def after_rename(self, olddn, newdn, merge=False): frappe.db.set(self, "company_name", newdn) - frappe.db.sql("""update `tabDefaultValue` set defvalue=%s - where defkey='Company' and defvalue=%s""", (newdn, olddn)) + frappe.db.sql( + """update `tabDefaultValue` set defvalue=%s + where defkey='Company' and defvalue=%s""", + (newdn, olddn), + ) clear_defaults_cache() def abbreviate(self): - self.abbr = ''.join(c[0].upper() for c in self.company_name.split()) + self.abbr = "".join(c[0].upper() for c in self.company_name.split()) def on_trash(self): """ - Trash accounts and cost centers for this company if no gl entry exists + Trash accounts and cost centers for this company if no gl entry exists """ NestedSet.validate_if_child_exists(self) frappe.utils.nestedset.update_nsm(self) rec = frappe.db.sql("SELECT name from `tabGL Entry` where company = %s", self.name) if not rec: - frappe.db.sql("""delete from `tabBudget Account` + frappe.db.sql( + """delete from `tabBudget Account` where exists(select name from tabBudget - where name=`tabBudget Account`.parent and company = %s)""", self.name) + where name=`tabBudget Account`.parent and company = %s)""", + self.name, + ) for doctype in ["Account", "Cost Center", "Budget", "Party Account"]: frappe.db.sql("delete from `tab{0}` where company = %s".format(doctype), self.name) @@ -372,26 +459,37 @@ class Company(NestedSet): # clear default accounts, warehouses from item warehouses = frappe.db.sql_list("select name from tabWarehouse where company=%s", self.name) if warehouses: - frappe.db.sql("""delete from `tabItem Reorder` where warehouse in (%s)""" - % ', '.join(['%s']*len(warehouses)), tuple(warehouses)) + frappe.db.sql( + """delete from `tabItem Reorder` where warehouse in (%s)""" + % ", ".join(["%s"] * len(warehouses)), + tuple(warehouses), + ) # reset default company - frappe.db.sql("""update `tabSingles` set value="" + frappe.db.sql( + """update `tabSingles` set value="" where doctype='Global Defaults' and field='default_company' - and value=%s""", self.name) + and value=%s""", + self.name, + ) # reset default company - frappe.db.sql("""update `tabSingles` set value="" + frappe.db.sql( + """update `tabSingles` set value="" where doctype='Chart of Accounts Importer' and field='company' - and value=%s""", self.name) + and value=%s""", + self.name, + ) # delete BOMs boms = frappe.db.sql_list("select name from tabBOM where company=%s", self.name) if boms: frappe.db.sql("delete from tabBOM where company=%s", self.name) for dt in ("BOM Operation", "BOM Item", "BOM Scrap Item", "BOM Explosion Item"): - frappe.db.sql("delete from `tab%s` where parent in (%s)""" - % (dt, ', '.join(['%s']*len(boms))), tuple(boms)) + frappe.db.sql( + "delete from `tab%s` where parent in (%s)" "" % (dt, ", ".join(["%s"] * len(boms))), + tuple(boms), + ) frappe.db.sql("delete from tabEmployee where company=%s", self.name) frappe.db.sql("delete from tabDepartment where company=%s", self.name) @@ -404,18 +502,20 @@ class Company(NestedSet): frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name) # delete Process Deferred Accounts if no GL Entry found - if not frappe.db.get_value('GL Entry', {'company': self.name}): + if not frappe.db.get_value("GL Entry", {"company": self.name}): frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name) def check_parent_changed(self): frappe.flags.parent_company_changed = False - if not self.is_new() and \ - self.parent_company != frappe.db.get_value("Company", self.name, "parent_company"): + if not self.is_new() and self.parent_company != frappe.db.get_value( + "Company", self.name, "parent_company" + ): frappe.flags.parent_company_changed = True + def get_name_with_abbr(name, company): - company_abbr = frappe.get_cached_value('Company', company, "abbr") + company_abbr = frappe.get_cached_value("Company", company, "abbr") parts = name.split(" - ") if parts[-1].lower() != company_abbr.lower(): @@ -423,21 +523,27 @@ def get_name_with_abbr(name, company): return " - ".join(parts) + def install_country_fixtures(company, country): - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) + path = frappe.get_app_path("erpnext", "regional", frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country)) frappe.get_attr(module_name)(company, False) except Exception as e: frappe.log_error() - frappe.throw(_("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format(frappe.bold(country))) + frappe.throw( + _("Failed to setup defaults for country {0}. Please contact support@erpnext.com").format( + frappe.bold(country) + ) + ) def update_company_current_month_sales(company): current_month_year = formatdate(today(), "MM-yyyy") - results = frappe.db.sql(''' + results = frappe.db.sql( + """ SELECT SUM(base_grand_total) AS total, DATE_FORMAT(`posting_date`, '%m-%Y') AS month_year @@ -449,44 +555,58 @@ def update_company_current_month_sales(company): AND company = {company} GROUP BY month_year - '''.format(current_month_year=current_month_year, company=frappe.db.escape(company)), - as_dict = True) + """.format( + current_month_year=current_month_year, company=frappe.db.escape(company) + ), + as_dict=True, + ) - monthly_total = results[0]['total'] if len(results) > 0 else 0 + monthly_total = results[0]["total"] if len(results) > 0 else 0 frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total) + def update_company_monthly_sales(company): - '''Cache past year monthly sales of every company based on sales invoices''' + """Cache past year monthly sales of every company based on sales invoices""" import json from frappe.utils.goal import get_monthly_results - filter_str = "company = {0} and status != 'Draft' and docstatus=1".format(frappe.db.escape(company)) - month_to_value_dict = get_monthly_results("Sales Invoice", "base_grand_total", - "posting_date", filter_str, "sum") + + filter_str = "company = {0} and status != 'Draft' and docstatus=1".format( + frappe.db.escape(company) + ) + month_to_value_dict = get_monthly_results( + "Sales Invoice", "base_grand_total", "posting_date", filter_str, "sum" + ) frappe.db.set_value("Company", company, "sales_monthly_history", json.dumps(month_to_value_dict)) + def update_transactions_annual_history(company, commit=False): transactions_history = get_all_transactions_annual_history(company) - frappe.db.set_value("Company", company, "transactions_annual_history", json.dumps(transactions_history)) + frappe.db.set_value( + "Company", company, "transactions_annual_history", json.dumps(transactions_history) + ) if commit: frappe.db.commit() + def cache_companies_monthly_sales_history(): - companies = [d['name'] for d in frappe.get_list("Company")] + companies = [d["name"] for d in frappe.get_list("Company")] for company in companies: update_company_monthly_sales(company) update_transactions_annual_history(company) frappe.db.commit() + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if parent == None or parent == "All Companies": parent = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name as value, is_group as expandable @@ -495,25 +615,30 @@ def get_children(doctype, parent=None, company=None, is_root=False): where ifnull(parent_company, "")={parent} """.format( - doctype = doctype, - parent=frappe.db.escape(parent) - ), as_dict=1) + doctype=doctype, parent=frappe.db.escape(parent) + ), + as_dict=1, + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = frappe.form_dict args = make_tree_args(**args) - if args.parent_company == 'All Companies': + if args.parent_company == "All Companies": args.parent_company = None frappe.get_doc(args).insert() + def get_all_transactions_annual_history(company): out = {} - items = frappe.db.sql(''' + items = frappe.db.sql( + """ select transaction_date, count(*) as count from ( @@ -553,61 +678,68 @@ def get_all_transactions_annual_history(company): group by transaction_date - ''', (company), as_dict=True) + """, + (company), + as_dict=True, + ) for d in items: timestamp = get_timestamp(d["transaction_date"]) - out.update({ timestamp: d["count"] }) + out.update({timestamp: d["count"]}) return out + def get_timeline_data(doctype, name): - '''returns timeline data based on linked records in dashboard''' + """returns timeline data based on linked records in dashboard""" out = {} date_to_value_dict = {} - history = frappe.get_cached_value('Company', name, "transactions_annual_history") + history = frappe.get_cached_value("Company", name, "transactions_annual_history") try: - date_to_value_dict = json.loads(history) if history and '{' in history else None + date_to_value_dict = json.loads(history) if history and "{" in history else None except ValueError: date_to_value_dict = None if date_to_value_dict is None: update_transactions_annual_history(name, True) - history = frappe.get_cached_value('Company', name, "transactions_annual_history") - return json.loads(history) if history and '{' in history else {} + history = frappe.get_cached_value("Company", name, "transactions_annual_history") + return json.loads(history) if history and "{" in history else {} return date_to_value_dict + @frappe.whitelist() -def get_default_company_address(name, sort_key='is_primary_address', existing_address=None): - if sort_key not in ['is_shipping_address', 'is_primary_address']: +def get_default_company_address(name, sort_key="is_primary_address", existing_address=None): + if sort_key not in ["is_shipping_address", "is_primary_address"]: return None - out = frappe.db.sql(""" SELECT + out = frappe.db.sql( + """ SELECT addr.name, addr.%s FROM `tabAddress` addr, `tabDynamic Link` dl WHERE dl.parent = addr.name and dl.link_doctype = 'Company' and dl.link_name = %s and ifnull(addr.disabled, 0) = 0 - """ %(sort_key, '%s'), (name)) #nosec + """ + % (sort_key, "%s"), + (name), + ) # nosec if existing_address: if existing_address in [d[0] for d in out]: return existing_address if out: - return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0] + return sorted(out, key=functools.cmp_to_key(lambda x, y: cmp(y[1], x[1])))[0][0] else: return None + @frappe.whitelist() def create_transaction_deletion_request(company): - tdr = frappe.get_doc({ - 'doctype': 'Transaction Deletion Record', - 'company': company - }) + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py index b63c05dbd11..ff1e7f1103b 100644 --- a/erpnext/setup/doctype/company/company_dashboard.py +++ b/erpnext/setup/doctype/company/company_dashboard.py @@ -1,41 +1,27 @@ - from frappe import _ def get_data(): return { - 'graph': True, - 'graph_method': "frappe.utils.goal.get_monthly_goal_graph_data", - 'graph_method_args': { - 'title': _('Sales'), - 'goal_value_field': 'monthly_sales_target', - 'goal_total_field': 'total_monthly_sales', - 'goal_history_field': 'sales_monthly_history', - 'goal_doctype': 'Sales Invoice', - 'goal_doctype_link': 'company', - 'goal_field': 'base_grand_total', - 'date_field': 'posting_date', - 'filter_str': "docstatus = 1 and is_opening != 'Yes'", - 'aggregation': 'sum' + "graph": True, + "graph_method": "frappe.utils.goal.get_monthly_goal_graph_data", + "graph_method_args": { + "title": _("Sales"), + "goal_value_field": "monthly_sales_target", + "goal_total_field": "total_monthly_sales", + "goal_history_field": "sales_monthly_history", + "goal_doctype": "Sales Invoice", + "goal_doctype_link": "company", + "goal_field": "base_grand_total", + "date_field": "posting_date", + "filter_str": "docstatus = 1 and is_opening != 'Yes'", + "aggregation": "sum", }, - - 'fieldname': 'company', - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation'] - }, - { - 'label': _('Orders'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Support'), - 'items': ['Issue'] - }, - { - 'label': _('Projects'), - 'items': ['Project'] - } - ] + "fieldname": "company", + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation"]}, + {"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Support"), "items": ["Issue"]}, + {"label": _("Projects"), "items": ["Project"]}, + ], } diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index e175c5435aa..29e056e34f0 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -14,7 +14,8 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"] test_dependencies = ["Fiscal Year"] -test_records = frappe.get_test_records('Company') +test_records = frappe.get_test_records("Company") + class TestCompany(unittest.TestCase): def test_coa_based_on_existing_company(self): @@ -37,8 +38,8 @@ class TestCompany(unittest.TestCase): "account_type": "Cash", "is_group": 0, "root_type": "Asset", - "parent_account": "Cash In Hand - CFEC" - } + "parent_account": "Cash In Hand - CFEC", + }, } for account, acc_property in expected_results.items(): @@ -69,15 +70,22 @@ class TestCompany(unittest.TestCase): company.chart_of_accounts = template company.save() - account_types = ["Cost of Goods Sold", "Depreciation", - "Expenses Included In Valuation", "Fixed Asset", "Payable", "Receivable", - "Stock Adjustment", "Stock Received But Not Billed", "Bank", "Cash", "Stock"] + account_types = [ + "Cost of Goods Sold", + "Depreciation", + "Expenses Included In Valuation", + "Fixed Asset", + "Payable", + "Receivable", + "Stock Adjustment", + "Stock Received But Not Billed", + "Bank", + "Cash", + "Stock", + ] for account_type in account_types: - filters = { - "company": template, - "account_type": account_type - } + filters = {"company": template, "account_type": account_type} if account_type in ["Bank", "Cash"]: filters["is_group"] = 1 @@ -90,8 +98,11 @@ class TestCompany(unittest.TestCase): frappe.delete_doc("Company", template) def delete_mode_of_payment(self, company): - frappe.db.sql(""" delete from `tabMode of Payment Account` - where company =%s """, (company)) + frappe.db.sql( + """ delete from `tabMode of Payment Account` + where company =%s """, + (company), + ) def test_basic_tree(self, records=None): min_lft = 1 @@ -101,12 +112,12 @@ class TestCompany(unittest.TestCase): records = test_records[2:] for company in records: - lft, rgt, parent_company = frappe.db.get_value("Company", company["company_name"], - ["lft", "rgt", "parent_company"]) + lft, rgt, parent_company = frappe.db.get_value( + "Company", company["company_name"], ["lft", "rgt", "parent_company"] + ) if parent_company: - parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company, - ["lft", "rgt"]) + parent_lft, parent_rgt = frappe.db.get_value("Company", parent_company, ["lft", "rgt"]) else: # root parent_lft = min_lft - 1 @@ -125,8 +136,11 @@ class TestCompany(unittest.TestCase): def get_no_of_children(companies, no_of_children): children = [] for company in companies: - children += frappe.db.sql_list("""select name from `tabCompany` - where ifnull(parent_company, '')=%s""", company or '') + children += frappe.db.sql_list( + """select name from `tabCompany` + where ifnull(parent_company, '')=%s""", + company or "", + ) if len(children): return get_no_of_children(children, no_of_children + len(children)) @@ -148,40 +162,45 @@ class TestCompany(unittest.TestCase): child_company.save() self.test_basic_tree() + def create_company_communication(doctype, docname): - comm = frappe.get_doc({ + comm = frappe.get_doc( + { "doctype": "Communication", "communication_type": "Communication", "content": "Deduplication of Links", "communication_medium": "Email", - "reference_doctype":doctype, - "reference_name":docname - }) + "reference_doctype": doctype, + "reference_name": docname, + } + ) comm.insert() + def create_child_company(): child_company = frappe.db.exists("Company", "Test Company") if not child_company: - child_company = frappe.get_doc({ - "doctype":"Company", - "company_name":"Test Company", - "abbr":"test_company", - "default_currency":"INR" - }) + child_company = frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test Company", + "abbr": "test_company", + "default_currency": "INR", + } + ) child_company.insert() else: child_company = frappe.get_doc("Company", child_company) return child_company.name + def create_test_lead_in_company(company): lead = frappe.db.exists("Lead", "Test Lead in new company") if not lead: - lead = frappe.get_doc({ - "doctype": "Lead", - "lead_name": "Test Lead in new company", - "scompany": company - }) + lead = frappe.get_doc( + {"doctype": "Lead", "lead_name": "Test Lead in new company", "scompany": company} + ) lead.insert() else: lead = frappe.get_doc("Lead", lead) diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.py b/erpnext/setup/doctype/currency_exchange/currency_exchange.py index 4191935742f..f9f3b3a7dcb 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.py @@ -17,13 +17,17 @@ class CurrencyExchange(Document): # If both selling and buying enabled purpose = "Selling-Buying" - if cint(self.for_buying)==0 and cint(self.for_selling)==1: + if cint(self.for_buying) == 0 and cint(self.for_selling) == 1: purpose = "Selling" - if cint(self.for_buying)==1 and cint(self.for_selling)==0: + if cint(self.for_buying) == 1 and cint(self.for_selling) == 0: purpose = "Buying" - self.name = '{0}-{1}-{2}{3}'.format(formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), - self.from_currency, self.to_currency, ("-" + purpose) if purpose else "") + self.name = "{0}-{1}-{2}{3}".format( + formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), + self.from_currency, + self.to_currency, + ("-" + purpose) if purpose else "", + ) def validate(self): self.validate_value("exchange_rate", ">", 0) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index c8d137c4ca2..dcd06607c3e 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -8,7 +8,7 @@ from frappe.utils import cint, flt from erpnext.setup.utils import get_exchange_rate -test_records = frappe.get_test_records('Currency Exchange') +test_records = frappe.get_test_records("Currency Exchange") def save_new_records(test_records): @@ -16,13 +16,19 @@ def save_new_records(test_records): # If both selling and buying enabled purpose = "Selling-Buying" - if cint(record.get("for_buying"))==0 and cint(record.get("for_selling"))==1: + if cint(record.get("for_buying")) == 0 and cint(record.get("for_selling")) == 1: purpose = "Selling" - if cint(record.get("for_buying"))==1 and cint(record.get("for_selling"))==0: + if cint(record.get("for_buying")) == 1 and cint(record.get("for_selling")) == 0: purpose = "Buying" kwargs = dict( doctype=record.get("doctype"), - docname=record.get("date") + '-' + record.get("from_currency") + '-' + record.get("to_currency") + '-' + purpose, + docname=record.get("date") + + "-" + + record.get("from_currency") + + "-" + + record.get("to_currency") + + "-" + + purpose, fieldname="exchange_rate", value=record.get("exchange_rate"), ) diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 5b917265d99..246cc195e12 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -8,7 +8,8 @@ from frappe.utils.nestedset import NestedSet, get_root_of class CustomerGroup(NestedSet): - nsm_parent_field = 'parent_customer_group' + nsm_parent_field = "parent_customer_group" + def validate(self): if not self.parent_customer_group: self.parent_customer_group = get_root_of("Customer Group") @@ -22,12 +23,18 @@ class CustomerGroup(NestedSet): if frappe.db.exists("Customer", self.name): frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1) -def get_parent_customer_groups(customer_group): - lft, rgt = frappe.db.get_value("Customer Group", customer_group, ['lft', 'rgt']) - return frappe.db.sql("""select name from `tabCustomer Group` +def get_parent_customer_groups(customer_group): + lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"]) + + return frappe.db.sql( + """select name from `tabCustomer Group` where lft <= %s and rgt >= %s - order by lft asc""", (lft, rgt), as_dict=True) + order by lft asc""", + (lft, rgt), + as_dict=True, + ) + def on_doctype_update(): frappe.db.add_index("Customer Group", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/customer_group/test_customer_group.py b/erpnext/setup/doctype/customer_group/test_customer_group.py index f02ae097928..88762701f59 100644 --- a/erpnext/setup/doctype/customer_group/test_customer_group.py +++ b/erpnext/setup/doctype/customer_group/test_customer_group.py @@ -4,7 +4,6 @@ test_ignore = ["Price List"] - import frappe -test_records = frappe.get_test_records('Customer Group') +test_records = frappe.get_test_records("Customer Group") diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 02f9156e196..cdfea7764f1 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -36,16 +36,22 @@ class EmailDigest(Document): self.from_date, self.to_date = self.get_from_to_date() self.set_dates() self._accounts = {} - self.currency = frappe.db.get_value('Company', self.company, "default_currency") + self.currency = frappe.db.get_value("Company", self.company, "default_currency") @frappe.whitelist() def get_users(self): """get list of users""" - user_list = frappe.db.sql(""" + user_list = frappe.db.sql( + """ select name, enabled from tabUser where name not in ({}) and user_type != "Website User" - order by enabled desc, name asc""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS, as_dict=1) + order by enabled desc, name asc""".format( + ", ".join(["%s"] * len(STANDARD_USERS)) + ), + STANDARD_USERS, + as_dict=1, + ) if self.recipient_list: recipient_list = self.recipient_list.split("\n") @@ -54,13 +60,18 @@ class EmailDigest(Document): for p in user_list: p["checked"] = p["name"] in recipient_list and 1 or 0 - frappe.response['user_list'] = user_list + frappe.response["user_list"] = user_list @frappe.whitelist() def send(self): # send email only to enabled users - valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` - where enabled=1""")] + valid_users = [ + p[0] + for p in frappe.db.sql( + """select name from `tabUser` + where enabled=1""" + ) + ] if self.recipients: for row in self.recipients: @@ -70,9 +81,10 @@ class EmailDigest(Document): recipients=row.recipient, subject=_("{0} Digest").format(self.frequency), message=msg_for_this_recipient, - reference_doctype = self.doctype, - reference_name = self.name, - unsubscribe_message = _("Unsubscribe from this Email Digest")) + reference_doctype=self.doctype, + reference_name=self.name, + unsubscribe_message=_("Unsubscribe from this Email Digest"), + ) def get_msg_html(self): """Build email digest content""" @@ -104,7 +116,10 @@ class EmailDigest(Document): context.quote = {"text": quote[0], "author": quote[1]} if self.get("purchase_orders_items_overdue"): - context.purchase_order_list, context.purchase_orders_items_overdue_list = self.get_purchase_orders_items_overdue_list() + ( + context.purchase_order_list, + context.purchase_orders_items_overdue_list, + ) = self.get_purchase_orders_items_overdue_list() if not context.purchase_order_list: frappe.throw(_("No items to be received are overdue")) @@ -114,49 +129,54 @@ class EmailDigest(Document): frappe.flags.ignore_account_permission = False # style - return frappe.render_template("erpnext/setup/doctype/email_digest/templates/default.html", - context, is_path=True) + return frappe.render_template( + "erpnext/setup/doctype/email_digest/templates/default.html", context, is_path=True + ) def set_title(self, context): """Set digest title""" - if self.frequency=="Daily": + if self.frequency == "Daily": context.title = _("Daily Reminders") context.subtitle = _("Pending activities for today") - elif self.frequency=="Weekly": + elif self.frequency == "Weekly": context.title = _("This Week's Summary") context.subtitle = _("Summary for this week and pending activities") - elif self.frequency=="Monthly": + elif self.frequency == "Monthly": context.title = _("This Month's Summary") context.subtitle = _("Summary for this month and pending activities") def set_style(self, context): """Set standard digest style""" - context.text_muted = '#8D99A6' - context.text_color = '#36414C' - context.h1 = 'margin-bottom: 30px; margin-top: 40px; font-weight: 400; font-size: 30px;' - context.h2 = 'margin-bottom: 30px; margin-top: -20px; font-weight: 400; font-size: 20px;' - context.label_css = '''display: inline-block; color: {text_muted}; - padding: 3px 7px; margin-right: 7px;'''.format(text_muted = context.text_muted) - context.section_head = 'margin-top: 60px; font-size: 16px;' - context.line_item = 'padding: 5px 0px; margin: 0; border-bottom: 1px solid #d1d8dd;' - context.link_css = 'color: {text_color}; text-decoration: none;'.format(text_color = context.text_color) - + context.text_muted = "#8D99A6" + context.text_color = "#36414C" + context.h1 = "margin-bottom: 30px; margin-top: 40px; font-weight: 400; font-size: 30px;" + context.h2 = "margin-bottom: 30px; margin-top: -20px; font-weight: 400; font-size: 20px;" + context.label_css = """display: inline-block; color: {text_muted}; + padding: 3px 7px; margin-right: 7px;""".format( + text_muted=context.text_muted + ) + context.section_head = "margin-top: 60px; font-size: 16px;" + context.line_item = "padding: 5px 0px; margin: 0; border-bottom: 1px solid #d1d8dd;" + context.link_css = "color: {text_color}; text-decoration: none;".format( + text_color=context.text_color + ) def get_notifications(self): """Get notifications for user""" notifications = frappe.desk.notifications.get_notifications() - notifications = sorted(notifications.get("open_count_doctype", {}).items(), - key=lambda a: a[1]) + notifications = sorted(notifications.get("open_count_doctype", {}).items(), key=lambda a: a[1]) - notifications = [{"key": n[0], "value": n[1], - "link": get_url_to_list(n[0])} for n in notifications if n[1]] + notifications = [ + {"key": n[0], "value": n[1], "link": get_url_to_list(n[0])} for n in notifications if n[1] + ] return notifications def get_calendar_events(self): """Get calendar events for given user""" from frappe.desk.doctype.event.event import get_events + from_date, to_date = get_future_date_for_calendaer_event(self.frequency) events = get_events(from_date, to_date) @@ -176,10 +196,13 @@ class EmailDigest(Document): if not user_id: user_id = frappe.session.user - todo_list = frappe.db.sql("""select * + todo_list = frappe.db.sql( + """select * from `tabToDo` where (owner=%s or assigned_by=%s) and status="Open" order by field(priority, 'High', 'Medium', 'Low') asc, date asc limit 20""", - (user_id, user_id), as_dict=True) + (user_id, user_id), + as_dict=True, + ) for t in todo_list: t.link = get_url_to_form("ToDo", t.name) @@ -191,9 +214,11 @@ class EmailDigest(Document): if not user_id: user_id = frappe.session.user - return frappe.db.sql("""select count(*) from `tabToDo` + return frappe.db.sql( + """select count(*) from `tabToDo` where status='Open' and (owner=%s or assigned_by=%s)""", - (user_id, user_id))[0][0] + (user_id, user_id), + )[0][0] def get_issue_list(self, user_id=None): """Get issue list""" @@ -205,9 +230,12 @@ class EmailDigest(Document): if not role_permissions.get("read"): return None - issue_list = frappe.db.sql("""select * + issue_list = frappe.db.sql( + """select * from `tabIssue` where status in ("Replied","Open") - order by modified asc limit 10""", as_dict=True) + order by modified asc limit 10""", + as_dict=True, + ) for t in issue_list: t.link = get_url_to_form("Issue", t.name) @@ -216,17 +244,22 @@ class EmailDigest(Document): def get_issue_count(self): """Get count of Issue""" - return frappe.db.sql("""select count(*) from `tabIssue` - where status in ('Open','Replied') """)[0][0] + return frappe.db.sql( + """select count(*) from `tabIssue` + where status in ('Open','Replied') """ + )[0][0] def get_project_list(self, user_id=None): """Get project list""" if not user_id: user_id = frappe.session.user - project_list = frappe.db.sql("""select * + project_list = frappe.db.sql( + """select * from `tabProject` where status='Open' and project_type='External' - order by modified asc limit 10""", as_dict=True) + order by modified asc limit 10""", + as_dict=True, + ) for t in project_list: t.link = get_url_to_form("Issue", t.name) @@ -235,22 +268,41 @@ class EmailDigest(Document): def get_project_count(self): """Get count of Project""" - return frappe.db.sql("""select count(*) from `tabProject` - where status='Open' and project_type='External'""")[0][0] + return frappe.db.sql( + """select count(*) from `tabProject` + where status='Open' and project_type='External'""" + )[0][0] def set_accounting_cards(self, context): """Create accounting cards if checked""" cache = frappe.cache() context.cards = [] - for key in ("income", "expenses_booked", "income_year_to_date", "expense_year_to_date", - "bank_balance", "credit_balance", "invoiced_amount", "payables", - "sales_orders_to_bill", "purchase_orders_to_bill", "sales_order", "purchase_order", - "sales_orders_to_deliver", "purchase_orders_to_receive", "sales_invoice", "purchase_invoice", - "new_quotations", "pending_quotations"): + for key in ( + "income", + "expenses_booked", + "income_year_to_date", + "expense_year_to_date", + "bank_balance", + "credit_balance", + "invoiced_amount", + "payables", + "sales_orders_to_bill", + "purchase_orders_to_bill", + "sales_order", + "purchase_order", + "sales_orders_to_deliver", + "purchase_orders_to_receive", + "sales_invoice", + "purchase_invoice", + "new_quotations", + "pending_quotations", + ): if self.get(key): - cache_key = "email_digest:card:{0}:{1}:{2}:{3}".format(self.company, self.frequency, key, self.from_date) + cache_key = "email_digest:card:{0}:{1}:{2}:{3}".format( + self.company, self.frequency, key, self.from_date + ) card = cache.get(cache_key) if card: @@ -271,8 +323,9 @@ class EmailDigest(Document): if key == "credit_balance": card.last_value = card.last_value * -1 - card.last_value = self.fmt_money(card.last_value,False if key in ("bank_balance", "credit_balance") else True) - + card.last_value = self.fmt_money( + card.last_value, False if key in ("bank_balance", "credit_balance") else True + ) if card.billed_value: card.billed = int(flt(card.billed_value) / card.value * 100) @@ -285,9 +338,11 @@ class EmailDigest(Document): else: card.delivered = "% Received " + str(card.delivered) - if key =="credit_balance": - card.value = card.value *-1 - card.value = self.fmt_money(card.value,False if key in ("bank_balance", "credit_balance") else True) + if key == "credit_balance": + card.value = card.value * -1 + card.value = self.fmt_money( + card.value, False if key in ("bank_balance", "credit_balance") else True + ) cache.set_value(cache_key, card, expires_in_sec=24 * 60 * 60) @@ -295,30 +350,25 @@ class EmailDigest(Document): def get_income(self): """Get income for given period""" - income, past_income, count = self.get_period_amounts(self.get_roots("income"),'income') + income, past_income, count = self.get_period_amounts(self.get_roots("income"), "income") - income_account = frappe.db.get_all('Account', + income_account = frappe.db.get_all( + "Account", fields=["name"], - filters={ - "root_type":"Income", - "parent_account":'', - "company": self.company - }) + filters={"root_type": "Income", "parent_account": "", "company": self.company}, + ) - label = get_link_to_report("General Ledger",self.meta.get_label("income"), + label = get_link_to_report( + "General Ledger", + self.meta.get_label("income"), filters={ "from_date": self.future_from_date, "to_date": self.future_to_date, "account": income_account[0].name, - "company": self.company - } + "company": self.company, + }, ) - return { - "label": label, - "value": income, - "last_value": past_income, - "count": count - } + return {"label": label, "value": income, "last_value": past_income, "count": count} def get_income_year_to_date(self): """Get income to date""" @@ -326,7 +376,7 @@ class EmailDigest(Document): def get_expense_year_to_date(self): """Get income to date""" - return self.get_year_to_date_balance("expense","expenses_booked") + return self.get_year_to_date_balance("expense", "expenses_booked") def get_year_to_date_balance(self, root_type, fieldname): """Get income to date""" @@ -334,67 +384,63 @@ class EmailDigest(Document): count = 0 for account in self.get_root_type_accounts(root_type): - balance += get_balance_on(account, date = self.future_to_date) - count += get_count_on(account, fieldname, date = self.future_to_date) + balance += get_balance_on(account, date=self.future_to_date) + count += get_count_on(account, fieldname, date=self.future_to_date) - if fieldname == 'income': - filters = { - "currency": self.currency - } - label = get_link_to_report('Profit and Loss Statement', label=self.meta.get_label(root_type + "_year_to_date"), filters=filters) + if fieldname == "income": + filters = {"currency": self.currency} + label = get_link_to_report( + "Profit and Loss Statement", + label=self.meta.get_label(root_type + "_year_to_date"), + filters=filters, + ) - elif fieldname == 'expenses_booked': - filters = { - "currency": self.currency - } - label = get_link_to_report('Profit and Loss Statement', label=self.meta.get_label(root_type + "_year_to_date"), filters=filters) + elif fieldname == "expenses_booked": + filters = {"currency": self.currency} + label = get_link_to_report( + "Profit and Loss Statement", + label=self.meta.get_label(root_type + "_year_to_date"), + filters=filters, + ) - return { - "label": label, - "value": balance, - "count": count - } + return {"label": label, "value": balance, "count": count} def get_bank_balance(self): # account is of type "Bank" and root_type is Asset - return self.get_type_balance('bank_balance', 'Bank', root_type='Asset') + return self.get_type_balance("bank_balance", "Bank", root_type="Asset") def get_credit_balance(self): # account is of type "Bank" and root_type is Liability - return self.get_type_balance('credit_balance', 'Bank', root_type='Liability') + return self.get_type_balance("credit_balance", "Bank", root_type="Liability") def get_payables(self): - return self.get_type_balance('payables', 'Payable') + return self.get_type_balance("payables", "Payable") def get_invoiced_amount(self): - return self.get_type_balance('invoiced_amount', 'Receivable') + return self.get_type_balance("invoiced_amount", "Receivable") def get_expenses_booked(self): - expenses, past_expenses, count = self.get_period_amounts(self.get_roots("expense"), 'expenses_booked') - - expense_account = frappe.db.get_all('Account', - fields=["name"], - filters={ - "root_type": "Expense", - "parent_account": '', - "company": self.company - } - ) - - label = get_link_to_report("General Ledger",self.meta.get_label("expenses_booked"), - filters={ - "company":self.company, - "from_date":self.future_from_date, - "to_date":self.future_to_date, - "account": expense_account[0].name - } + expenses, past_expenses, count = self.get_period_amounts( + self.get_roots("expense"), "expenses_booked" ) - return { - "label": label, - "value": expenses, - "last_value": past_expenses, - "count": count - } + + expense_account = frappe.db.get_all( + "Account", + fields=["name"], + filters={"root_type": "Expense", "parent_account": "", "company": self.company}, + ) + + label = get_link_to_report( + "General Ledger", + self.meta.get_label("expenses_booked"), + filters={ + "company": self.company, + "from_date": self.future_from_date, + "to_date": self.future_to_date, + "account": expense_account[0].name, + }, + ) + return {"label": label, "value": expenses, "last_value": past_expenses, "count": count} def get_period_amounts(self, accounts, fieldname): """Get amounts for current and past periods""" @@ -410,113 +456,129 @@ class EmailDigest(Document): def get_sales_orders_to_bill(self): """Get value not billed""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), count(*) from `tabSales Order` where (transaction_date <= %(to_date)s) and billing_status != "Fully Billed" and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_bill"), + label = get_link_to_report( + "Sales Order", + label=self.meta.get_label("sales_orders_to_bill"), report_type="Report Builder", doctype="Sales Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"]], - "per_billed": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"]], + "per_billed": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_sales_orders_to_deliver(self): """Get value not delivered""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_delivered/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_delivered/100)),0), count(*) from `tabSales Order` where (transaction_date <= %(to_date)s) and delivery_status != "Fully Delivered" and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Sales Order', label=self.meta.get_label("sales_orders_to_deliver"), + label = get_link_to_report( + "Sales Order", + label=self.meta.get_label("sales_orders_to_deliver"), report_type="Report Builder", doctype="Sales Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "delivery_status": [['!=', "Fully Delivered"]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "delivery_status": [["!=", "Fully Delivered"]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_purchase_orders_to_receive(self): """Get value not received""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total))-(sum(grand_total*per_received/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total))-(sum(grand_total*per_received/100)),0), count(*) from `tabPurchase Order` where (transaction_date <= %(to_date)s) and per_received < 100 and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_receive"), + label = get_link_to_report( + "Purchase Order", + label=self.meta.get_label("purchase_orders_to_receive"), report_type="Report Builder", doctype="Purchase Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_purchase_orders_to_bill(self): """Get purchase not billed""" - value, count = frappe.db.sql("""select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), + value, count = frappe.db.sql( + """select ifnull((sum(grand_total)) - (sum(grand_total*per_billed/100)),0), count(*) from `tabPurchase Order` where (transaction_date <= %(to_date)s) and per_billed < 100 and company = %(company)s - and status not in ('Closed','Cancelled', 'Completed') """, {"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Closed','Cancelled', 'Completed') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - label = get_link_to_report('Purchase Order', label=self.meta.get_label("purchase_orders_to_bill"), + label = get_link_to_report( + "Purchase Order", + label=self.meta.get_label("purchase_orders_to_bill"), report_type="Report Builder", doctype="Purchase Order", - filters = { - "status": [['!=', "Closed"], ['!=', "Cancelled"], ['!=', "Completed"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Closed"], ["!=", "Cancelled"], ["!=", "Completed"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "count": count - } + return {"label": label, "value": value, "count": count} def get_type_balance(self, fieldname, account_type, root_type=None): if root_type: - accounts = [d.name for d in \ - frappe.db.get_all("Account", filters={"account_type": account_type, - "company": self.company, "is_group": 0, "root_type": root_type})] + accounts = [ + d.name + for d in frappe.db.get_all( + "Account", + filters={ + "account_type": account_type, + "company": self.company, + "is_group": 0, + "root_type": root_type, + }, + ) + ] else: - accounts = [d.name for d in \ - frappe.db.get_all("Account", filters={"account_type": account_type, - "company": self.company, "is_group": 0})] + accounts = [ + d.name + for d in frappe.db.get_all( + "Account", filters={"account_type": account_type, "company": self.company, "is_group": 0} + ) + ] balance = prev_balance = 0.0 count = 0 @@ -525,92 +587,99 @@ class EmailDigest(Document): count += get_count_on(account, fieldname, date=self.future_to_date) prev_balance += get_balance_on(account, date=self.past_to_date, in_account_currency=False) - if fieldname in ("bank_balance","credit_balance"): + if fieldname in ("bank_balance", "credit_balance"): label = "" if fieldname == "bank_balance": filters = { "root_type": "Asset", "account_type": "Bank", "report_date": self.future_to_date, - "company": self.company + "company": self.company, } - label = get_link_to_report('Account Balance', label=self.meta.get_label(fieldname), filters=filters) + label = get_link_to_report( + "Account Balance", label=self.meta.get_label(fieldname), filters=filters + ) else: filters = { "root_type": "Liability", "account_type": "Bank", "report_date": self.future_to_date, - "company": self.company + "company": self.company, } - label = get_link_to_report('Account Balance', label=self.meta.get_label(fieldname), filters=filters) + label = get_link_to_report( + "Account Balance", label=self.meta.get_label(fieldname), filters=filters + ) - return { - 'label': label, - 'value': balance, - 'last_value': prev_balance - } + return {"label": label, "value": balance, "last_value": prev_balance} else: - if account_type == 'Payable': - label = get_link_to_report('Accounts Payable', label=self.meta.get_label(fieldname), - filters={ - "report_date": self.future_to_date, - "company": self.company - } ) - elif account_type == 'Receivable': - label = get_link_to_report('Accounts Receivable', label=self.meta.get_label(fieldname), - filters={ - "report_date": self.future_to_date, - "company": self.company - }) + if account_type == "Payable": + label = get_link_to_report( + "Accounts Payable", + label=self.meta.get_label(fieldname), + filters={"report_date": self.future_to_date, "company": self.company}, + ) + elif account_type == "Receivable": + label = get_link_to_report( + "Accounts Receivable", + label=self.meta.get_label(fieldname), + filters={"report_date": self.future_to_date, "company": self.company}, + ) else: label = self.meta.get_label(fieldname) - return { - 'label': label, - 'value': balance, - 'last_value': prev_balance, - 'count': count - } + return {"label": label, "value": balance, "last_value": prev_balance, "count": count} def get_roots(self, root_type): - return [d.name for d in frappe.db.get_all("Account", - filters={"root_type": root_type.title(), "company": self.company, - "is_group": 1, "parent_account": ["in", ("", None)]})] + return [ + d.name + for d in frappe.db.get_all( + "Account", + filters={ + "root_type": root_type.title(), + "company": self.company, + "is_group": 1, + "parent_account": ["in", ("", None)], + }, + ) + ] def get_root_type_accounts(self, root_type): if not root_type in self._accounts: - self._accounts[root_type] = [d.name for d in \ - frappe.db.get_all("Account", filters={"root_type": root_type.title(), - "company": self.company, "is_group": 0})] + self._accounts[root_type] = [ + d.name + for d in frappe.db.get_all( + "Account", filters={"root_type": root_type.title(), "company": self.company, "is_group": 0} + ) + ] return self._accounts[root_type] def get_purchase_order(self): - return self.get_summary_of_doc("Purchase Order","purchase_order") + return self.get_summary_of_doc("Purchase Order", "purchase_order") def get_sales_order(self): - return self.get_summary_of_doc("Sales Order","sales_order") + return self.get_summary_of_doc("Sales Order", "sales_order") def get_pending_purchase_orders(self): - return self.get_summary_of_pending("Purchase Order","pending_purchase_orders","per_received") + return self.get_summary_of_pending("Purchase Order", "pending_purchase_orders", "per_received") def get_pending_sales_orders(self): - return self.get_summary_of_pending("Sales Order","pending_sales_orders","per_delivered") + return self.get_summary_of_pending("Sales Order", "pending_sales_orders", "per_delivered") def get_sales_invoice(self): - return self.get_summary_of_doc("Sales Invoice","sales_invoice") + return self.get_summary_of_doc("Sales Invoice", "sales_invoice") def get_purchase_invoice(self): - return self.get_summary_of_doc("Purchase Invoice","purchase_invoice") + return self.get_summary_of_doc("Purchase Invoice", "purchase_invoice") def get_new_quotations(self): - return self.get_summary_of_doc("Quotation","new_quotations") + return self.get_summary_of_doc("Quotation", "new_quotations") def get_pending_quotations(self): @@ -618,89 +687,104 @@ class EmailDigest(Document): def get_summary_of_pending(self, doc_type, fieldname, getfield): - value, count, billed_value, delivered_value = frappe.db.sql("""select ifnull(sum(grand_total),0), count(*), + value, count, billed_value, delivered_value = frappe.db.sql( + """select ifnull(sum(grand_total),0), count(*), ifnull(sum(grand_total*per_billed/100),0), ifnull(sum(grand_total*{0}/100),0) from `tab{1}` where (transaction_date <= %(to_date)s) and status not in ('Closed','Cancelled', 'Completed') - and company = %(company)s """.format(getfield, doc_type), - {"to_date": self.future_to_date, "company": self.company})[0] + and company = %(company)s """.format( + getfield, doc_type + ), + {"to_date": self.future_to_date, "company": self.company}, + )[0] return { "label": self.meta.get_label(fieldname), "value": value, "billed_value": billed_value, "delivered_value": delivered_value, - "count": count + "count": count, } def get_summary_of_pending_quotations(self, fieldname): - value, count = frappe.db.sql("""select ifnull(sum(grand_total),0), count(*) from `tabQuotation` + value, count = frappe.db.sql( + """select ifnull(sum(grand_total),0), count(*) from `tabQuotation` where (transaction_date <= %(to_date)s) and company = %(company)s - and status not in ('Ordered','Cancelled', 'Lost') """,{"to_date": self.future_to_date, "company": self.company})[0] + and status not in ('Ordered','Cancelled', 'Lost') """, + {"to_date": self.future_to_date, "company": self.company}, + )[0] - last_value = frappe.db.sql("""select ifnull(sum(grand_total),0) from `tabQuotation` + last_value = frappe.db.sql( + """select ifnull(sum(grand_total),0) from `tabQuotation` where (transaction_date <= %(to_date)s) and company = %(company)s - and status not in ('Ordered','Cancelled', 'Lost') """,{"to_date": self.past_to_date, "company": self.company})[0][0] + and status not in ('Ordered','Cancelled', 'Lost') """, + {"to_date": self.past_to_date, "company": self.company}, + )[0][0] - label = get_link_to_report('Quotation', label=self.meta.get_label(fieldname), + label = get_link_to_report( + "Quotation", + label=self.meta.get_label(fieldname), report_type="Report Builder", doctype="Quotation", - filters = { - "status": [['!=', "Ordered"], ['!=', "Cancelled"], ['!=', "Lost"]], - "per_received": [['<', 100]], - "transaction_date": [['<=', self.future_to_date]], - "company": self.company - } + filters={ + "status": [["!=", "Ordered"], ["!=", "Cancelled"], ["!=", "Lost"]], + "per_received": [["<", 100]], + "transaction_date": [["<=", self.future_to_date]], + "company": self.company, + }, ) - return { - "label": label, - "value": value, - "last_value": last_value, - "count": count - } + return {"label": label, "value": value, "last_value": last_value, "count": count} def get_summary_of_doc(self, doc_type, fieldname): - date_field = 'posting_date' if doc_type in ['Sales Invoice', 'Purchase Invoice'] \ - else 'transaction_date' + date_field = ( + "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" + ) - value = flt(self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total) + value = flt( + self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].grand_total + ) count = self.get_total_on(doc_type, self.future_from_date, self.future_to_date)[0].count - last_value = flt(self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total) + last_value = flt( + self.get_total_on(doc_type, self.past_from_date, self.past_to_date)[0].grand_total + ) filters = { - date_field: [['>=', self.future_from_date], ['<=', self.future_to_date]], - "status": [['!=','Cancelled']], - "company": self.company + date_field: [[">=", self.future_from_date], ["<=", self.future_to_date]], + "status": [["!=", "Cancelled"]], + "company": self.company, } - label = get_link_to_report(doc_type,label=self.meta.get_label(fieldname), - report_type="Report Builder", filters=filters, doctype=doc_type) + label = get_link_to_report( + doc_type, + label=self.meta.get_label(fieldname), + report_type="Report Builder", + filters=filters, + doctype=doc_type, + ) - return { - "label": label, - "value": value, - "last_value": last_value, - "count": count - } + return {"label": label, "value": value, "last_value": last_value, "count": count} def get_total_on(self, doc_type, from_date, to_date): - date_field = 'posting_date' if doc_type in ['Sales Invoice', 'Purchase Invoice'] \ - else 'transaction_date' + date_field = ( + "posting_date" if doc_type in ["Sales Invoice", "Purchase Invoice"] else "transaction_date" + ) - return frappe.get_all(doc_type, + return frappe.get_all( + doc_type, filters={ - date_field: ['between', (from_date, to_date)], - 'status': ['not in', ('Cancelled')], - 'company': self.company + date_field: ["between", (from_date, to_date)], + "status": ["not in", ("Cancelled")], + "company": self.company, }, - fields=['count(*) as count', 'sum(grand_total) as grand_total']) + fields=["count(*) as count", "sum(grand_total) as grand_total"], + ) def get_from_to_date(self): today = now_datetime().date() @@ -717,7 +801,7 @@ class EmailDigest(Document): to_date = from_date + timedelta(days=6) else: # from date is the 1st day of the previous month - from_date = today - relativedelta(days=today.day-1, months=1) + from_date = today - relativedelta(days=today.day - 1, months=1) # to date is the last day of the previous month to_date = today - relativedelta(days=today.day) @@ -728,7 +812,7 @@ class EmailDigest(Document): # decide from date based on email digest frequency if self.frequency == "Daily": - self.past_from_date = self.past_to_date = self.future_from_date - relativedelta(days = 1) + self.past_from_date = self.past_to_date = self.future_from_date - relativedelta(days=1) elif self.frequency == "Weekly": self.past_from_date = self.future_from_date - relativedelta(weeks=1) @@ -755,27 +839,33 @@ class EmailDigest(Document): def onload(self): self.get_next_sending() - def fmt_money(self, value,absol=True): + def fmt_money(self, value, absol=True): if absol: - return fmt_money(abs(value), currency = self.currency) + return fmt_money(abs(value), currency=self.currency) else: return fmt_money(value, currency=self.currency) def get_purchase_orders_items_overdue_list(self): fields_po = "distinct `tabPurchase Order Item`.parent as po" - fields_poi = "`tabPurchase Order Item`.parent, `tabPurchase Order Item`.schedule_date, item_code," \ - "received_qty, qty - received_qty as missing_qty, rate, amount" + fields_poi = ( + "`tabPurchase Order Item`.parent, `tabPurchase Order Item`.schedule_date, item_code," + "received_qty, qty - received_qty as missing_qty, rate, amount" + ) sql_po = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.parent DESC, - `tabPurchase Order Item`.schedule_date DESC""".format(fields=fields_po) + `tabPurchase Order Item`.schedule_date DESC""".format( + fields=fields_po + ) sql_poi = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date - and received_qty < qty order by `tabPurchase Order Item`.idx""".format(fields=fields_poi) + and received_qty < qty order by `tabPurchase Order Item`.idx""".format( + fields=fields_poi + ) purchase_order_list = frappe.db.sql(sql_po, as_dict=True) purchase_order_items_overdue_list = frappe.db.sql(sql_poi, as_dict=True) @@ -785,37 +875,44 @@ class EmailDigest(Document): t.amount = fmt_money(t.amount, 2, t.currency) return purchase_order_list, purchase_order_items_overdue_list + def send(): now_date = now_datetime().date() - for ed in frappe.db.sql("""select name from `tabEmail Digest` - where enabled=1 and docstatus<2""", as_list=1): - ed_obj = frappe.get_doc('Email Digest', ed[0]) - if (now_date == ed_obj.get_next_sending()): + for ed in frappe.db.sql( + """select name from `tabEmail Digest` + where enabled=1 and docstatus<2""", + as_list=1, + ): + ed_obj = frappe.get_doc("Email Digest", ed[0]) + if now_date == ed_obj.get_next_sending(): ed_obj.send() + @frappe.whitelist() def get_digest_msg(name): return frappe.get_doc("Email Digest", name).get_msg_html() + def get_incomes_expenses_for_period(account, from_date, to_date): - """Get amounts for current and past periods""" + """Get amounts for current and past periods""" - val = 0.0 - balance_on_to_date = get_balance_on(account, date = to_date) - balance_before_from_date = get_balance_on(account, date = from_date - timedelta(days=1)) + val = 0.0 + balance_on_to_date = get_balance_on(account, date=to_date) + balance_before_from_date = get_balance_on(account, date=from_date - timedelta(days=1)) - fy_start_date = get_fiscal_year(to_date)[1] + fy_start_date = get_fiscal_year(to_date)[1] - if from_date == fy_start_date: - val = balance_on_to_date - elif from_date > fy_start_date: - val = balance_on_to_date - balance_before_from_date - else: - last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1)) - val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) + if from_date == fy_start_date: + val = balance_on_to_date + elif from_date > fy_start_date: + val = balance_on_to_date - balance_before_from_date + else: + last_year_closing_balance = get_balance_on(account, date=fy_start_date - timedelta(days=1)) + val = balance_on_to_date + (last_year_closing_balance - balance_before_from_date) + + return val - return val def get_count_for_period(account, fieldname, from_date, to_date): count = 0.0 @@ -833,6 +930,7 @@ def get_count_for_period(account, fieldname, from_date, to_date): return count + def get_future_date_for_calendaer_event(frequency): from_date = to_date = today() diff --git a/erpnext/setup/doctype/email_digest/quotes.py b/erpnext/setup/doctype/email_digest/quotes.py index 0fbadd98cd5..8c077a524c2 100644 --- a/erpnext/setup/doctype/email_digest/quotes.py +++ b/erpnext/setup/doctype/email_digest/quotes.py @@ -1,34 +1,65 @@ - import random def get_random_quote(): quotes = [ - ("Start by doing what's necessary; then do what's possible; and suddenly you are doing the impossible.", "Francis of Assisi"), - ("The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart.", "Hellen Keller"), - ("I can't change the direction of the wind, but I can adjust my sails to always reach my destination.", "Jimmy Dean"), + ( + "Start by doing what's necessary; then do what's possible; and suddenly you are doing the impossible.", + "Francis of Assisi", + ), + ( + "The best and most beautiful things in the world cannot be seen or even touched - they must be felt with the heart.", + "Hellen Keller", + ), + ( + "I can't change the direction of the wind, but I can adjust my sails to always reach my destination.", + "Jimmy Dean", + ), ("We know what we are, but know not what we may be.", "William Shakespeare"), - ("There are only two mistakes one can make along the road to truth; not going all the way, and not starting.", "Buddha"), + ( + "There are only two mistakes one can make along the road to truth; not going all the way, and not starting.", + "Buddha", + ), ("Always remember that you are absolutely unique. Just like everyone else.", "Margaret Mead"), - ("You have to learn the rules of the game. And then you have to play better than anyone else.", "Albert Einstein"), + ( + "You have to learn the rules of the game. And then you have to play better than anyone else.", + "Albert Einstein", + ), ("Once we accept our limits, we go beyond them.", "Albert Einstein"), ("Quality is not an act, it is a habit.", "Aristotle"), - ("The more that you read, the more things you will know. The more that you learn, the more places you'll go.", "Dr. Seuss"), + ( + "The more that you read, the more things you will know. The more that you learn, the more places you'll go.", + "Dr. Seuss", + ), ("From there to here, and here to there, funny things are everywhere.", "Dr. Seuss"), ("The secret of getting ahead is getting started.", "Mark Twain"), ("All generalizations are false, including this one.", "Mark Twain"), ("Don't let schooling interfere with your education.", "Mark Twain"), ("Cauliflower is nothing but cabbage with a college education.", "Mark Twain"), - ("It's not the size of the dog in the fight, it's the size of the fight in the dog.", "Mark Twain"), + ( + "It's not the size of the dog in the fight, it's the size of the fight in the dog.", + "Mark Twain", + ), ("Climate is what we expect, weather is what we get.", "Mark Twain"), ("There are lies, damned lies and statistics.", "Mark Twain"), - ("Happiness is when what you think, what you say, and what you do are in harmony.", "Mahatma Gandhi"), - ("First they ignore you, then they laugh at you, then they fight you, then you win.", "Mahatma Gandhi"), + ( + "Happiness is when what you think, what you say, and what you do are in harmony.", + "Mahatma Gandhi", + ), + ( + "First they ignore you, then they laugh at you, then they fight you, then you win.", + "Mahatma Gandhi", + ), ("There is more to life than increasing its speed.", "Mahatma Gandhi"), - ("A small body of determined spirits fired by an unquenchable faith in their mission can alter the course of history.", "Mahatma Gandhi"), + ( + "A small body of determined spirits fired by an unquenchable faith in their mission can alter the course of history.", + "Mahatma Gandhi", + ), ("If two wrongs don't make a right, try three.", "Laurence J. Peter"), ("Inspiration exists, but it has to find you working.", "Pablo Picasso"), - ("The world’s first speeding ticket was given to a man going 4 times the speed limit! Walter Arnold was traveling at a breakneck 8 miles an hour in a 2mph zone, and was caught by a policeman on bicycle and fined one shilling!"), + ( + "The world’s first speeding ticket was given to a man going 4 times the speed limit! Walter Arnold was traveling at a breakneck 8 miles an hour in a 2mph zone, and was caught by a policeman on bicycle and fined one shilling!" + ), ] return random.choice(quotes) diff --git a/erpnext/setup/doctype/email_digest/test_email_digest.py b/erpnext/setup/doctype/email_digest/test_email_digest.py index 3fdf168a65e..dae28b81b5e 100644 --- a/erpnext/setup/doctype/email_digest/test_email_digest.py +++ b/erpnext/setup/doctype/email_digest/test_email_digest.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Email Digest') + class TestEmailDigest(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index f0b720a42e1..984bab47294 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -11,35 +11,37 @@ from frappe.utils import cint keydict = { # "key in defaults": "key in Global Defaults" "fiscal_year": "current_fiscal_year", - 'company': 'default_company', - 'currency': 'default_currency', + "company": "default_company", + "currency": "default_currency", "country": "country", - 'hide_currency_symbol':'hide_currency_symbol', - 'account_url':'account_url', - 'disable_rounded_total': 'disable_rounded_total', - 'disable_in_words': 'disable_in_words', + "hide_currency_symbol": "hide_currency_symbol", + "account_url": "account_url", + "disable_rounded_total": "disable_rounded_total", + "disable_in_words": "disable_in_words", } from frappe.model.document import Document class GlobalDefaults(Document): - def on_update(self): """update defaults""" for key in keydict: - frappe.db.set_default(key, self.get(keydict[key], '')) + frappe.db.set_default(key, self.get(keydict[key], "")) # update year start date and year end date from fiscal_year - year_start_end_date = frappe.db.sql("""select year_start_date, year_end_date - from `tabFiscal Year` where name=%s""", self.current_fiscal_year) + year_start_end_date = frappe.db.sql( + """select year_start_date, year_end_date + from `tabFiscal Year` where name=%s""", + self.current_fiscal_year, + ) if year_start_end_date: - ysd = year_start_end_date[0][0] or '' - yed = year_start_end_date[0][1] or '' + ysd = year_start_end_date[0][0] or "" + yed = year_start_end_date[0][1] or "" if ysd and yed: - frappe.db.set_default('year_start_date', ysd.strftime('%Y-%m-%d')) - frappe.db.set_default('year_end_date', yed.strftime('%Y-%m-%d')) + frappe.db.set_default("year_start_date", ysd.strftime("%Y-%m-%d")) + frappe.db.set_default("year_end_date", yed.strftime("%Y-%m-%d")) # enable default currency if self.default_currency: @@ -59,21 +61,81 @@ class GlobalDefaults(Document): self.disable_rounded_total = cint(self.disable_rounded_total) # Make property setters to hide rounded total fields - for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", - "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"): - make_property_setter(doctype, "base_rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False) + for doctype in ( + "Quotation", + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Supplier Quotation", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + ): + make_property_setter( + doctype, + "base_rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False + ) - make_property_setter(doctype, "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "rounded_total", + "hidden", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, + "rounded_total", + "print_hide", + self.disable_rounded_total, + "Check", + validate_fields_for_doctype=False, + ) - make_property_setter(doctype, "disable_rounded_total", "default", cint(self.disable_rounded_total), "Text", validate_fields_for_doctype=False) + make_property_setter( + doctype, + "disable_rounded_total", + "default", + cint(self.disable_rounded_total), + "Text", + validate_fields_for_doctype=False, + ) def toggle_in_words(self): self.disable_in_words = cint(self.disable_in_words) # Make property setters to hide in words fields - for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note", - "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"): - make_property_setter(doctype, "in_words", "hidden", self.disable_in_words, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "in_words", "print_hide", self.disable_in_words, "Check", validate_fields_for_doctype=False) + for doctype in ( + "Quotation", + "Sales Order", + "Sales Invoice", + "Delivery Note", + "Supplier Quotation", + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + ): + make_property_setter( + doctype, + "in_words", + "hidden", + self.disable_in_words, + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + doctype, + "in_words", + "print_hide", + self.disable_in_words, + "Check", + validate_fields_for_doctype=False, + ) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 91b2f4f974f..890b18c37a9 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -16,12 +16,12 @@ from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder class ItemGroup(NestedSet, WebsiteGenerator): - nsm_parent_field = 'parent_item_group' + nsm_parent_field = "parent_item_group" website = frappe._dict( - condition_field = "show_in_website", - template = "templates/generators/item_group.html", - no_cache = 1, - no_breadcrumbs = 1 + condition_field="show_in_website", + template="templates/generators/item_group.html", + no_cache=1, + no_breadcrumbs=1, ) def autoname(self): @@ -31,8 +31,8 @@ class ItemGroup(NestedSet, WebsiteGenerator): super(ItemGroup, self).validate() if not self.parent_item_group and not frappe.flags.in_test: - if frappe.db.exists("Item Group", _('All Item Groups')): - self.parent_item_group = _('All Item Groups') + if frappe.db.exists("Item Group", _("All Item Groups")): + self.parent_item_group = _("All Item Groups") self.make_route() self.validate_item_group_defaults() @@ -44,15 +44,15 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.delete_child_item_groups_key() def make_route(self): - '''Make website route''' + """Make website route""" if not self.route: - self.route = '' + self.route = "" if self.parent_item_group: - parent_item_group = frappe.get_doc('Item Group', self.parent_item_group) + parent_item_group = frappe.get_doc("Item Group", self.parent_item_group) # make parent route only if not root if parent_item_group.parent_item_group and parent_item_group.route: - self.route = parent_item_group.route + '/' + self.route = parent_item_group.route + "/" self.route += self.scrub(self.item_group_name) @@ -66,28 +66,22 @@ class ItemGroup(NestedSet, WebsiteGenerator): def get_context(self, context): context.show_search = True context.body_class = "product-page" - context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page')) or 6 - context.search_link = '/product_search' + context.page_length = ( + cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 6 + ) + context.search_link = "/product_search" filter_engine = ProductFiltersBuilder(self.name) context.field_filters = filter_engine.get_field_filters() context.attribute_filters = filter_engine.get_attribute_filters() - context.update({ - "parents": get_parent_item_groups(self.parent_item_group), - "title": self.name - }) + context.update({"parents": get_parent_item_groups(self.parent_item_group), "title": self.name}) if self.slideshow: - values = { - 'show_indicators': 1, - 'show_controls': 0, - 'rounded': 1, - 'slider_name': self.slideshow - } + values = {"show_indicators": 1, "show_controls": 0, "rounded": 1, "slider_name": self.slideshow} slideshow = frappe.get_doc("Website Slideshow", self.slideshow) - slides = slideshow.get({"doctype":"Website Slideshow Item"}) + slides = slideshow.get({"doctype": "Website Slideshow Item"}) for index, slide in enumerate(slides): values[f"slide_{index + 1}_image"] = slide.image values[f"slide_{index + 1}_title"] = slide.heading @@ -110,68 +104,64 @@ class ItemGroup(NestedSet, WebsiteGenerator): def validate_item_group_defaults(self): from erpnext.stock.doctype.item.item import validate_item_default_company_links + validate_item_default_company_links(self.item_group_defaults) + def get_child_groups_for_website(item_group_name, immediate=False, include_self=False): """Returns child item groups *excluding* passed group.""" item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - filters = { - "lft": [">", item_group.lft], - "rgt": ["<", item_group.rgt], - "show_in_website": 1 - } + filters = {"lft": [">", item_group.lft], "rgt": ["<", item_group.rgt], "show_in_website": 1} if immediate: filters["parent_item_group"] = item_group_name if include_self: - filters.update({ - "lft": [">=", item_group.lft], - "rgt": ["<=", item_group.rgt] - }) + filters.update({"lft": [">=", item_group.lft], "rgt": ["<=", item_group.rgt]}) + + return frappe.get_all("Item Group", filters=filters, fields=["name", "route"], order_by="name") - return frappe.get_all( - "Item Group", - filters=filters, - fields=["name", "route"], - order_by="name" - ) def get_child_item_groups(item_group_name): - item_group = frappe.get_cached_value("Item Group", - item_group_name, ["lft", "rgt"], as_dict=1) + item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - child_item_groups = [d.name for d in frappe.get_all('Item Group', - filters= {'lft': ('>=', item_group.lft),'rgt': ('<=', item_group.rgt)})] + child_item_groups = [ + d.name + for d in frappe.get_all( + "Item Group", filters={"lft": (">=", item_group.lft), "rgt": ("<=", item_group.rgt)} + ) + ] return child_item_groups or {} + def get_item_for_list_in_html(context): # add missing absolute link in files # user may forget it during upload if (context.get("website_image") or "").startswith("files/"): context["website_image"] = "/" + quote(context["website_image"]) - context["show_availability_status"] = cint(frappe.db.get_single_value('E Commerce Settings', - 'show_availability_status')) + context["show_availability_status"] = cint( + frappe.db.get_single_value("E Commerce Settings", "show_availability_status") + ) - products_template = 'templates/includes/products_as_list.html' + products_template = "templates/includes/products_as_list.html" return frappe.get_template(products_template).render(context) def get_parent_item_groups(item_group_name, from_item=False): - base_nav_page = {"name": _("Shop by Category"), "route":"/shop-by-category"} + base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"} if from_item and frappe.request.environ.get("HTTP_REFERER"): # base page after 'Home' will vary on Item page - last_page = frappe.request.environ["HTTP_REFERER"].split('/')[-1] + last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1] if last_page and last_page in ("shop-by-category", "all-products"): base_nav_page_title = " ".join(last_page.split("-")).title() - base_nav_page = {"name": _(base_nav_page_title), "route":"/"+last_page} + base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page} base_parents = [ - {"name": _("Home"), "route":"/"}, + {"name": _("Home"), "route": "/"}, base_nav_page, ] @@ -179,21 +169,27 @@ def get_parent_item_groups(item_group_name, from_item=False): return base_parents item_group = frappe.db.get_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) - parent_groups = frappe.db.sql("""select name, route from `tabItem Group` + parent_groups = frappe.db.sql( + """select name, route from `tabItem Group` where lft <= %s and rgt >= %s and show_in_website=1 - order by lft asc""", (item_group.lft, item_group.rgt), as_dict=True) + order by lft asc""", + (item_group.lft, item_group.rgt), + as_dict=True, + ) return base_parents + parent_groups + def invalidate_cache_for(doc, item_group=None): if not item_group: item_group = doc.name for d in get_parent_item_groups(item_group): - item_group_name = frappe.db.get_value("Item Group", d.get('name')) + item_group_name = frappe.db.get_value("Item Group", d.get("name")) if item_group_name: - clear_cache(frappe.db.get_value('Item Group', item_group_name, 'route')) + clear_cache(frappe.db.get_value("Item Group", item_group_name, "route")) + def get_item_group_defaults(item, company): item = frappe.get_cached_doc("Item", item) diff --git a/erpnext/setup/doctype/item_group/test_item_group.py b/erpnext/setup/doctype/item_group/test_item_group.py index f6e9ed4ce59..11bc9b92c12 100644 --- a/erpnext/setup/doctype/item_group/test_item_group.py +++ b/erpnext/setup/doctype/item_group/test_item_group.py @@ -14,7 +14,8 @@ from frappe.utils.nestedset import ( rebuild_tree, ) -test_records = frappe.get_test_records('Item Group') +test_records = frappe.get_test_records("Item Group") + class TestItem(unittest.TestCase): def test_basic_tree(self, records=None): @@ -25,12 +26,12 @@ class TestItem(unittest.TestCase): records = test_records[2:] for item_group in records: - lft, rgt, parent_item_group = frappe.db.get_value("Item Group", item_group["item_group_name"], - ["lft", "rgt", "parent_item_group"]) + lft, rgt, parent_item_group = frappe.db.get_value( + "Item Group", item_group["item_group_name"], ["lft", "rgt", "parent_item_group"] + ) if parent_item_group: - parent_lft, parent_rgt = frappe.db.get_value("Item Group", parent_item_group, - ["lft", "rgt"]) + parent_lft, parent_rgt = frappe.db.get_value("Item Group", parent_item_group, ["lft", "rgt"]) else: # root parent_lft = min_lft - 1 @@ -55,8 +56,11 @@ class TestItem(unittest.TestCase): def get_no_of_children(item_groups, no_of_children): children = [] for ig in item_groups: - children += frappe.db.sql_list("""select name from `tabItem Group` - where ifnull(parent_item_group, '')=%s""", ig or '') + children += frappe.db.sql_list( + """select name from `tabItem Group` + where ifnull(parent_item_group, '')=%s""", + ig or "", + ) if len(children): return get_no_of_children(children, no_of_children + len(children)) @@ -119,7 +123,10 @@ class TestItem(unittest.TestCase): def print_tree(self): import json - print(json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1)) + + print( + json.dumps(frappe.db.sql("select name, lft, rgt from `tabItem Group` order by lft"), indent=1) + ) def test_move_leaf_into_another_group(self): # before move @@ -149,12 +156,20 @@ class TestItem(unittest.TestCase): def test_delete_leaf(self): # for checking later - parent_item_group = frappe.db.get_value("Item Group", "_Test Item Group B - 3", "parent_item_group") + parent_item_group = frappe.db.get_value( + "Item Group", "_Test Item Group B - 3", "parent_item_group" + ) rgt = frappe.db.get_value("Item Group", parent_item_group, "rgt") ancestors = get_ancestors_of("Item Group", "_Test Item Group B - 3") - ancestors = frappe.db.sql("""select name, rgt from `tabItem Group` - where name in ({})""".format(", ".join(["%s"]*len(ancestors))), tuple(ancestors), as_dict=True) + ancestors = frappe.db.sql( + """select name, rgt from `tabItem Group` + where name in ({})""".format( + ", ".join(["%s"] * len(ancestors)) + ), + tuple(ancestors), + as_dict=True, + ) frappe.delete_doc("Item Group", "_Test Item Group B - 3") records_to_test = test_records[2:] @@ -173,7 +188,9 @@ class TestItem(unittest.TestCase): def test_delete_group(self): # cannot delete group with child, but can delete leaf - self.assertRaises(NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B") + self.assertRaises( + NestedSetChildExistsError, frappe.delete_doc, "Item Group", "_Test Item Group B" + ) def test_merge_groups(self): frappe.rename_doc("Item Group", "_Test Item Group B", "_Test Item Group C", merge=True) @@ -186,8 +203,10 @@ class TestItem(unittest.TestCase): self.test_basic_tree() # move its children back - for name in frappe.db.sql_list("""select name from `tabItem Group` - where parent_item_group='_Test Item Group C'"""): + for name in frappe.db.sql_list( + """select name from `tabItem Group` + where parent_item_group='_Test Item Group C'""" + ): doc = frappe.get_doc("Item Group", name) doc.parent_item_group = "_Test Item Group B" @@ -206,9 +225,21 @@ class TestItem(unittest.TestCase): self.test_basic_tree() def test_merge_leaf_into_group(self): - self.assertRaises(NestedSetInvalidMergeError, frappe.rename_doc, "Item Group", "_Test Item Group B - 3", - "_Test Item Group B", merge=True) + self.assertRaises( + NestedSetInvalidMergeError, + frappe.rename_doc, + "Item Group", + "_Test Item Group B - 3", + "_Test Item Group B", + merge=True, + ) def test_merge_group_into_leaf(self): - self.assertRaises(NestedSetInvalidMergeError, frappe.rename_doc, "Item Group", "_Test Item Group B", - "_Test Item Group B - 3", merge=True) + self.assertRaises( + NestedSetInvalidMergeError, + frappe.rename_doc, + "Item Group", + "_Test Item Group B", + "_Test Item Group B - 3", + merge=True, + ) diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index 986b4e87ff0..4fba776cb55 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -11,15 +11,25 @@ from frappe.permissions import get_doctypes_with_read from frappe.utils import cint, cstr -class NamingSeriesNotSetError(frappe.ValidationError): pass +class NamingSeriesNotSetError(frappe.ValidationError): + pass + class NamingSeries(Document): @frappe.whitelist() def get_transactions(self, arg=None): - doctypes = list(set(frappe.db.sql_list("""select parent - from `tabDocField` df where fieldname='naming_series'""") - + frappe.db.sql_list("""select dt from `tabCustom Field` - where fieldname='naming_series'"""))) + doctypes = list( + set( + frappe.db.sql_list( + """select parent + from `tabDocField` df where fieldname='naming_series'""" + ) + + frappe.db.sql_list( + """select dt from `tabCustom Field` + where fieldname='naming_series'""" + ) + ) + ) doctypes = list(set(get_doctypes_with_read()).intersection(set(doctypes))) prefixes = "" @@ -28,8 +38,8 @@ class NamingSeries(Document): try: options = self.get_options(d) except frappe.DoesNotExistError: - frappe.msgprint(_('Unable to find DocType {0}').format(d)) - #frappe.pass_does_not_exist_error() + frappe.msgprint(_("Unable to find DocType {0}").format(d)) + # frappe.pass_does_not_exist_error() continue if options: @@ -37,17 +47,21 @@ class NamingSeries(Document): prefixes.replace("\n\n", "\n") prefixes = prefixes.split("\n") - custom_prefixes = frappe.get_all('DocType', fields=["autoname"], - filters={"name": ('not in', doctypes), "autoname":('like', '%.#%'), 'module': ('not in', ['Core'])}) + custom_prefixes = frappe.get_all( + "DocType", + fields=["autoname"], + filters={ + "name": ("not in", doctypes), + "autoname": ("like", "%.#%"), + "module": ("not in", ["Core"]), + }, + ) if custom_prefixes: - prefixes = prefixes + [d.autoname.rsplit('.', 1)[0] for d in custom_prefixes] + prefixes = prefixes + [d.autoname.rsplit(".", 1)[0] for d in custom_prefixes] prefixes = "\n".join(sorted(prefixes)) - return { - "transactions": "\n".join([''] + sorted(doctypes)), - "prefixes": prefixes - } + return {"transactions": "\n".join([""] + sorted(doctypes)), "prefixes": prefixes} def scrub_options_list(self, ol): options = list(filter(lambda x: x, [cstr(n).strip() for n in ol])) @@ -64,7 +78,7 @@ class NamingSeries(Document): self.set_series_for(self.select_doc_for_series, series_list) # create series - map(self.insert_series, [d.split('.')[0] for d in series_list if d.strip()]) + map(self.insert_series, [d.split(".")[0] for d in series_list if d.strip()]) msgprint(_("Series Updated")) @@ -82,32 +96,35 @@ class NamingSeries(Document): self.validate_series_name(i) if options and self.user_must_always_select: - options = [''] + options + options = [""] + options - default = options[0] if options else '' + default = options[0] if options else "" # update in property setter - prop_dict = {'options': "\n".join(options), 'default': default} + prop_dict = {"options": "\n".join(options), "default": default} for prop in prop_dict: - ps_exists = frappe.db.get_value("Property Setter", - {"field_name": 'naming_series', 'doc_type': doctype, 'property': prop}) + ps_exists = frappe.db.get_value( + "Property Setter", {"field_name": "naming_series", "doc_type": doctype, "property": prop} + ) if ps_exists: - ps = frappe.get_doc('Property Setter', ps_exists) + ps = frappe.get_doc("Property Setter", ps_exists) ps.value = prop_dict[prop] ps.save() else: - ps = frappe.get_doc({ - 'doctype': 'Property Setter', - 'doctype_or_field': 'DocField', - 'doc_type': doctype, - 'field_name': 'naming_series', - 'property': prop, - 'value': prop_dict[prop], - 'property_type': 'Text', - '__islocal': 1 - }) + ps = frappe.get_doc( + { + "doctype": "Property Setter", + "doctype_or_field": "DocField", + "doc_type": doctype, + "field_name": "naming_series", + "property": prop, + "value": prop_dict[prop], + "property_type": "Text", + "__islocal": 1, + } + ) ps.save() self.set_options = "\n".join(options) @@ -115,16 +132,22 @@ class NamingSeries(Document): frappe.clear_cache(doctype=doctype) def check_duplicate(self): - parent = list(set( - frappe.db.sql_list("""select dt.name + parent = list( + set( + frappe.db.sql_list( + """select dt.name from `tabDocField` df, `tabDocType` dt where dt.name = df.parent and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series) - + frappe.db.sql_list("""select dt.name + self.select_doc_for_series, + ) + + frappe.db.sql_list( + """select dt.name from `tabCustom Field` df, `tabDocType` dt where dt.name = df.dt and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series) - )) + self.select_doc_for_series, + ) + ) + ) sr = [[frappe.get_meta(p).get_field("naming_series").options, p] for p in parent] dt = frappe.get_doc("DocType", self.select_doc_for_series) options = self.scrub_options_list(self.set_options.split("\n")) @@ -132,14 +155,17 @@ class NamingSeries(Document): validate_series(dt, series) for i in sr: if i[0]: - existing_series = [d.split('.')[0] for d in i[0].split("\n")] + existing_series = [d.split(".")[0] for d in i[0].split("\n")] if series.split(".")[0] in existing_series: - frappe.throw(_("Series {0} already used in {1}").format(series,i[1])) + frappe.throw(_("Series {0} already used in {1}").format(series, i[1])) def validate_series_name(self, n): import re + if not re.match(r"^[\w\- \/.#{}]+$", n, re.UNICODE): - throw(_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series')) + throw( + _('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series') + ) @frappe.whitelist() def get_options(self, arg=None): @@ -151,12 +177,11 @@ class NamingSeries(Document): """get series current""" if self.prefix: prefix = self.parse_naming_series() - self.current_value = frappe.db.get_value("Series", - prefix, "current", order_by = "name") + self.current_value = frappe.db.get_value("Series", prefix, "current", order_by="name") def insert_series(self, series): """insert series if missing""" - if frappe.db.get_value('Series', series, 'name', order_by="name") == None: + if frappe.db.get_value("Series", series, "name", order_by="name") == None: frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series)) @frappe.whitelist() @@ -164,14 +189,15 @@ class NamingSeries(Document): if self.prefix: prefix = self.parse_naming_series() self.insert_series(prefix) - frappe.db.sql("update `tabSeries` set current = %s where name = %s", - (cint(self.current_value), prefix)) + frappe.db.sql( + "update `tabSeries` set current = %s where name = %s", (cint(self.current_value), prefix) + ) msgprint(_("Series Updated Successfully")) else: msgprint(_("Please select prefix first")) def parse_naming_series(self): - parts = self.prefix.split('.') + parts = self.prefix.split(".") # Remove ### from the end of series if parts[-1] == "#" * len(parts[-1]): @@ -180,34 +206,59 @@ class NamingSeries(Document): prefix = parse_naming_series(parts) return prefix -def set_by_naming_series(doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1): + +def set_by_naming_series( + doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1 +): from frappe.custom.doctype.property_setter.property_setter import make_property_setter + if naming_series: - make_property_setter(doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False + ) # set values for mandatory try: - frappe.db.sql("""update `tab{doctype}` set naming_series={s} where - ifnull(naming_series, '')=''""".format(doctype=doctype, s="%s"), - get_default_naming_series(doctype)) + frappe.db.sql( + """update `tab{doctype}` set naming_series={s} where + ifnull(naming_series, '')=''""".format( + doctype=doctype, s="%s" + ), + get_default_naming_series(doctype), + ) except NamingSeriesNotSetError: pass if hide_name_field: make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False + ) else: - make_property_setter(doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False) - make_property_setter(doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False + ) if hide_name_field: - make_property_setter(doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False + ) make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False) # set values for mandatory - frappe.db.sql("""update `tab{doctype}` set `{fieldname}`=`name` where - ifnull({fieldname}, '')=''""".format(doctype=doctype, fieldname=fieldname)) + frappe.db.sql( + """update `tab{doctype}` set `{fieldname}`=`name` where + ifnull({fieldname}, '')=''""".format( + doctype=doctype, fieldname=fieldname + ) + ) + def get_default_naming_series(doctype): naming_series = frappe.get_meta(doctype).get_field("naming_series").options or "" @@ -215,7 +266,9 @@ def get_default_naming_series(doctype): out = naming_series[0] or (naming_series[1] if len(naming_series) > 1 else None) if not out: - frappe.throw(_("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype), - NamingSeriesNotSetError) + frappe.throw( + _("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype), + NamingSeriesNotSetError, + ) else: return out diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index d0d2946e94a..d07ab084500 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -9,18 +9,20 @@ from frappe.model.document import Document class PartyType(Document): pass + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_party_type(doctype, txt, searchfield, start, page_len, filters): - cond = '' - if filters and filters.get('account'): - account_type = frappe.db.get_value('Account', filters.get('account'), 'account_type') + cond = "" + if filters and filters.get("account"): + account_type = frappe.db.get_value("Account", filters.get("account"), "account_type") cond = "and account_type = '%s'" % account_type - return frappe.db.sql("""select name from `tabParty Type` + return frappe.db.sql( + """select name from `tabParty Type` where `{key}` LIKE %(txt)s {cond} - order by name limit %(start)s, %(page_len)s""" - .format(key=searchfield, cond=cond), { - 'txt': '%' + txt + '%', - 'start': start, 'page_len': page_len - }) + order by name limit %(start)s, %(page_len)s""".format( + key=searchfield, cond=cond + ), + {"txt": "%" + txt + "%", "start": start, "page_len": page_len}, + ) diff --git a/erpnext/setup/doctype/party_type/test_party_type.py b/erpnext/setup/doctype/party_type/test_party_type.py index a9a3db8777e..ab92ee15fcb 100644 --- a/erpnext/setup/doctype/party_type/test_party_type.py +++ b/erpnext/setup/doctype/party_type/test_party_type.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Party Type') + class TestPartyType(unittest.TestCase): pass diff --git a/erpnext/setup/doctype/print_heading/test_print_heading.py b/erpnext/setup/doctype/print_heading/test_print_heading.py index 04de08d2697..f0e4c763c4b 100644 --- a/erpnext/setup/doctype/print_heading/test_print_heading.py +++ b/erpnext/setup/doctype/print_heading/test_print_heading.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Print Heading') +test_records = frappe.get_test_records("Print Heading") diff --git a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py index 9330ba85870..891864a69ea 100644 --- a/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py +++ b/erpnext/setup/doctype/quotation_lost_reason/test_quotation_lost_reason.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Quotation Lost Reason') +test_records = frappe.get_test_records("Quotation Lost Reason") diff --git a/erpnext/setup/doctype/sales_partner/sales_partner.py b/erpnext/setup/doctype/sales_partner/sales_partner.py index d2ec49dd6c3..c3136715fe5 100644 --- a/erpnext/setup/doctype/sales_partner/sales_partner.py +++ b/erpnext/setup/doctype/sales_partner/sales_partner.py @@ -10,9 +10,9 @@ from frappe.website.website_generator import WebsiteGenerator class SalesPartner(WebsiteGenerator): website = frappe._dict( - page_title_field = "partner_name", - condition_field = "show_in_website", - template = "templates/generators/sales_partner.html" + page_title_field="partner_name", + condition_field="show_in_website", + template="templates/generators/sales_partner.html", ) def onload(self): @@ -30,18 +30,25 @@ class SalesPartner(WebsiteGenerator): self.partner_website = "http://" + self.partner_website def get_context(self, context): - address = frappe.db.get_value("Address", - {"sales_partner": self.name, "is_primary_address": 1}, - "*", as_dict=True) + address = frappe.db.get_value( + "Address", {"sales_partner": self.name, "is_primary_address": 1}, "*", as_dict=True + ) if address: city_state = ", ".join(filter(None, [address.city, address.state])) - address_rows = [address.address_line1, address.address_line2, - city_state, address.pincode, address.country] + address_rows = [ + address.address_line1, + address.address_line2, + city_state, + address.pincode, + address.country, + ] - context.update({ - "email": address.email_id, - "partner_address": filter_strip_join(address_rows, "\n
    "), - "phone": filter_strip_join(cstr(address.phone).split(","), "\n
    ") - }) + context.update( + { + "email": address.email_id, + "partner_address": filter_strip_join(address_rows, "\n
    "), + "phone": filter_strip_join(cstr(address.phone).split(","), "\n
    "), + } + ) return context diff --git a/erpnext/setup/doctype/sales_partner/test_sales_partner.py b/erpnext/setup/doctype/sales_partner/test_sales_partner.py index 80ef3680147..933f68da5b0 100644 --- a/erpnext/setup/doctype/sales_partner/test_sales_partner.py +++ b/erpnext/setup/doctype/sales_partner/test_sales_partner.py @@ -3,6 +3,6 @@ import frappe -test_records = frappe.get_test_records('Sales Partner') +test_records = frappe.get_test_records("Sales Partner") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index b79a566578d..cd3a6e1f596 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -11,13 +11,13 @@ from erpnext import get_default_currency class SalesPerson(NestedSet): - nsm_parent_field = 'parent_sales_person' + nsm_parent_field = "parent_sales_person" def validate(self): if not self.parent_sales_person: self.parent_sales_person = get_root_of("Sales Person") - for d in self.get('targets') or []: + for d in self.get("targets") or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory.")) self.validate_employee_id() @@ -28,17 +28,20 @@ class SalesPerson(NestedSet): def load_dashboard_info(self): company_default_currency = get_default_currency() - allocated_amount = frappe.db.sql(""" + allocated_amount = frappe.db.sql( + """ select sum(allocated_amount) from `tabSales Team` where sales_person = %s and docstatus=1 and parenttype = 'Sales Order' - """,(self.sales_person_name)) + """, + (self.sales_person_name), + ) info = {} info["allocated_amount"] = flt(allocated_amount[0][0]) if allocated_amount else 0 info["currency"] = company_default_currency - self.set_onload('dashboard_info', info) + self.set_onload("dashboard_info", info) def on_update(self): super(SalesPerson, self).on_update() @@ -57,30 +60,46 @@ class SalesPerson(NestedSet): sales_person = frappe.db.get_value("Sales Person", {"employee": self.employee}) if sales_person and sales_person != self.name: - frappe.throw(_("Another Sales Person {0} exists with the same Employee id").format(sales_person)) + frappe.throw( + _("Another Sales Person {0} exists with the same Employee id").format(sales_person) + ) + def on_doctype_update(): frappe.db.add_index("Sales Person", ["lft", "rgt"]) + def get_timeline_data(doctype, name): out = {} - out.update(dict(frappe.db.sql('''select + out.update( + dict( + frappe.db.sql( + """select unix_timestamp(dt.transaction_date), count(st.parenttype) from `tabSales Order` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.transaction_date > date_sub(curdate(), interval 1 year) - group by dt.transaction_date ''', name))) + group by dt.transaction_date """, + name, + ) + ) + ) - sales_invoice = dict(frappe.db.sql('''select + sales_invoice = dict( + frappe.db.sql( + """select unix_timestamp(dt.posting_date), count(st.parenttype) from `tabSales Invoice` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date ''', name)) + group by dt.posting_date """, + name, + ) + ) for key in sales_invoice: if out.get(key): @@ -88,13 +107,18 @@ def get_timeline_data(doctype, name): else: out[key] = sales_invoice[key] - delivery_note = dict(frappe.db.sql('''select + delivery_note = dict( + frappe.db.sql( + """select unix_timestamp(dt.posting_date), count(st.parenttype) from `tabDelivery Note` dt, `tabSales Team` st where st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year) - group by dt.posting_date ''', name)) + group by dt.posting_date """, + name, + ) + ) for key in delivery_note: if out.get(key): diff --git a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py index d0a5dd99dfc..2ec2002d3d7 100644 --- a/erpnext/setup/doctype/sales_person/sales_person_dashboard.py +++ b/erpnext/setup/doctype/sales_person/sales_person_dashboard.py @@ -1,16 +1,14 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on transactions against this Sales Person. See timeline below for details'), - 'fieldname': 'sales_person', - 'transactions': [ - { - 'label': _('Sales'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - ] + "heatmap": True, + "heatmap_message": _( + "This is based on transactions against this Sales Person. See timeline below for details" + ), + "fieldname": "sales_person", + "transactions": [ + {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + ], } diff --git a/erpnext/setup/doctype/sales_person/test_sales_person.py b/erpnext/setup/doctype/sales_person/test_sales_person.py index 786d2cac4da..6ff1888230e 100644 --- a/erpnext/setup/doctype/sales_person/test_sales_person.py +++ b/erpnext/setup/doctype/sales_person/test_sales_person.py @@ -5,6 +5,6 @@ test_dependencies = ["Employee"] import frappe -test_records = frappe.get_test_records('Sales Person') +test_records = frappe.get_test_records("Sales Person") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.py b/erpnext/setup/doctype/supplier_group/supplier_group.py index 381e1250c82..9d2b733b743 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.py +++ b/erpnext/setup/doctype/supplier_group/supplier_group.py @@ -7,7 +7,7 @@ from frappe.utils.nestedset import NestedSet, get_root_of class SupplierGroup(NestedSet): - nsm_parent_field = 'parent_supplier_group' + nsm_parent_field = "parent_supplier_group" def validate(self): if not self.parent_supplier_group: diff --git a/erpnext/setup/doctype/supplier_group/test_supplier_group.py b/erpnext/setup/doctype/supplier_group/test_supplier_group.py index 283b3bfec3c..97ba705a502 100644 --- a/erpnext/setup/doctype/supplier_group/test_supplier_group.py +++ b/erpnext/setup/doctype/supplier_group/test_supplier_group.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Supplier Group') +test_records = frappe.get_test_records("Supplier Group") diff --git a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py index 8e6421eba8a..cafb3eca7f5 100644 --- a/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/terms_and_conditions.py @@ -16,9 +16,15 @@ class TermsandConditions(Document): def validate(self): if self.terms: validate_template(self.terms) - if not cint(self.buying) and not cint(self.selling) and not cint(self.hr) and not cint(self.disabled): + if ( + not cint(self.buying) + and not cint(self.selling) + and not cint(self.hr) + and not cint(self.disabled) + ): throw(_("At least one of the Applicable Modules should be selected")) + @frappe.whitelist() def get_terms_and_conditions(template_name, doc): if isinstance(doc, string_types): diff --git a/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py b/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py index ca9e6c1aef8..171840af98f 100644 --- a/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py +++ b/erpnext/setup/doctype/terms_and_conditions/test_terms_and_conditions.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('Terms and Conditions') +test_records = frappe.get_test_records("Terms and Conditions") diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 4c47d829e98..9bb5569de5b 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -9,13 +9,13 @@ from frappe.utils.nestedset import NestedSet, get_root_of class Territory(NestedSet): - nsm_parent_field = 'parent_territory' + nsm_parent_field = "parent_territory" def validate(self): if not self.parent_territory: self.parent_territory = get_root_of("Territory") - for d in self.get('targets') or []: + for d in self.get("targets") or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory")) @@ -23,5 +23,6 @@ class Territory(NestedSet): super(Territory, self).on_update() self.validate_one_root() + def on_doctype_update(): frappe.db.add_index("Territory", ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/territory/test_territory.py b/erpnext/setup/doctype/territory/test_territory.py index a18b7bf70ef..4ec695d385b 100644 --- a/erpnext/setup/doctype/territory/test_territory.py +++ b/erpnext/setup/doctype/territory/test_territory.py @@ -3,6 +3,6 @@ import frappe -test_records = frappe.get_test_records('Territory') +test_records = frappe.get_test_records("Territory") test_ignore = ["Item Group"] diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index 095c3d0b6fb..319d435ca69 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -8,61 +8,51 @@ import frappe class TestTransactionDeletionRecord(unittest.TestCase): def setUp(self): - create_company('Dunder Mifflin Paper Co') + create_company("Dunder Mifflin Paper Co") def tearDown(self): frappe.db.rollback() def test_doctypes_contain_company_field(self): - tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co') + tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: contains_company = False - doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()['fields'] + doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"] for doctype_field in doctype_fields: - if doctype_field['fieldtype'] == 'Link' and doctype_field['options'] == 'Company': + if doctype_field["fieldtype"] == "Link" and doctype_field["options"] == "Company": contains_company = True break self.assertTrue(contains_company) def test_no_of_docs_is_correct(self): for i in range(5): - create_task('Dunder Mifflin Paper Co') - tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co') + create_task("Dunder Mifflin Paper Co") + tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") for doctype in tdr.doctypes: - if doctype.doctype_name == 'Task': + if doctype.doctype_name == "Task": self.assertEqual(doctype.no_of_docs, 5) def test_deletion_is_successful(self): - create_task('Dunder Mifflin Paper Co') - create_transaction_deletion_request('Dunder Mifflin Paper Co') - tasks_containing_company = frappe.get_all('Task', - filters = { - 'company' : 'Dunder Mifflin Paper Co' - }) + create_task("Dunder Mifflin Paper Co") + create_transaction_deletion_request("Dunder Mifflin Paper Co") + tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"}) self.assertEqual(tasks_containing_company, []) + def create_company(company_name): - company = frappe.get_doc({ - 'doctype': 'Company', - 'company_name': company_name, - 'default_currency': 'INR' - }) - company.insert(ignore_if_duplicate = True) + company = frappe.get_doc( + {"doctype": "Company", "company_name": company_name, "default_currency": "INR"} + ) + company.insert(ignore_if_duplicate=True) + def create_transaction_deletion_request(company): - tdr = frappe.get_doc({ - 'doctype': 'Transaction Deletion Record', - 'company': company - }) + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() return tdr def create_task(company): - task = frappe.get_doc({ - 'doctype': 'Task', - 'company': company, - 'subject': 'Delete' - }) + task = frappe.get_doc({"doctype": "Task", "company": company, "subject": "Delete"}) task.insert() diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 83ce042cde0..78b3939012d 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -11,15 +11,19 @@ from frappe.utils import cint class TransactionDeletionRecord(Document): def validate(self): - frappe.only_for('System Manager') + frappe.only_for("System Manager") self.validate_doctypes_to_be_ignored() def validate_doctypes_to_be_ignored(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in self.doctypes_to_be_ignored: if doctype.doctype_name not in doctypes_to_be_ignored_list: - frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it."), - title=_("Not Allowed")) + frappe.throw( + _( + "DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it." + ), + title=_("Not Allowed"), + ) def before_submit(self): if not self.doctypes_to_be_ignored: @@ -34,38 +38,55 @@ class TransactionDeletionRecord(Document): def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: - self.append('doctypes_to_be_ignored', { - 'doctype_name' : doctype - }) + self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) def delete_bins(self): - frappe.db.sql("""delete from tabBin where warehouse in - (select name from tabWarehouse where company=%s)""", self.company) + frappe.db.sql( + """delete from tabBin where warehouse in + (select name from tabWarehouse where company=%s)""", + self.company, + ) def delete_lead_addresses(self): """Delete addresses to which leads are linked""" - leads = frappe.get_all('Lead', filters={'company': self.company}) + leads = frappe.get_all("Lead", filters={"company": self.company}) leads = ["'%s'" % row.get("name") for row in leads] addresses = [] if leads: - addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name - in ({leads})""".format(leads=",".join(leads))) + addresses = frappe.db.sql_list( + """select parent from `tabDynamic Link` where link_name + in ({leads})""".format( + leads=",".join(leads) + ) + ) if addresses: addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] - frappe.db.sql("""delete from tabAddress where name in ({addresses}) and + frappe.db.sql( + """delete from tabAddress where name in ({addresses}) and name not in (select distinct dl1.parent from `tabDynamic Link` dl1 inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent - and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses))) + and dl1.link_doctype<>dl2.link_doctype)""".format( + addresses=",".join(addresses) + ) + ) - frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead' - and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads))) + frappe.db.sql( + """delete from `tabDynamic Link` where link_doctype='Lead' + and parenttype='Address' and link_name in ({leads})""".format( + leads=",".join(leads) + ) + ) - frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) + frappe.db.sql( + """update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format( + leads=",".join(leads) + ) + ) def reset_company_values(self): - company_obj = frappe.get_doc('Company', self.company) + company_obj = frappe.get_doc("Company", self.company) company_obj.total_monthly_sales = 0 company_obj.sales_monthly_history = None company_obj.save() @@ -76,24 +97,26 @@ class TransactionDeletionRecord(Document): tables = self.get_all_child_doctypes() for docfield in docfields: - if docfield['parent'] != self.doctype: - no_of_docs = self.get_number_of_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + if docfield["parent"] != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company( + docfield["parent"], docfield["fieldname"] + ) if no_of_docs > 0: - self.delete_version_log(docfield['parent'], docfield['fieldname']) - self.delete_communications(docfield['parent'], docfield['fieldname']) - self.populate_doctypes_table(tables, docfield['parent'], no_of_docs) + self.delete_version_log(docfield["parent"], docfield["fieldname"]) + self.delete_communications(docfield["parent"], docfield["fieldname"]) + self.populate_doctypes_table(tables, docfield["parent"], no_of_docs) - self.delete_child_tables(docfield['parent'], docfield['fieldname']) - self.delete_docs_linked_with_specified_company(docfield['parent'], docfield['fieldname']) + self.delete_child_tables(docfield["parent"], docfield["fieldname"]) + self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"]) - naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname') + naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname") if naming_series: - if '#' in naming_series: - self.update_naming_series(naming_series, docfield['parent']) + if "#" in naming_series: + self.update_naming_series(naming_series, docfield["parent"]) def get_doctypes_to_be_ignored_list(self): - singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name') + singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") doctypes_to_be_ignored_list = singles for doctype in self.doctypes_to_be_ignored: doctypes_to_be_ignored_list.append(doctype.doctype_name) @@ -101,81 +124,104 @@ class TransactionDeletionRecord(Document): return doctypes_to_be_ignored_list def get_doctypes_with_company_field(self, doctypes_to_be_ignored_list): - docfields = frappe.get_all('DocField', - filters = { - 'fieldtype': 'Link', - 'options': 'Company', - 'parent': ['not in', doctypes_to_be_ignored_list]}, - fields=['parent', 'fieldname']) + docfields = frappe.get_all( + "DocField", + filters={ + "fieldtype": "Link", + "options": "Company", + "parent": ["not in", doctypes_to_be_ignored_list], + }, + fields=["parent", "fieldname"], + ) return docfields def get_all_child_doctypes(self): - return frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name') + return frappe.get_all("DocType", filters={"istable": 1}, pluck="name") def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname): - return frappe.db.count(doctype, {company_fieldname : self.company}) + return frappe.db.count(doctype, {company_fieldname: self.company}) def populate_doctypes_table(self, tables, doctype, no_of_docs): if doctype not in tables: - self.append('doctypes', { - 'doctype_name' : doctype, - 'no_of_docs' : no_of_docs - }) + self.append("doctypes", {"doctype_name": doctype, "no_of_docs": no_of_docs}) def delete_child_tables(self, doctype, company_fieldname): - parent_docs_to_be_deleted = frappe.get_all(doctype, { - company_fieldname : self.company - }, pluck = 'name') + parent_docs_to_be_deleted = frappe.get_all( + doctype, {company_fieldname: self.company}, pluck="name" + ) - child_tables = frappe.get_all('DocField', filters = { - 'fieldtype': 'Table', - 'parent': doctype - }, pluck = 'options') + child_tables = frappe.get_all( + "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" + ) for table in child_tables: - frappe.db.delete(table, { - 'parent': ['in', parent_docs_to_be_deleted] - }) + frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]}) def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): - frappe.db.delete(doctype, { - company_fieldname : self.company - }) + frappe.db.delete(doctype, {company_fieldname: self.company}) def update_naming_series(self, naming_series, doctype_name): - if '.' in naming_series: - prefix, hashes = naming_series.rsplit('.', 1) + if "." in naming_series: + prefix, hashes = naming_series.rsplit(".", 1) else: - prefix, hashes = naming_series.rsplit('{', 1) - last = frappe.db.sql("""select max(name) from `tab{0}` - where name like %s""".format(doctype_name), prefix + '%') + prefix, hashes = naming_series.rsplit("{", 1) + last = frappe.db.sql( + """select max(name) from `tab{0}` + where name like %s""".format( + doctype_name + ), + prefix + "%", + ) if last and last[0][0]: - last = cint(last[0][0].replace(prefix, '')) + last = cint(last[0][0].replace(prefix, "")) else: last = 0 frappe.db.sql("""update tabSeries set current = %s where name=%s""", (last, prefix)) def delete_version_log(self, doctype, company_fieldname): - frappe.db.sql("""delete from `tabVersion` where ref_doctype=%s and docname in - (select name from `tab{0}` where `{1}`=%s)""".format(doctype, - company_fieldname), (doctype, self.company)) + frappe.db.sql( + """delete from `tabVersion` where ref_doctype=%s and docname in + (select name from `tab{0}` where `{1}`=%s)""".format( + doctype, company_fieldname + ), + (doctype, self.company), + ) def delete_communications(self, doctype, company_fieldname): - reference_docs = frappe.get_all(doctype, filters={company_fieldname:self.company}) + reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company}) reference_doc_names = [r.name for r in reference_docs] - communications = frappe.get_all('Communication', filters={'reference_doctype':doctype,'reference_name':['in', reference_doc_names]}) + communications = frappe.get_all( + "Communication", + filters={"reference_doctype": doctype, "reference_name": ["in", reference_doc_names]}, + ) communication_names = [c.name for c in communications] - frappe.delete_doc('Communication', communication_names, ignore_permissions=True) + frappe.delete_doc("Communication", communication_names, ignore_permissions=True) + @frappe.whitelist() def get_doctypes_to_be_ignored(): - doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget', - 'Party Account', 'Employee', 'Sales Taxes and Charges Template', - 'Purchase Taxes and Charges Template', 'POS Profile', 'BOM', - 'Company', 'Bank Account', 'Item Tax Template', 'Mode of Payment', - 'Item Default', 'Customer', 'Supplier', 'GST Account'] + doctypes_to_be_ignored_list = [ + "Account", + "Cost Center", + "Warehouse", + "Budget", + "Party Account", + "Employee", + "Sales Taxes and Charges Template", + "Purchase Taxes and Charges Template", + "POS Profile", + "BOM", + "Company", + "Bank Account", + "Item Tax Template", + "Mode of Payment", + "Item Default", + "Customer", + "Supplier", + "GST Account", + ] return doctypes_to_be_ignored_list diff --git a/erpnext/setup/doctype/uom/test_uom.py b/erpnext/setup/doctype/uom/test_uom.py index feb43293079..3278d4eab88 100644 --- a/erpnext/setup/doctype/uom/test_uom.py +++ b/erpnext/setup/doctype/uom/test_uom.py @@ -3,4 +3,4 @@ import frappe -test_records = frappe.get_test_records('UOM') +test_records = frappe.get_test_records("UOM") diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 67d11438714..20ba74b8cde 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -20,7 +20,7 @@ default_mail_footer = """
    {1}".format(fileurl, args.get("company_name"))) + frappe.db.set_value( + "Website Settings", + "Website Settings", + "brand_html", + " {1}".format( + fileurl, args.get("company_name") + ), + ) + def create_website(args): website_maker(args) + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/default_website.py b/erpnext/setup/setup_wizard/operations/default_website.py index c11910b584c..40b02b35dfd 100644 --- a/erpnext/setup/setup_wizard/operations/default_website.py +++ b/erpnext/setup/setup_wizard/operations/default_website.py @@ -12,14 +12,14 @@ class website_maker(object): self.args = args self.company = args.company_name self.tagline = args.company_tagline - self.user = args.get('email') + self.user = args.get("email") self.make_web_page() self.make_website_settings() self.make_blog() def make_web_page(self): # home page - homepage = frappe.get_doc('Homepage', 'Homepage') + homepage = frappe.get_doc("Homepage", "Homepage") homepage.company = self.company homepage.tag_line = self.tagline homepage.setup_items() @@ -28,34 +28,25 @@ class website_maker(object): def make_website_settings(self): # update in home page in settings website_settings = frappe.get_doc("Website Settings", "Website Settings") - website_settings.home_page = 'home' + website_settings.home_page = "home" website_settings.brand_html = self.company website_settings.copyright = self.company website_settings.top_bar_items = [] - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label":"Contact", - "url": "/contact" - }) - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label":"Blog", - "url": "/blog" - }) - website_settings.append("top_bar_items", { - "doctype": "Top Bar Item", - "label": _("Products"), - "url": "/all-products" - }) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": "Contact", "url": "/contact"} + ) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": "Blog", "url": "/blog"} + ) + website_settings.append( + "top_bar_items", {"doctype": "Top Bar Item", "label": _("Products"), "url": "/all-products"} + ) website_settings.save() def make_blog(self): - blog_category = frappe.get_doc({ - "doctype": "Blog Category", - "category_name": "general", - "published": 1, - "title": _("General") - }).insert() + blog_category = frappe.get_doc( + {"doctype": "Blog Category", "category_name": "general", "published": 1, "title": _("General")} + ).insert() if not self.user: # Admin setup @@ -69,21 +60,30 @@ class website_maker(object): blogger.avatar = user.user_image blogger.insert() - frappe.get_doc({ - "doctype": "Blog Post", - "title": "Welcome", - "published": 1, - "published_on": nowdate(), - "blogger": blogger.name, - "blog_category": blog_category.name, - "blog_intro": "My First Blog", - "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), - }).insert() + frappe.get_doc( + { + "doctype": "Blog Post", + "title": "Welcome", + "published": 1, + "published_on": nowdate(), + "blogger": blogger.name, + "blog_category": blog_category.name, + "blog_intro": "My First Blog", + "content": frappe.get_template("setup/setup_wizard/data/sample_blog_post.html").render(), + } + ).insert() + def test(): frappe.delete_doc("Web Page", "test-company") frappe.delete_doc("Blog Post", "welcome") frappe.delete_doc("Blogger", "administrator") frappe.delete_doc("Blog Category", "general") - website_maker({'company':"Test Company", 'company_tagline': "Better Tools for Everyone", 'name': "Administrator"}) + website_maker( + { + "company": "Test Company", + "company_tagline": "Better Tools for Everyone", + "name": "Administrator", + } + ) frappe.db.commit() diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py index a54c7b680ce..e079abe5f57 100644 --- a/erpnext/setup/setup_wizard/operations/defaults_setup.py +++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py @@ -12,12 +12,14 @@ def set_default_settings(args): frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") - global_defaults.update({ - 'current_fiscal_year': get_fy_details(args.get('fy_start_date'), args.get('fy_end_date')), - 'default_currency': args.get('currency'), - 'default_company':args.get('company_name') , - "country": args.get("country"), - }) + global_defaults.update( + { + "current_fiscal_year": get_fy_details(args.get("fy_start_date"), args.get("fy_end_date")), + "default_currency": args.get("currency"), + "default_company": args.get("company_name"), + "country": args.get("country"), + } + ) global_defaults.save() @@ -25,13 +27,15 @@ def set_default_settings(args): system_settings.email_footer_address = args.get("company_name") system_settings.save() - domain_settings = frappe.get_single('Domain Settings') - domain_settings.set_active_domains(args.get('domains')) + domain_settings = frappe.get_single("Domain Settings") + domain_settings.set_active_domains(args.get("domains")) stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')}) + stock_settings.default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores")} + ) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 @@ -72,61 +76,74 @@ def set_default_settings(args): hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder") hr_settings.save() + def set_no_copy_fields_in_variant_settings(): # set no copy fields of an item doctype to item variant settings - doc = frappe.get_doc('Item Variant Settings') + doc = frappe.get_doc("Item Variant Settings") doc.set_default_fields() doc.save() + def create_price_lists(args): for pl_type, pl_name in (("Selling", _("Standard Selling")), ("Buying", _("Standard Buying"))): - frappe.get_doc({ - "doctype": "Price List", - "price_list_name": pl_name, - "enabled": 1, - "buying": 1 if pl_type == "Buying" else 0, - "selling": 1 if pl_type == "Selling" else 0, - "currency": args["currency"] - }).insert() + frappe.get_doc( + { + "doctype": "Price List", + "price_list_name": pl_name, + "enabled": 1, + "buying": 1 if pl_type == "Buying" else 0, + "selling": 1 if pl_type == "Selling" else 0, + "currency": args["currency"], + } + ).insert() + def create_employee_for_self(args): - if frappe.session.user == 'Administrator': + if frappe.session.user == "Administrator": return # create employee for self - emp = frappe.get_doc({ - "doctype": "Employee", - "employee_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])), - "user_id": frappe.session.user, - "status": "Active", - "company": args.get("company_name") - }) + emp = frappe.get_doc( + { + "doctype": "Employee", + "employee_name": " ".join(filter(None, [args.get("first_name"), args.get("last_name")])), + "user_id": frappe.session.user, + "status": "Active", + "company": args.get("company_name"), + } + ) emp.flags.ignore_mandatory = True - emp.insert(ignore_permissions = True) + emp.insert(ignore_permissions=True) + def create_territories(): """create two default territories, one for home country and one named Rest of the World""" from frappe.utils.nestedset import get_root_of + country = frappe.db.get_default("country") root_territory = get_root_of("Territory") for name in (country, _("Rest Of The World")): if name and not frappe.db.exists("Territory", name): - frappe.get_doc({ - "doctype": "Territory", - "territory_name": name.replace("'", ""), - "parent_territory": root_territory, - "is_group": "No" - }).insert() + frappe.get_doc( + { + "doctype": "Territory", + "territory_name": name.replace("'", ""), + "parent_territory": root_territory, + "is_group": "No", + } + ).insert() + def create_feed_and_todo(): """update Activity feed and create todo for creation of item, customer, vendor""" return + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index d76d97663cd..530fb9ffaca 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -17,199 +17,376 @@ from frappe.utils.nestedset import rebuild_tree from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates -default_lead_sources = ["Existing Customer", "Reference", "Advertisement", - "Cold Calling", "Exhibition", "Supplier Reference", "Mass Mailing", - "Customer's Vendor", "Campaign", "Walk In"] +default_lead_sources = [ + "Existing Customer", + "Reference", + "Advertisement", + "Cold Calling", + "Exhibition", + "Supplier Reference", + "Mass Mailing", + "Customer's Vendor", + "Campaign", + "Walk In", +] + +default_sales_partner_type = [ + "Channel Partner", + "Distributor", + "Dealer", + "Agent", + "Retailer", + "Implementation Partner", + "Reseller", +] -default_sales_partner_type = ["Channel Partner", "Distributor", "Dealer", "Agent", - "Retailer", "Implementation Partner", "Reseller"] def install(country=None): records = [ # domains - { 'doctype': 'Domain', 'domain': 'Distribution'}, - { 'doctype': 'Domain', 'domain': 'Manufacturing'}, - { 'doctype': 'Domain', 'domain': 'Retail'}, - { 'doctype': 'Domain', 'domain': 'Services'}, - { 'doctype': 'Domain', 'domain': 'Education'}, - { 'doctype': 'Domain', 'domain': 'Healthcare'}, - { 'doctype': 'Domain', 'domain': 'Agriculture'}, - { 'doctype': 'Domain', 'domain': 'Non Profit'}, - + {"doctype": "Domain", "domain": "Distribution"}, + {"doctype": "Domain", "domain": "Manufacturing"}, + {"doctype": "Domain", "domain": "Retail"}, + {"doctype": "Domain", "domain": "Services"}, + {"doctype": "Domain", "domain": "Education"}, + {"doctype": "Domain", "domain": "Healthcare"}, + {"doctype": "Domain", "domain": "Agriculture"}, + {"doctype": "Domain", "domain": "Non Profit"}, # ensure at least an empty Address Template exists for this Country - {'doctype':"Address Template", "country": country}, - + {"doctype": "Address Template", "country": country}, # item group - {'doctype': 'Item Group', 'item_group_name': _('All Item Groups'), - 'is_group': 1, 'parent_item_group': ''}, - {'doctype': 'Item Group', 'item_group_name': _('Products'), - 'is_group': 0, 'parent_item_group': _('All Item Groups'), "show_in_website": 1 }, - {'doctype': 'Item Group', 'item_group_name': _('Raw Material'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Services'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Sub Assemblies'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - {'doctype': 'Item Group', 'item_group_name': _('Consumable'), - 'is_group': 0, 'parent_item_group': _('All Item Groups') }, - + { + "doctype": "Item Group", + "item_group_name": _("All Item Groups"), + "is_group": 1, + "parent_item_group": "", + }, + { + "doctype": "Item Group", + "item_group_name": _("Products"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + "show_in_website": 1, + }, + { + "doctype": "Item Group", + "item_group_name": _("Raw Material"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Services"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Sub Assemblies"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, + { + "doctype": "Item Group", + "item_group_name": _("Consumable"), + "is_group": 0, + "parent_item_group": _("All Item Groups"), + }, # salary component - {'doctype': 'Salary Component', 'salary_component': _('Income Tax'), 'description': _('Income Tax'), 'type': 'Deduction', 'is_income_tax_component': 1}, - {'doctype': 'Salary Component', 'salary_component': _('Basic'), 'description': _('Basic'), 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': _('Arrear'), 'description': _('Arrear'), 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': _('Leave Encashment'), 'description': _('Leave Encashment'), 'type': 'Earning'}, - - + { + "doctype": "Salary Component", + "salary_component": _("Income Tax"), + "description": _("Income Tax"), + "type": "Deduction", + "is_income_tax_component": 1, + }, + { + "doctype": "Salary Component", + "salary_component": _("Basic"), + "description": _("Basic"), + "type": "Earning", + }, + { + "doctype": "Salary Component", + "salary_component": _("Arrear"), + "description": _("Arrear"), + "type": "Earning", + }, + { + "doctype": "Salary Component", + "salary_component": _("Leave Encashment"), + "description": _("Leave Encashment"), + "type": "Earning", + }, # expense claim type - {'doctype': 'Expense Claim Type', 'name': _('Calls'), 'expense_type': _('Calls')}, - {'doctype': 'Expense Claim Type', 'name': _('Food'), 'expense_type': _('Food')}, - {'doctype': 'Expense Claim Type', 'name': _('Medical'), 'expense_type': _('Medical')}, - {'doctype': 'Expense Claim Type', 'name': _('Others'), 'expense_type': _('Others')}, - {'doctype': 'Expense Claim Type', 'name': _('Travel'), 'expense_type': _('Travel')}, - + {"doctype": "Expense Claim Type", "name": _("Calls"), "expense_type": _("Calls")}, + {"doctype": "Expense Claim Type", "name": _("Food"), "expense_type": _("Food")}, + {"doctype": "Expense Claim Type", "name": _("Medical"), "expense_type": _("Medical")}, + {"doctype": "Expense Claim Type", "name": _("Others"), "expense_type": _("Others")}, + {"doctype": "Expense Claim Type", "name": _("Travel"), "expense_type": _("Travel")}, # leave type - {'doctype': 'Leave Type', 'leave_type_name': _('Casual Leave'), 'name': _('Casual Leave'), - 'allow_encashment': 1, 'is_carry_forward': 1, 'max_continuous_days_allowed': '3', 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Compensatory Off'), 'name': _('Compensatory Off'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1, 'is_compensatory':1 }, - {'doctype': 'Leave Type', 'leave_type_name': _('Sick Leave'), 'name': _('Sick Leave'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Privilege Leave'), 'name': _('Privilege Leave'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'include_holiday': 1}, - {'doctype': 'Leave Type', 'leave_type_name': _('Leave Without Pay'), 'name': _('Leave Without Pay'), - 'allow_encashment': 0, 'is_carry_forward': 0, 'is_lwp':1, 'include_holiday': 1}, - + { + "doctype": "Leave Type", + "leave_type_name": _("Casual Leave"), + "name": _("Casual Leave"), + "allow_encashment": 1, + "is_carry_forward": 1, + "max_continuous_days_allowed": "3", + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Compensatory Off"), + "name": _("Compensatory Off"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + "is_compensatory": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Sick Leave"), + "name": _("Sick Leave"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Privilege Leave"), + "name": _("Privilege Leave"), + "allow_encashment": 0, + "is_carry_forward": 0, + "include_holiday": 1, + }, + { + "doctype": "Leave Type", + "leave_type_name": _("Leave Without Pay"), + "name": _("Leave Without Pay"), + "allow_encashment": 0, + "is_carry_forward": 0, + "is_lwp": 1, + "include_holiday": 1, + }, # Employment Type - {'doctype': 'Employment Type', 'employee_type_name': _('Full-time')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Part-time')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Probation')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Contract')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Commission')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Piecework')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Intern')}, - {'doctype': 'Employment Type', 'employee_type_name': _('Apprentice')}, - - + {"doctype": "Employment Type", "employee_type_name": _("Full-time")}, + {"doctype": "Employment Type", "employee_type_name": _("Part-time")}, + {"doctype": "Employment Type", "employee_type_name": _("Probation")}, + {"doctype": "Employment Type", "employee_type_name": _("Contract")}, + {"doctype": "Employment Type", "employee_type_name": _("Commission")}, + {"doctype": "Employment Type", "employee_type_name": _("Piecework")}, + {"doctype": "Employment Type", "employee_type_name": _("Intern")}, + {"doctype": "Employment Type", "employee_type_name": _("Apprentice")}, # Stock Entry Type - {'doctype': 'Stock Entry Type', 'name': 'Material Issue', 'purpose': 'Material Issue'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Receipt', 'purpose': 'Material Receipt'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Transfer', 'purpose': 'Material Transfer'}, - {'doctype': 'Stock Entry Type', 'name': 'Manufacture', 'purpose': 'Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Repack', 'purpose': 'Repack'}, - {'doctype': 'Stock Entry Type', 'name': 'Send to Subcontractor', 'purpose': 'Send to Subcontractor'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Transfer for Manufacture', 'purpose': 'Material Transfer for Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Material Consumption for Manufacture', 'purpose': 'Material Consumption for Manufacture'}, - + {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, + {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, + {"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, + {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, + {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, + { + "doctype": "Stock Entry Type", + "name": "Send to Subcontractor", + "purpose": "Send to Subcontractor", + }, + { + "doctype": "Stock Entry Type", + "name": "Material Transfer for Manufacture", + "purpose": "Material Transfer for Manufacture", + }, + { + "doctype": "Stock Entry Type", + "name": "Material Consumption for Manufacture", + "purpose": "Material Consumption for Manufacture", + }, # Designation - {'doctype': 'Designation', 'designation_name': _('CEO')}, - {'doctype': 'Designation', 'designation_name': _('Manager')}, - {'doctype': 'Designation', 'designation_name': _('Analyst')}, - {'doctype': 'Designation', 'designation_name': _('Engineer')}, - {'doctype': 'Designation', 'designation_name': _('Accountant')}, - {'doctype': 'Designation', 'designation_name': _('Secretary')}, - {'doctype': 'Designation', 'designation_name': _('Associate')}, - {'doctype': 'Designation', 'designation_name': _('Administrative Officer')}, - {'doctype': 'Designation', 'designation_name': _('Business Development Manager')}, - {'doctype': 'Designation', 'designation_name': _('HR Manager')}, - {'doctype': 'Designation', 'designation_name': _('Project Manager')}, - {'doctype': 'Designation', 'designation_name': _('Head of Marketing and Sales')}, - {'doctype': 'Designation', 'designation_name': _('Software Developer')}, - {'doctype': 'Designation', 'designation_name': _('Designer')}, - {'doctype': 'Designation', 'designation_name': _('Researcher')}, - + {"doctype": "Designation", "designation_name": _("CEO")}, + {"doctype": "Designation", "designation_name": _("Manager")}, + {"doctype": "Designation", "designation_name": _("Analyst")}, + {"doctype": "Designation", "designation_name": _("Engineer")}, + {"doctype": "Designation", "designation_name": _("Accountant")}, + {"doctype": "Designation", "designation_name": _("Secretary")}, + {"doctype": "Designation", "designation_name": _("Associate")}, + {"doctype": "Designation", "designation_name": _("Administrative Officer")}, + {"doctype": "Designation", "designation_name": _("Business Development Manager")}, + {"doctype": "Designation", "designation_name": _("HR Manager")}, + {"doctype": "Designation", "designation_name": _("Project Manager")}, + {"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")}, + {"doctype": "Designation", "designation_name": _("Software Developer")}, + {"doctype": "Designation", "designation_name": _("Designer")}, + {"doctype": "Designation", "designation_name": _("Researcher")}, # territory: with two default territories, one for home country and one named Rest of the World - {'doctype': 'Territory', 'territory_name': _('All Territories'), 'is_group': 1, 'name': _('All Territories'), 'parent_territory': ''}, - {'doctype': 'Territory', 'territory_name': country.replace("'", ""), 'is_group': 0, 'parent_territory': _('All Territories')}, - {'doctype': 'Territory', 'territory_name': _("Rest Of The World"), 'is_group': 0, 'parent_territory': _('All Territories')}, - + { + "doctype": "Territory", + "territory_name": _("All Territories"), + "is_group": 1, + "name": _("All Territories"), + "parent_territory": "", + }, + { + "doctype": "Territory", + "territory_name": country.replace("'", ""), + "is_group": 0, + "parent_territory": _("All Territories"), + }, + { + "doctype": "Territory", + "territory_name": _("Rest Of The World"), + "is_group": 0, + "parent_territory": _("All Territories"), + }, # customer group - {'doctype': 'Customer Group', 'customer_group_name': _('All Customer Groups'), 'is_group': 1, 'name': _('All Customer Groups'), 'parent_customer_group': ''}, - {'doctype': 'Customer Group', 'customer_group_name': _('Individual'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Commercial'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Non Profit'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - {'doctype': 'Customer Group', 'customer_group_name': _('Government'), 'is_group': 0, 'parent_customer_group': _('All Customer Groups')}, - + { + "doctype": "Customer Group", + "customer_group_name": _("All Customer Groups"), + "is_group": 1, + "name": _("All Customer Groups"), + "parent_customer_group": "", + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Individual"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Commercial"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Non Profit"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, + { + "doctype": "Customer Group", + "customer_group_name": _("Government"), + "is_group": 0, + "parent_customer_group": _("All Customer Groups"), + }, # supplier group - {'doctype': 'Supplier Group', 'supplier_group_name': _('All Supplier Groups'), 'is_group': 1, 'name': _('All Supplier Groups'), 'parent_supplier_group': ''}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Services'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Local'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Raw Material'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Electrical'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Hardware'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Pharmaceutical'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - {'doctype': 'Supplier Group', 'supplier_group_name': _('Distributor'), 'is_group': 0, 'parent_supplier_group': _('All Supplier Groups')}, - + { + "doctype": "Supplier Group", + "supplier_group_name": _("All Supplier Groups"), + "is_group": 1, + "name": _("All Supplier Groups"), + "parent_supplier_group": "", + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Services"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Local"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Raw Material"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Electrical"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Hardware"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Pharmaceutical"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, + { + "doctype": "Supplier Group", + "supplier_group_name": _("Distributor"), + "is_group": 0, + "parent_supplier_group": _("All Supplier Groups"), + }, # Sales Person - {'doctype': 'Sales Person', 'sales_person_name': _('Sales Team'), 'is_group': 1, "parent_sales_person": ""}, - + { + "doctype": "Sales Person", + "sales_person_name": _("Sales Team"), + "is_group": 1, + "parent_sales_person": "", + }, # Mode of Payment - {'doctype': 'Mode of Payment', - 'mode_of_payment': 'Check' if country=="United States" else _('Cheque'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Cash'), - 'type': 'Cash'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Credit Card'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Wire Transfer'), - 'type': 'Bank'}, - {'doctype': 'Mode of Payment', 'mode_of_payment': _('Bank Draft'), - 'type': 'Bank'}, - + { + "doctype": "Mode of Payment", + "mode_of_payment": "Check" if country == "United States" else _("Cheque"), + "type": "Bank", + }, + {"doctype": "Mode of Payment", "mode_of_payment": _("Cash"), "type": "Cash"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Credit Card"), "type": "Bank"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Wire Transfer"), "type": "Bank"}, + {"doctype": "Mode of Payment", "mode_of_payment": _("Bank Draft"), "type": "Bank"}, # Activity Type - {'doctype': 'Activity Type', 'activity_type': _('Planning')}, - {'doctype': 'Activity Type', 'activity_type': _('Research')}, - {'doctype': 'Activity Type', 'activity_type': _('Proposal Writing')}, - {'doctype': 'Activity Type', 'activity_type': _('Execution')}, - {'doctype': 'Activity Type', 'activity_type': _('Communication')}, - - {'doctype': "Item Attribute", "attribute_name": _("Size"), "item_attribute_values": [ - {"attribute_value": _("Extra Small"), "abbr": "XS"}, - {"attribute_value": _("Small"), "abbr": "S"}, - {"attribute_value": _("Medium"), "abbr": "M"}, - {"attribute_value": _("Large"), "abbr": "L"}, - {"attribute_value": _("Extra Large"), "abbr": "XL"} - ]}, - - {'doctype': "Item Attribute", "attribute_name": _("Colour"), "item_attribute_values": [ - {"attribute_value": _("Red"), "abbr": "RED"}, - {"attribute_value": _("Green"), "abbr": "GRE"}, - {"attribute_value": _("Blue"), "abbr": "BLU"}, - {"attribute_value": _("Black"), "abbr": "BLA"}, - {"attribute_value": _("White"), "abbr": "WHI"} - ]}, - + {"doctype": "Activity Type", "activity_type": _("Planning")}, + {"doctype": "Activity Type", "activity_type": _("Research")}, + {"doctype": "Activity Type", "activity_type": _("Proposal Writing")}, + {"doctype": "Activity Type", "activity_type": _("Execution")}, + {"doctype": "Activity Type", "activity_type": _("Communication")}, + { + "doctype": "Item Attribute", + "attribute_name": _("Size"), + "item_attribute_values": [ + {"attribute_value": _("Extra Small"), "abbr": "XS"}, + {"attribute_value": _("Small"), "abbr": "S"}, + {"attribute_value": _("Medium"), "abbr": "M"}, + {"attribute_value": _("Large"), "abbr": "L"}, + {"attribute_value": _("Extra Large"), "abbr": "XL"}, + ], + }, + { + "doctype": "Item Attribute", + "attribute_name": _("Colour"), + "item_attribute_values": [ + {"attribute_value": _("Red"), "abbr": "RED"}, + {"attribute_value": _("Green"), "abbr": "GRE"}, + {"attribute_value": _("Blue"), "abbr": "BLU"}, + {"attribute_value": _("Black"), "abbr": "BLA"}, + {"attribute_value": _("White"), "abbr": "WHI"}, + ], + }, # Issue Priority - {'doctype': 'Issue Priority', 'name': _('Low')}, - {'doctype': 'Issue Priority', 'name': _('Medium')}, - {'doctype': 'Issue Priority', 'name': _('High')}, - - #Job Applicant Source - {'doctype': 'Job Applicant Source', 'source_name': _('Website Listing')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Walk In')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Employee Referral')}, - {'doctype': 'Job Applicant Source', 'source_name': _('Campaign')}, - - {'doctype': "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, - {'doctype': "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, - {'doctype': "Email Account", "email_id": "jobs@example.com", "append_to": "Job Applicant"}, - - {'doctype': "Party Type", "party_type": "Customer", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Supplier", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Employee", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"}, - - {'doctype': "Opportunity Type", "name": "Hub"}, - {'doctype': "Opportunity Type", "name": _("Sales")}, - {'doctype': "Opportunity Type", "name": _("Support")}, - {'doctype': "Opportunity Type", "name": _("Maintenance")}, - - {'doctype': "Project Type", "project_type": "Internal"}, - {'doctype': "Project Type", "project_type": "External"}, - {'doctype': "Project Type", "project_type": "Other"}, - + {"doctype": "Issue Priority", "name": _("Low")}, + {"doctype": "Issue Priority", "name": _("Medium")}, + {"doctype": "Issue Priority", "name": _("High")}, + # Job Applicant Source + {"doctype": "Job Applicant Source", "source_name": _("Website Listing")}, + {"doctype": "Job Applicant Source", "source_name": _("Walk In")}, + {"doctype": "Job Applicant Source", "source_name": _("Employee Referral")}, + {"doctype": "Job Applicant Source", "source_name": _("Campaign")}, + {"doctype": "Email Account", "email_id": "sales@example.com", "append_to": "Opportunity"}, + {"doctype": "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, + {"doctype": "Email Account", "email_id": "jobs@example.com", "append_to": "Job Applicant"}, + {"doctype": "Party Type", "party_type": "Customer", "account_type": "Receivable"}, + {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Member", "account_type": "Receivable"}, + {"doctype": "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, + {"doctype": "Party Type", "party_type": "Student", "account_type": "Receivable"}, + {"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"}, + {"doctype": "Opportunity Type", "name": "Hub"}, + {"doctype": "Opportunity Type", "name": _("Sales")}, + {"doctype": "Opportunity Type", "name": _("Support")}, + {"doctype": "Opportunity Type", "name": _("Maintenance")}, + {"doctype": "Project Type", "project_type": "Internal"}, + {"doctype": "Project Type", "project_type": "External"}, + {"doctype": "Project Type", "project_type": "Other"}, {"doctype": "Offer Term", "offer_term": _("Date of Joining")}, {"doctype": "Offer Term", "offer_term": _("Annual Salary")}, {"doctype": "Offer Term", "offer_term": _("Probationary Period")}, @@ -222,23 +399,22 @@ def install(country=None): {"doctype": "Offer Term", "offer_term": _("Leaves per Year")}, {"doctype": "Offer Term", "offer_term": _("Notice Period")}, {"doctype": "Offer Term", "offer_term": _("Incentives")}, - - {'doctype': "Print Heading", 'print_heading': _("Credit Note")}, - {'doctype': "Print Heading", 'print_heading': _("Debit Note")}, - + {"doctype": "Print Heading", "print_heading": _("Credit Note")}, + {"doctype": "Print Heading", "print_heading": _("Debit Note")}, # Assessment Group - {'doctype': 'Assessment Group', 'assessment_group_name': _('All Assessment Groups'), - 'is_group': 1, 'parent_assessment_group': ''}, - + { + "doctype": "Assessment Group", + "assessment_group_name": _("All Assessment Groups"), + "is_group": 1, + "parent_assessment_group": "", + }, # Share Management {"doctype": "Share Type", "title": _("Equity")}, {"doctype": "Share Type", "title": _("Preference")}, - # Market Segments {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")}, - # Sales Stages {"doctype": "Sales Stage", "stage_name": _("Prospecting")}, {"doctype": "Sales Stage", "stage_name": _("Qualification")}, @@ -248,42 +424,87 @@ def install(country=None): {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, - # Warehouse Type - {'doctype': 'Warehouse Type', 'name': 'Transit'}, + {"doctype": "Warehouse Type", "name": "Transit"}, ] from erpnext.setup.setup_wizard.data.industry_type import get_industry_types - records += [{"doctype":"Industry Type", "industry": d} for d in get_industry_types()] - # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] - records += [{'doctype': 'Lead Source', 'source_name': _(d)} for d in default_lead_sources] - records += [{'doctype': 'Sales Partner Type', 'sales_partner_type': _(d)} for d in default_sales_partner_type] + records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] + # records += [{"doctype":"Operation", "operation": d} for d in get_operations()] + records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] + + records += [ + {"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type + ] base_path = frappe.get_app_path("erpnext", "hr", "doctype") - response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html")) + response = frappe.read_file( + os.path.join(base_path, "leave_application/leave_application_email_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response, - 'subject': _("Leave Approval Notification"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Leave Approval Notification"), + "response": response, + "subject": _("Leave Approval Notification"), + "owner": frappe.session.user, + } + ] - records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response, - 'subject': _("Leave Status Notification"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Leave Status Notification"), + "response": response, + "subject": _("Leave Status Notification"), + "owner": frappe.session.user, + } + ] - response = frappe.read_file(os.path.join(base_path, "interview/interview_reminder_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "interview/interview_reminder_notification_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _('Interview Reminder'), 'response': response, - 'subject': _('Interview Reminder'), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Interview Reminder"), + "response": response, + "subject": _("Interview Reminder"), + "owner": frappe.session.user, + } + ] - response = frappe.read_file(os.path.join(base_path, "interview/interview_feedback_reminder_template.html")) + response = frappe.read_file( + os.path.join(base_path, "interview/interview_feedback_reminder_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response, - 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Interview Feedback Reminder"), + "response": response, + "subject": _("Interview Feedback Reminder"), + "owner": frappe.session.user, + } + ] base_path = frappe.get_app_path("erpnext", "stock", "doctype") - response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html")) + response = frappe.read_file( + os.path.join(base_path, "delivery_trip/dispatch_notification_template.html") + ) - records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response, - 'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}] + records += [ + { + "doctype": "Email Template", + "name": _("Dispatch Notification"), + "response": response, + "subject": _("Your order is out for delivery!"), + "owner": frappe.session.user, + } + ] # Records for the Supplier Scorecard from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import make_default_records @@ -294,6 +515,7 @@ def install(country=None): set_more_defaults() update_global_search_doctypes() + def set_more_defaults(): # Do more setup stuff that can be done here with no dependencies update_selling_defaults() @@ -302,6 +524,7 @@ def set_more_defaults(): add_uom_data() update_item_variant_settings() + def update_selling_defaults(): selling_settings = frappe.get_doc("Selling Settings") selling_settings.cust_master_name = "Customer Name" @@ -311,6 +534,7 @@ def update_selling_defaults(): selling_settings.sales_update_frequency = "Each Transaction" selling_settings.save() + def update_buying_defaults(): buying_settings = frappe.get_doc("Buying Settings") buying_settings.supp_master_name = "Supplier Name" @@ -320,6 +544,7 @@ def update_buying_defaults(): buying_settings.allow_multiple_items = 1 buying_settings.save() + def update_hr_defaults(): hr_settings = frappe.get_doc("HR Settings") hr_settings.emp_created_by = "Naming Series" @@ -335,53 +560,66 @@ def update_hr_defaults(): hr_settings.save() + def update_item_variant_settings(): # set no copy fields of an item doctype to item variant settings - doc = frappe.get_doc('Item Variant Settings') + doc = frappe.get_doc("Item Variant Settings") doc.set_default_fields() doc.save() + def add_uom_data(): # add UOMs - uoms = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read()) + uoms = json.loads( + open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_data.json")).read() + ) for d in uoms: - if not frappe.db.exists('UOM', _(d.get("uom_name"))): - uom_doc = frappe.get_doc({ - "doctype": "UOM", - "uom_name": _(d.get("uom_name")), - "name": _(d.get("uom_name")), - "must_be_whole_number": d.get("must_be_whole_number"), - "enabled": 1, - }).db_insert() + if not frappe.db.exists("UOM", _(d.get("uom_name"))): + uom_doc = frappe.get_doc( + { + "doctype": "UOM", + "uom_name": _(d.get("uom_name")), + "name": _(d.get("uom_name")), + "must_be_whole_number": d.get("must_be_whole_number"), + "enabled": 1, + } + ).db_insert() # bootstrap uom conversion factors - uom_conversions = json.loads(open(frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_conversion_data.json")).read()) + uom_conversions = json.loads( + open( + frappe.get_app_path("erpnext", "setup", "setup_wizard", "data", "uom_conversion_data.json") + ).read() + ) for d in uom_conversions: if not frappe.db.exists("UOM Category", _(d.get("category"))): - frappe.get_doc({ - "doctype": "UOM Category", - "category_name": _(d.get("category")) - }).db_insert() + frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() + + if not frappe.db.exists( + "UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} + ): + uom_conversion = frappe.get_doc( + { + "doctype": "UOM Conversion Factor", + "category": _(d.get("category")), + "from_uom": _(d.get("from_uom")), + "to_uom": _(d.get("to_uom")), + "value": d.get("value"), + } + ).insert(ignore_permissions=True) - if not frappe.db.exists("UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))}): - uom_conversion = frappe.get_doc({ - "doctype": "UOM Conversion Factor", - "category": _(d.get("category")), - "from_uom": _(d.get("from_uom")), - "to_uom": _(d.get("to_uom")), - "value": d.get("value") - }).insert(ignore_permissions=True) def add_market_segments(): records = [ # Market Segments {"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")}, - {"doctype": "Market Segment", "market_segment": _("Upper Income")} + {"doctype": "Market Segment", "market_segment": _("Upper Income")}, ] make_records(records) + def add_sale_stages(): # Sale Stages records = [ @@ -392,33 +630,33 @@ def add_sale_stages(): {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")} + {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, ] for sales_stage in records: frappe.get_doc(sales_stage).db_insert() + def install_company(args): records = [ # Fiscal Year { - 'doctype': "Fiscal Year", - 'year': get_fy_details(args.fy_start_date, args.fy_end_date), - 'year_start_date': args.fy_start_date, - 'year_end_date': args.fy_end_date + "doctype": "Fiscal Year", + "year": get_fy_details(args.fy_start_date, args.fy_end_date), + "year_start_date": args.fy_start_date, + "year_end_date": args.fy_end_date, }, - # Company { - "doctype":"Company", - 'company_name': args.company_name, - 'enable_perpetual_inventory': 1, - 'abbr': args.company_abbr, - 'default_currency': args.currency, - 'country': args.country, - 'create_chart_of_accounts_based_on': 'Standard Template', - 'chart_of_accounts': args.chart_of_accounts, - 'domain': args.domain - } + "doctype": "Company", + "company_name": args.company_name, + "enable_perpetual_inventory": 1, + "abbr": args.company_abbr, + "default_currency": args.currency, + "country": args.country, + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": args.chart_of_accounts, + "domain": args.domain, + }, ] make_records(records) @@ -427,20 +665,90 @@ def install_company(args): def install_post_company_fixtures(args=None): records = [ # Department - {'doctype': 'Department', 'department_name': _('All Departments'), 'is_group': 1, 'parent_department': ''}, - {'doctype': 'Department', 'department_name': _('Accounts'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Marketing'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Sales'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Purchase'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Operations'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Production'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Dispatch'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Customer Service'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Human Resources'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Management'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Quality Management'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Research & Development'), 'parent_department': _('All Departments'), 'company': args.company_name}, - {'doctype': 'Department', 'department_name': _('Legal'), 'parent_department': _('All Departments'), 'company': args.company_name}, + { + "doctype": "Department", + "department_name": _("All Departments"), + "is_group": 1, + "parent_department": "", + }, + { + "doctype": "Department", + "department_name": _("Accounts"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Marketing"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Sales"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Purchase"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Operations"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Production"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Dispatch"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Customer Service"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Human Resources"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Management"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Quality Management"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Research & Development"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, + { + "doctype": "Department", + "department_name": _("Legal"), + "parent_department": _("All Departments"), + "company": args.company_name, + }, ] # Make root department with NSM updation @@ -456,8 +764,22 @@ def install_post_company_fixtures(args=None): def install_defaults(args=None): records = [ # Price Lists - { "doctype": "Price List", "price_list_name": _("Standard Buying"), "enabled": 1, "buying": 1, "selling": 0, "currency": args.currency }, - { "doctype": "Price List", "price_list_name": _("Standard Selling"), "enabled": 1, "buying": 0, "selling": 1, "currency": args.currency }, + { + "doctype": "Price List", + "price_list_name": _("Standard Buying"), + "enabled": 1, + "buying": 1, + "selling": 0, + "currency": args.currency, + }, + { + "doctype": "Price List", + "price_list_name": _("Standard Selling"), + "enabled": 1, + "buying": 0, + "selling": 1, + "currency": args.currency, + }, ] make_records(records) @@ -474,27 +796,34 @@ def install_defaults(args=None): args.update({"set_default": 1}) create_bank_account(args) + def set_global_defaults(args): global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") current_fiscal_year = frappe.get_all("Fiscal Year")[0] - global_defaults.update({ - 'current_fiscal_year': current_fiscal_year.name, - 'default_currency': args.get('currency'), - 'default_company':args.get('company_name') , - "country": args.get("country"), - }) + global_defaults.update( + { + "current_fiscal_year": current_fiscal_year.name, + "default_currency": args.get("currency"), + "default_company": args.get("company_name"), + "country": args.get("country"), + } + ) global_defaults.save() + def set_active_domains(args): - frappe.get_single('Domain Settings').set_active_domains(args.get('domains')) + frappe.get_single("Domain Settings").set_active_domains(args.get("domains")) + def update_stock_settings(): stock_settings = frappe.get_doc("Stock Settings") stock_settings.item_naming_by = "Item Code" stock_settings.valuation_method = "FIFO" - stock_settings.default_warehouse = frappe.db.get_value('Warehouse', {'warehouse_name': _('Stores')}) + stock_settings.default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores")} + ) stock_settings.stock_uom = _("Nos") stock_settings.auto_indent = 1 stock_settings.auto_insert_price_list_rate_if_missing = 1 @@ -502,52 +831,65 @@ def update_stock_settings(): stock_settings.set_qty_in_transactions_based_on_serial_no_input = 1 stock_settings.save() + def create_bank_account(args): - if not args.get('bank_account'): + if not args.get("bank_account"): return - company_name = args.get('company_name') - bank_account_group = frappe.db.get_value("Account", - {"account_type": "Bank", "is_group": 1, "root_type": "Asset", - "company": company_name}) + company_name = args.get("company_name") + bank_account_group = frappe.db.get_value( + "Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} + ) if bank_account_group: - bank_account = frappe.get_doc({ - "doctype": "Account", - 'account_name': args.get('bank_account'), - 'parent_account': bank_account_group, - 'is_group':0, - 'company': company_name, - "account_type": "Bank", - }) + bank_account = frappe.get_doc( + { + "doctype": "Account", + "account_name": args.get("bank_account"), + "parent_account": bank_account_group, + "is_group": 0, + "company": company_name, + "account_type": "Bank", + } + ) try: doc = bank_account.insert() - if args.get('set_default'): - frappe.db.set_value("Company", args.get('company_name'), "default_bank_account", bank_account.name, update_modified=False) + if args.get("set_default"): + frappe.db.set_value( + "Company", + args.get("company_name"), + "default_bank_account", + bank_account.name, + update_modified=False, + ) return doc except RootNotEditable: - frappe.throw(_("Bank account cannot be named as {0}").format(args.get('bank_account'))) + frappe.throw(_("Bank account cannot be named as {0}").format(args.get("bank_account"))) except frappe.DuplicateEntryError: # bank account same as a CoA entry pass + def update_shopping_cart_settings(args): shopping_cart = frappe.get_doc("E Commerce Settings") - shopping_cart.update({ - "enabled": 1, - 'company': args.company_name, - 'price_list': frappe.db.get_value("Price List", {"selling": 1}), - 'default_customer_group': _("Individual"), - 'quotation_series': "QTN-", - }) + shopping_cart.update( + { + "enabled": 1, + "company": args.company_name, + "price_list": frappe.db.get_value("Price List", {"selling": 1}), + "default_customer_group": _("Individual"), + "quotation_series": "QTN-", + } + ) shopping_cart.update_single(shopping_cart.get_valid_dict()) + def get_fy_details(fy_start_date, fy_end_date): start_year = getdate(fy_start_date).year if start_year == getdate(fy_end_date).year: fy = cstr(start_year) else: - fy = cstr(start_year) + '-' + cstr(start_year + 1) + fy = cstr(start_year) + "-" + cstr(start_year + 1) return fy diff --git a/erpnext/setup/setup_wizard/operations/sample_data.py b/erpnext/setup/setup_wizard/operations/sample_data.py index 16859947741..7fdd5688f2b 100644 --- a/erpnext/setup/setup_wizard/operations/sample_data.py +++ b/erpnext/setup/setup_wizard/operations/sample_data.py @@ -12,12 +12,12 @@ from frappe import _ from frappe.utils.make_random import add_random_children -def make_sample_data(domains, make_dependent = False): +def make_sample_data(domains, make_dependent=False): """Create a few opportunities, quotes, material requests, issues, todos, projects to help the user get started""" if make_dependent: - items = frappe.get_all("Item", {'is_sales_item': 1}) + items = frappe.get_all("Item", {"is_sales_item": 1}) customers = frappe.get_all("Customer") warehouses = frappe.get_all("Warehouse") @@ -33,91 +33,109 @@ def make_sample_data(domains, make_dependent = False): make_projects(domains) import_notification() -def make_opportunity(items, customer): - b = frappe.get_doc({ - "doctype": "Opportunity", - "opportunity_from": "Customer", - "customer": customer, - "opportunity_type": _("Sales"), - "with_items": 1 - }) - add_random_children(b, "items", rows=len(items), randomize = { - "qty": (1, 5), - "item_code": ["Item"] - }, unique="item_code") +def make_opportunity(items, customer): + b = frappe.get_doc( + { + "doctype": "Opportunity", + "opportunity_from": "Customer", + "customer": customer, + "opportunity_type": _("Sales"), + "with_items": 1, + } + ) + + add_random_children( + b, "items", rows=len(items), randomize={"qty": (1, 5), "item_code": ["Item"]}, unique="item_code" + ) b.insert(ignore_permissions=True) - b.add_comment('Comment', text="This is a dummy record") + b.add_comment("Comment", text="This is a dummy record") + def make_quote(items, customer): - qtn = frappe.get_doc({ - "doctype": "Quotation", - "quotation_to": "Customer", - "party_name": customer, - "order_type": "Sales" - }) + qtn = frappe.get_doc( + { + "doctype": "Quotation", + "quotation_to": "Customer", + "party_name": customer, + "order_type": "Sales", + } + ) - add_random_children(qtn, "items", rows=len(items), randomize = { - "qty": (1, 5), - "item_code": ["Item"] - }, unique="item_code") + add_random_children( + qtn, + "items", + rows=len(items), + randomize={"qty": (1, 5), "item_code": ["Item"]}, + unique="item_code", + ) qtn.insert(ignore_permissions=True) - qtn.add_comment('Comment', text="This is a dummy record") + qtn.add_comment("Comment", text="This is a dummy record") + def make_material_request(items): for i in items: - mr = frappe.get_doc({ - "doctype": "Material Request", - "material_request_type": "Purchase", - "schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7), - "items": [{ + mr = frappe.get_doc( + { + "doctype": "Material Request", + "material_request_type": "Purchase", "schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7), - "item_code": i.name, - "qty": 10 - }] - }) + "items": [ + { + "schedule_date": frappe.utils.add_days(frappe.utils.nowdate(), 7), + "item_code": i.name, + "qty": 10, + } + ], + } + ) mr.insert() mr.submit() - mr.add_comment('Comment', text="This is a dummy record") + mr.add_comment("Comment", text="This is a dummy record") def make_issue(): pass + def make_projects(domains): current_date = frappe.utils.nowdate() - project = frappe.get_doc({ - "doctype": "Project", - "project_name": "ERPNext Implementation", - }) + project = frappe.get_doc( + { + "doctype": "Project", + "project_name": "ERPNext Implementation", + } + ) tasks = [ { "title": "Explore ERPNext", "start_date": current_date, "end_date": current_date, - "file": "explore.md" - }] + "file": "explore.md", + } + ] - if 'Education' in domains: + if "Education" in domains: tasks += [ { "title": _("Setup your Institute in ERPNext"), "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 1), - "file": "education_masters.md" + "file": "education_masters.md", }, { "title": "Setup Master Data", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 1), - "file": "education_masters.md" - }] + "file": "education_masters.md", + }, + ] else: tasks += [ @@ -125,55 +143,59 @@ def make_projects(domains): "title": "Setup Your Company", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 1), - "file": "masters.md" + "file": "masters.md", }, { "title": "Start Tracking your Sales", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 2), - "file": "sales.md" + "file": "sales.md", }, { "title": "Start Managing Purchases", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 3), - "file": "purchase.md" + "file": "purchase.md", }, { "title": "Import Data", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 4), - "file": "import_data.md" + "file": "import_data.md", }, { "title": "Go Live!", "start_date": current_date, "end_date": frappe.utils.add_days(current_date, 5), - "file": "go_live.md" - }] + "file": "go_live.md", + }, + ] for t in tasks: - with open (os.path.join(os.path.dirname(__file__), "tasks", t['file'])) as f: - t['description'] = frappe.utils.md_to_html(f.read()) - del t['file'] + with open(os.path.join(os.path.dirname(__file__), "tasks", t["file"])) as f: + t["description"] = frappe.utils.md_to_html(f.read()) + del t["file"] - project.append('tasks', t) + project.append("tasks", t) project.insert(ignore_permissions=True) + def import_notification(): - '''Import notification for task start''' - with open (os.path.join(os.path.dirname(__file__), "tasks/task_alert.json")) as f: + """Import notification for task start""" + with open(os.path.join(os.path.dirname(__file__), "tasks/task_alert.json")) as f: notification = frappe.get_doc(json.loads(f.read())[0]) notification.insert() # trigger the first message! from frappe.email.doctype.notification.notification import trigger_daily_alerts + trigger_daily_alerts() + def test_sample(): - frappe.db.sql('delete from `tabNotification`') - frappe.db.sql('delete from tabProject') - frappe.db.sql('delete from tabTask') - make_projects('Education') + frappe.db.sql("delete from `tabNotification`") + frappe.db.sql("delete from tabProject") + frappe.db.sql("delete from tabTask") + make_projects("Education") import_notification() diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index b126cc9e6a7..686a0109589 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -10,11 +10,11 @@ from frappe import _ def setup_taxes_and_charges(company_name: str, country: str): - if not frappe.db.exists('Company', company_name): - frappe.throw(_('Company {} does not exist yet. Taxes setup aborted.').format(company_name)) + if not frappe.db.exists("Company", company_name): + frappe.throw(_("Company {} does not exist yet. Taxes setup aborted.").format(company_name)) - file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json') - with open(file_path, 'r') as json_file: + file_path = os.path.join(os.path.dirname(__file__), "..", "data", "country_wise_tax.json") + with open(file_path, "r") as json_file: tax_data = json.load(json_file) country_wise_tax = tax_data.get(country) @@ -22,7 +22,7 @@ def setup_taxes_and_charges(company_name: str, country: str): if not country_wise_tax: return - if 'chart_of_accounts' not in country_wise_tax: + if "chart_of_accounts" not in country_wise_tax: country_wise_tax = simple_to_detailed(country_wise_tax) from_detailed_data(company_name, country_wise_tax) @@ -36,39 +36,44 @@ def simple_to_detailed(templates): Example input: { - "France VAT 20%": { - "account_name": "VAT 20%", - "tax_rate": 20, - "default": 1 - }, - "France VAT 10%": { - "account_name": "VAT 10%", - "tax_rate": 10 - } + "France VAT 20%": { + "account_name": "VAT 20%", + "tax_rate": 20, + "default": 1 + }, + "France VAT 10%": { + "account_name": "VAT 10%", + "tax_rate": 10 + } } """ return { - 'chart_of_accounts': { - '*': { - 'item_tax_templates': [{ - 'title': title, - 'taxes': [{ - 'tax_type': { - 'account_name': data.get('account_name'), - 'tax_rate': data.get('tax_rate') - } - }] - } for title, data in templates.items()], - '*': [{ - 'title': title, - 'is_default': data.get('default', 0), - 'taxes': [{ - 'account_head': { - 'account_name': data.get('account_name'), - 'tax_rate': data.get('tax_rate') - } - }] - } for title, data in templates.items()] + "chart_of_accounts": { + "*": { + "item_tax_templates": [ + { + "title": title, + "taxes": [ + {"tax_type": {"account_name": data.get("account_name"), "tax_rate": data.get("tax_rate")}} + ], + } + for title, data in templates.items() + ], + "*": [ + { + "title": title, + "is_default": data.get("default", 0), + "taxes": [ + { + "account_head": { + "account_name": data.get("account_name"), + "tax_rate": data.get("tax_rate"), + } + } + ], + } + for title, data in templates.items() + ], } } } @@ -76,13 +81,13 @@ def simple_to_detailed(templates): def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" - coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts') - coa_data = data.get('chart_of_accounts', {}) - tax_templates = coa_data.get(coa_name) or coa_data.get('*', {}) - tax_categories = data.get('tax_categories') - sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*', {}) - purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*', {}) - item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*', {}) + coa_name = frappe.db.get_value("Company", company_name, "chart_of_accounts") + coa_data = data.get("chart_of_accounts", {}) + tax_templates = coa_data.get(coa_name) or coa_data.get("*", {}) + tax_categories = data.get("tax_categories") + sales_tax_templates = tax_templates.get("sales_tax_templates") or tax_templates.get("*", {}) + purchase_tax_templates = tax_templates.get("purchase_tax_templates") or tax_templates.get("*", {}) + item_tax_templates = tax_templates.get("item_tax_templates") or tax_templates.get("*", {}) if tax_categories: for tax_category in tax_categories: @@ -90,11 +95,11 @@ def from_detailed_data(company_name, data): if sales_tax_templates: for template in sales_tax_templates: - make_taxes_and_charges_template(company_name, 'Sales Taxes and Charges Template', template) + make_taxes_and_charges_template(company_name, "Sales Taxes and Charges Template", template) if purchase_tax_templates: for template in purchase_tax_templates: - make_taxes_and_charges_template(company_name, 'Purchase Taxes and Charges Template', template) + make_taxes_and_charges_template(company_name, "Purchase Taxes and Charges Template", template) if item_tax_templates: for template in item_tax_templates: @@ -102,40 +107,45 @@ def from_detailed_data(company_name, data): def update_regional_tax_settings(country, company): - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) + path = frappe.get_app_path("erpnext", "regional", frappe.scrub(country)) if os.path.exists(path.encode("utf-8")): try: - module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format(frappe.scrub(country)) + module_name = "erpnext.regional.{0}.setup.update_regional_tax_settings".format( + frappe.scrub(country) + ) frappe.get_attr(module_name)(country, company) except Exception as e: # Log error and ignore if failed to setup regional tax settings frappe.log_error() pass -def make_taxes_and_charges_template(company_name, doctype, template): - template['company'] = company_name - template['doctype'] = doctype - if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}): +def make_taxes_and_charges_template(company_name, doctype, template): + template["company"] = company_name + template["doctype"] = doctype + + if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return - for tax_row in template.get('taxes'): - account_data = tax_row.get('account_head') + for tax_row in template.get("taxes"): + account_data = tax_row.get("account_head") tax_row_defaults = { - 'category': 'Total', - 'charge_type': 'On Net Total', - 'cost_center': frappe.db.get_value('Company', company_name, 'cost_center') + "category": "Total", + "charge_type": "On Net Total", + "cost_center": frappe.db.get_value("Company", company_name, "cost_center"), } - if doctype == 'Purchase Taxes and Charges Template': - tax_row_defaults['add_deduct_tax'] = 'Add' + if doctype == "Purchase Taxes and Charges Template": + tax_row_defaults["add_deduct_tax"] = "Add" # if account_head is a dict, search or create the account and get it's name if isinstance(account_data, dict): - tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate')) - tax_row_defaults['rate'] = account_data.get('tax_rate') + tax_row_defaults["description"] = "{0} @ {1}".format( + account_data.get("account_name"), account_data.get("tax_rate") + ) + tax_row_defaults["rate"] = account_data.get("tax_rate") account = get_or_create_account(company_name, account_data) - tax_row['account_head'] = account.name + tax_row["account_head"] = account.name # use the default value if nothing other is specified for fieldname, default_value in tax_row_defaults.items(): @@ -151,28 +161,29 @@ def make_taxes_and_charges_template(company_name, doctype, template): doc.insert(ignore_permissions=True) return doc + def make_item_tax_template(company_name, template): """Create an Item Tax Template. This requires a separate method because Item Tax Template is structured differently from Sales and Purchase Tax Templates. """ - doctype = 'Item Tax Template' - template['company'] = company_name - template['doctype'] = doctype + doctype = "Item Tax Template" + template["company"] = company_name + template["doctype"] = doctype - if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}): + if frappe.db.exists(doctype, {"title": template.get("title"), "company": company_name}): return - for tax_row in template.get('taxes'): - account_data = tax_row.get('tax_type') + for tax_row in template.get("taxes"): + account_data = tax_row.get("tax_type") # if tax_type is a dict, search or create the account and get it's name if isinstance(account_data, dict): account = get_or_create_account(company_name, account_data) - tax_row['tax_type'] = account.name - if 'tax_rate' not in tax_row: - tax_row['tax_rate'] = account_data.get('tax_rate') + tax_row["tax_type"] = account.name + if "tax_rate" not in tax_row: + tax_row["tax_rate"] = account_data.get("tax_rate") doc = frappe.get_doc(template) @@ -183,46 +194,47 @@ def make_item_tax_template(company_name, template): doc.insert(ignore_permissions=True) return doc + def make_tax_category(tax_category): - """ Make tax category based on title if not already created """ - doctype = 'Tax Category' - if not frappe.db.exists(doctype, tax_category['title']): - tax_category['doctype'] = doctype + """Make tax category based on title if not already created""" + doctype = "Tax Category" + if not frappe.db.exists(doctype, tax_category["title"]): + tax_category["doctype"] = doctype doc = frappe.get_doc(tax_category) doc.flags.ignore_links = True doc.flags.ignore_validate = True doc.insert(ignore_permissions=True) + def get_or_create_account(company_name, account): """ Check if account already exists. If not, create it. Return a tax account or None. """ - default_root_type = 'Liability' - root_type = account.get('root_type', default_root_type) + default_root_type = "Liability" + root_type = account.get("root_type", default_root_type) - existing_accounts = frappe.get_all('Account', - filters={ - 'company': company_name, - 'root_type': root_type - }, + existing_accounts = frappe.get_all( + "Account", + filters={"company": company_name, "root_type": root_type}, or_filters={ - 'account_name': account.get('account_name'), - 'account_number': account.get('account_number') - }) + "account_name": account.get("account_name"), + "account_number": account.get("account_number"), + }, + ) if existing_accounts: - return frappe.get_doc('Account', existing_accounts[0].name) + return frappe.get_doc("Account", existing_accounts[0].name) tax_group = get_or_create_tax_group(company_name, root_type) - account['doctype'] = 'Account' - account['company'] = company_name - account['parent_account'] = tax_group - account['report_type'] = 'Balance Sheet' - account['account_type'] = 'Tax' - account['root_type'] = root_type - account['is_group'] = 0 + account["doctype"] = "Account" + account["company"] = company_name + account["parent_account"] = tax_group + account["report_type"] = "Balance Sheet" + account["account_type"] = "Tax" + account["root_type"] = root_type + account["is_group"] = 0 doc = frappe.get_doc(account) doc.flags.ignore_links = True @@ -230,50 +242,53 @@ def get_or_create_account(company_name, account): doc.insert(ignore_permissions=True, ignore_mandatory=True) return doc + def get_or_create_tax_group(company_name, root_type): # Look for a group account of type 'Tax' - tax_group_name = frappe.db.get_value('Account', { - 'is_group': 1, - 'root_type': root_type, - 'account_type': 'Tax', - 'company': company_name - }) + tax_group_name = frappe.db.get_value( + "Account", + {"is_group": 1, "root_type": root_type, "account_type": "Tax", "company": company_name}, + ) if tax_group_name: return tax_group_name # Look for a group account named 'Duties and Taxes' or 'Tax Assets' - account_name = _('Duties and Taxes') if root_type == 'Liability' else _('Tax Assets') - tax_group_name = frappe.db.get_value('Account', { - 'is_group': 1, - 'root_type': root_type, - 'account_name': account_name, - 'company': company_name - }) + account_name = _("Duties and Taxes") if root_type == "Liability" else _("Tax Assets") + tax_group_name = frappe.db.get_value( + "Account", + {"is_group": 1, "root_type": root_type, "account_name": account_name, "company": company_name}, + ) if tax_group_name: return tax_group_name # Create a new group account named 'Duties and Taxes' or 'Tax Assets' just # below the root account - root_account = frappe.get_all('Account', { - 'is_group': 1, - 'root_type': root_type, - 'company': company_name, - 'report_type': 'Balance Sheet', - 'parent_account': ('is', 'not set') - }, limit=1)[0] + root_account = frappe.get_all( + "Account", + { + "is_group": 1, + "root_type": root_type, + "company": company_name, + "report_type": "Balance Sheet", + "parent_account": ("is", "not set"), + }, + limit=1, + )[0] - tax_group_account = frappe.get_doc({ - 'doctype': 'Account', - 'company': company_name, - 'is_group': 1, - 'report_type': 'Balance Sheet', - 'root_type': root_type, - 'account_type': 'Tax', - 'account_name': account_name, - 'parent_account': root_account.name - }) + tax_group_account = frappe.get_doc( + { + "doctype": "Account", + "company": company_name, + "is_group": 1, + "report_type": "Balance Sheet", + "root_type": root_type, + "account_type": "Tax", + "account_name": account_name, + "parent_account": root_account.name, + } + ) tax_group_account.flags.ignore_links = True tax_group_account.flags.ignore_validate = True @@ -285,11 +300,11 @@ def get_or_create_tax_group(company_name, root_type): def make_tax_catgory(tax_category): - doctype = 'Tax Category' + doctype = "Tax Category" if isinstance(tax_category, str): - tax_category = {'title': tax_category} + tax_category = {"title": tax_category} - tax_category['doctype'] = doctype - if not frappe.db.exists(doctype, tax_category['title']): + tax_category["doctype"] = doctype + if not frappe.db.exists(doctype, tax_category["title"]): doc = frappe.get_doc(tax_category) doc.insert(ignore_permissions=True) diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index c9ed184e04e..f588ae2fd02 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -14,96 +14,66 @@ def get_setup_stages(args=None): if frappe.db.sql("select name from tabCompany"): stages = [ { - 'status': _('Wrapping up'), - 'fail_msg': _('Failed to login'), - 'tasks': [ - { - 'fn': fin, - 'args': args, - 'fail_msg': _("Failed to login") - } - ] + "status": _("Wrapping up"), + "fail_msg": _("Failed to login"), + "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], } ] else: stages = [ { - 'status': _('Installing presets'), - 'fail_msg': _('Failed to install presets'), - 'tasks': [ - { - 'fn': stage_fixtures, - 'args': args, - 'fail_msg': _("Failed to install presets") - } - ] + "status": _("Installing presets"), + "fail_msg": _("Failed to install presets"), + "tasks": [{"fn": stage_fixtures, "args": args, "fail_msg": _("Failed to install presets")}], }, { - 'status': _('Setting up company'), - 'fail_msg': _('Failed to setup company'), - 'tasks': [ - { - 'fn': setup_company, - 'args': args, - 'fail_msg': _("Failed to setup company") - } - ] + "status": _("Setting up company"), + "fail_msg": _("Failed to setup company"), + "tasks": [{"fn": setup_company, "args": args, "fail_msg": _("Failed to setup company")}], }, { - 'status': _('Setting defaults'), - 'fail_msg': 'Failed to set defaults', - 'tasks': [ - { - 'fn': setup_defaults, - 'args': args, - 'fail_msg': _("Failed to setup defaults") - }, - { - 'fn': stage_four, - 'args': args, - 'fail_msg': _("Failed to create website") - }, - { - 'fn': set_active_domains, - 'args': args, - 'fail_msg': _("Failed to add Domain") - }, - ] + "status": _("Setting defaults"), + "fail_msg": "Failed to set defaults", + "tasks": [ + {"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")}, + {"fn": stage_four, "args": args, "fail_msg": _("Failed to create website")}, + {"fn": set_active_domains, "args": args, "fail_msg": _("Failed to add Domain")}, + ], }, { - 'status': _('Wrapping up'), - 'fail_msg': _('Failed to login'), - 'tasks': [ - { - 'fn': fin, - 'args': args, - 'fail_msg': _("Failed to login") - } - ] - } + "status": _("Wrapping up"), + "fail_msg": _("Failed to login"), + "tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}], + }, ] return stages + def stage_fixtures(args): - fixtures.install(args.get('country')) + fixtures.install(args.get("country")) + def setup_company(args): fixtures.install_company(args) + def setup_defaults(args): fixtures.install_defaults(frappe._dict(args)) + def stage_four(args): company_setup.create_website(args) company_setup.create_email_digest() company_setup.create_logo(args) + def fin(args): frappe.local.message_log = [] login_as_first_user(args) - make_sample_data(args.get('domains')) + make_sample_data(args.get("domains")) + def make_sample_data(domains): try: @@ -114,6 +84,7 @@ def make_sample_data(domains): frappe.message_log.pop() pass + def login_as_first_user(args): if args.get("email") and hasattr(frappe.local, "login_manager"): frappe.local.login_manager.login_as(args.get("email")) @@ -127,6 +98,7 @@ def setup_complete(args=None): stage_four(args) fin(args) + def set_active_domains(args): - domain_settings = frappe.get_single('Domain Settings') - domain_settings.set_active_domains(args.get('domains')) + domain_settings = frappe.get_single("Domain Settings") + domain_settings.set_active_domains(args.get("domains")) diff --git a/erpnext/setup/setup_wizard/utils.py b/erpnext/setup/setup_wizard/utils.py index afab7e712bb..53f80d68b18 100644 --- a/erpnext/setup/setup_wizard/utils.py +++ b/erpnext/setup/setup_wizard/utils.py @@ -1,4 +1,3 @@ - import json import os @@ -6,8 +5,7 @@ from frappe.desk.page.setup_wizard.setup_wizard import setup_complete def complete(): - with open(os.path.join(os.path.dirname(__file__), - 'data', 'test_mfg.json'), 'r') as f: + with open(os.path.join(os.path.dirname(__file__), "data", "test_mfg.json"), "r") as f: data = json.loads(f.read()) setup_complete(data) diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 93b6e8d3af3..5a019c68c9d 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -11,40 +11,51 @@ from erpnext import get_default_company def get_root_of(doctype): """Get root element of a DocType with a tree structure""" - result = frappe.db.sql_list("""select name from `tab%s` - where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" % - (doctype, doctype)) + result = frappe.db.sql_list( + """select name from `tab%s` + where lft=1 and rgt=(select max(rgt) from `tab%s` where docstatus < 2)""" + % (doctype, doctype) + ) return result[0] if result else None + def get_ancestors_of(doctype, name): """Get ancestor elements of a DocType with a tree structure""" lft, rgt = frappe.db.get_value(doctype, name, ["lft", "rgt"]) - result = frappe.db.sql_list("""select name from `tab%s` - where lft<%s and rgt>%s order by lft desc""" % (doctype, "%s", "%s"), (lft, rgt)) + result = frappe.db.sql_list( + """select name from `tab%s` + where lft<%s and rgt>%s order by lft desc""" + % (doctype, "%s", "%s"), + (lft, rgt), + ) return result or [] + def before_tests(): frappe.clear_cache() # complete setup if missing from frappe.desk.page.setup_wizard.setup_wizard import setup_complete + if not frappe.get_list("Company"): - setup_complete({ - "currency" :"USD", - "full_name" :"Test User", - "company_name" :"Wind Power LLC", - "timezone" :"America/New_York", - "company_abbr" :"WP", - "industry" :"Manufacturing", - "country" :"United States", - "fy_start_date" :"2011-01-01", - "fy_end_date" :"2011-12-31", - "language" :"english", - "company_tagline" :"Testing", - "email" :"test@erpnext.com", - "password" :"test", - "chart_of_accounts" : "Standard", - "domains" : ["Manufacturing"], - }) + setup_complete( + { + "currency": "USD", + "full_name": "Test User", + "company_name": "Wind Power LLC", + "timezone": "America/New_York", + "company_abbr": "WP", + "industry": "Manufacturing", + "country": "United States", + "fy_start_date": "2011-01-01", + "fy_end_date": "2011-12-31", + "language": "english", + "company_tagline": "Testing", + "email": "test@erpnext.com", + "password": "test", + "chart_of_accounts": "Standard", + "domains": ["Manufacturing"], + } + ) frappe.db.sql("delete from `tabLeave Allocation`") frappe.db.sql("delete from `tabLeave Application`") @@ -57,6 +68,7 @@ def before_tests(): frappe.db.commit() + @frappe.whitelist() def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=None): if not (from_currency and to_currency): @@ -73,7 +85,7 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No filters = [ ["date", "<=", get_datetime_str(transaction_date)], ["from_currency", "=", from_currency], - ["to_currency", "=", to_currency] + ["to_currency", "=", to_currency], ] if args == "for_buying": @@ -88,8 +100,8 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No # cksgb 19/09/2016: get last entry in Currency Exchange with from_currency and to_currency. entries = frappe.get_all( - "Currency Exchange", fields=["exchange_rate"], filters=filters, order_by="date desc", - limit=1) + "Currency Exchange", fields=["exchange_rate"], filters=filters, order_by="date desc", limit=1 + ) if entries: return flt(entries[0].exchange_rate) @@ -100,12 +112,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if not value: import requests + api_url = "https://api.exchangerate.host/convert" - response = requests.get(api_url, params={ - "date": transaction_date, - "from": from_currency, - "to": to_currency - }) + response = requests.get( + api_url, params={"date": transaction_date, "from": from_currency, "to": to_currency} + ) # expire in 6 hours response.raise_for_status() value = response.json()["result"] @@ -113,20 +124,26 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No return flt(value) except Exception: frappe.log_error(title="Get Exchange Rate") - frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date)) + frappe.msgprint( + _( + "Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually" + ).format(from_currency, to_currency, transaction_date) + ) return 0.0 + def enable_all_roles_and_domains(): - """ enable all roles and domain for testing """ + """enable all roles and domain for testing""" # add all roles to users domains = frappe.get_all("Domain") if not domains: return from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to - frappe.get_single('Domain Settings').set_active_domains(\ - [d.name for d in domains]) - add_all_roles_to('Administrator') + + frappe.get_single("Domain Settings").set_active_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 @@ -145,12 +162,13 @@ def insert_record(records): doc.insert(ignore_permissions=True) except frappe.DuplicateEntryError as e: # pass DuplicateEntryError and continue - if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: + if e.args and e.args[0] == doc.doctype and e.args[1] == doc.name: # make sure DuplicateEntryError is for the exact same doc and not a related doc pass else: raise + def welcome_email(): site_name = get_default_company() or "ERPNext" title = _("Welcome to {0}").format(site_name) diff --git a/erpnext/startup/__init__.py b/erpnext/startup/__init__.py index d1933d23969..489e24499ca 100644 --- a/erpnext/startup/__init__.py +++ b/erpnext/startup/__init__.py @@ -1,4 +1,3 @@ - # ERPNext - web based ERP (http://erpnext.com) # Copyright (C) 2012 Frappe Technologies Pvt Ltd # @@ -18,7 +17,4 @@ # default settings that can be made for a user. product_name = "ERPNext" -user_defaults = { - "Company": "company", - "Territory": "territory" -} +user_defaults = {"Company": "company", "Territory": "territory"} diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index ed8c878ad4a..52bd979e884 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt" - import frappe from frappe.utils import cint @@ -10,69 +9,72 @@ from frappe.utils import cint def boot_session(bootinfo): """boot session - send website info if guest""" - bootinfo.custom_css = frappe.db.get_value('Style Settings', None, 'custom_css') or '' + bootinfo.custom_css = frappe.db.get_value("Style Settings", None, "custom_css") or "" - if frappe.session['user']!='Guest': + if frappe.session["user"] != "Guest": update_page_info(bootinfo) load_country_and_currency(bootinfo) - bootinfo.sysdefaults.territory = frappe.db.get_single_value('Selling Settings', - 'territory') - bootinfo.sysdefaults.customer_group = frappe.db.get_single_value('Selling Settings', - 'customer_group') - bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings', - 'allow_stale')) - bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings', - 'default_valid_till')) + bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory") + bootinfo.sysdefaults.customer_group = frappe.db.get_single_value( + "Selling Settings", "customer_group" + ) + bootinfo.sysdefaults.allow_stale = cint( + frappe.db.get_single_value("Accounts Settings", "allow_stale") + ) + bootinfo.sysdefaults.quotation_valid_till = cint( + frappe.db.get_single_value("Selling Settings", "default_valid_till") + ) # if no company, show a dialog box to create a new company bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] if not bootinfo.customer_count: - bootinfo.setup_complete = frappe.db.sql("""SELECT `name` + bootinfo.setup_complete = ( + frappe.db.sql( + """SELECT `name` FROM `tabCompany` - LIMIT 1""") and 'Yes' or 'No' + LIMIT 1""" + ) + and "Yes" + or "No" + ) - bootinfo.docs += frappe.db.sql("""select name, default_currency, cost_center, default_selling_terms, default_buying_terms, + bootinfo.docs += frappe.db.sql( + """select name, default_currency, cost_center, default_selling_terms, default_buying_terms, default_letter_head, default_bank_account, enable_perpetual_inventory, country from `tabCompany`""", - as_dict=1, update={"doctype":":Company"}) + as_dict=1, + update={"doctype": ":Company"}, + ) - party_account_types = frappe.db.sql(""" select name, ifnull(account_type, '') from `tabParty Type`""") + party_account_types = frappe.db.sql( + """ select name, ifnull(account_type, '') from `tabParty Type`""" + ) bootinfo.party_account_types = frappe._dict(party_account_types) + def load_country_and_currency(bootinfo): country = frappe.db.get_default("country") if country and frappe.db.exists("Country", country): bootinfo.docs += [frappe.get_doc("Country", country)] - bootinfo.docs += frappe.db.sql("""select name, fraction, fraction_units, + bootinfo.docs += frappe.db.sql( + """select name, fraction, fraction_units, number_format, smallest_currency_fraction_value, symbol from tabCurrency - where enabled=1""", as_dict=1, update={"doctype":":Currency"}) + where enabled=1""", + as_dict=1, + update={"doctype": ":Currency"}, + ) + def update_page_info(bootinfo): - bootinfo.page_info.update({ - "Chart of Accounts": { - "title": "Chart of Accounts", - "route": "Tree/Account" - }, - "Chart of Cost Centers": { - "title": "Chart of Cost Centers", - "route": "Tree/Cost Center" - }, - "Item Group Tree": { - "title": "Item Group Tree", - "route": "Tree/Item Group" - }, - "Customer Group Tree": { - "title": "Customer Group Tree", - "route": "Tree/Customer Group" - }, - "Territory Tree": { - "title": "Territory Tree", - "route": "Tree/Territory" - }, - "Sales Person Tree": { - "title": "Sales Person Tree", - "route": "Tree/Sales Person" + bootinfo.page_info.update( + { + "Chart of Accounts": {"title": "Chart of Accounts", "route": "Tree/Account"}, + "Chart of Cost Centers": {"title": "Chart of Cost Centers", "route": "Tree/Cost Center"}, + "Item Group Tree": {"title": "Item Group Tree", "route": "Tree/Item Group"}, + "Customer Group Tree": {"title": "Customer Group Tree", "route": "Tree/Customer Group"}, + "Territory Tree": {"title": "Territory Tree", "route": "Tree/Territory"}, + "Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"}, } - }) + ) diff --git a/erpnext/startup/filters.py b/erpnext/startup/filters.py index c0ccf54d5f6..4fd64312f56 100644 --- a/erpnext/startup/filters.py +++ b/erpnext/startup/filters.py @@ -1,6 +1,3 @@ - - - def get_filters_config(): filters_config = { "fiscal year": { diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index a92abf11130..da7edbf8144 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -1,5 +1,3 @@ - - import frappe from frappe.utils import cint @@ -8,66 +6,66 @@ def get_leaderboards(): leaderboards = { "Customer": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - 'total_qty_sold', - {'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + "total_qty_sold", + {"fieldname": "outstanding_amount", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_customers", - "icon": "customer" + "icon": "customer", }, "Item": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - 'total_qty_sold', - {'fieldname': 'total_purchase_amount', 'fieldtype': 'Currency'}, - 'total_qty_purchased', - 'available_stock_qty', - {'fieldname': 'available_stock_value', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + "total_qty_sold", + {"fieldname": "total_purchase_amount", "fieldtype": "Currency"}, + "total_qty_purchased", + "available_stock_qty", + {"fieldname": "available_stock_value", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_items", - "icon": "stock" + "icon": "stock", }, "Supplier": { "fields": [ - {'fieldname': 'total_purchase_amount', 'fieldtype': 'Currency'}, - 'total_qty_purchased', - {'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'} + {"fieldname": "total_purchase_amount", "fieldtype": "Currency"}, + "total_qty_purchased", + {"fieldname": "outstanding_amount", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_suppliers", - "icon": "buying" + "icon": "buying", }, "Sales Partner": { "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}, - {'fieldname': 'total_commission', 'fieldtype': 'Currency'} + {"fieldname": "total_sales_amount", "fieldtype": "Currency"}, + {"fieldname": "total_commission", "fieldtype": "Currency"}, ], "method": "erpnext.startup.leaderboard.get_all_sales_partner", - "icon": "hr" + "icon": "hr", }, "Sales Person": { - "fields": [ - {'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'} - ], + "fields": [{"fieldname": "total_sales_amount", "fieldtype": "Currency"}], "method": "erpnext.startup.leaderboard.get_all_sales_person", - "icon": "customer" - } + "icon": "customer", + }, } return leaderboards + @frappe.whitelist() -def get_all_customers(date_range, company, field, limit = None): +def get_all_customers(date_range, company, field, limit=None): if field == "outstanding_amount": - filters = [['docstatus', '=', '1'], ['company', '=', company]] + filters = [["docstatus", "=", "1"], ["company", "=", company]] if date_range: date_range = frappe.parse_json(date_range) - filters.append(['posting_date', '>=', 'between', [date_range[0], date_range[1]]]) - return frappe.db.get_all('Sales Invoice', - fields = ['customer as name', 'sum(outstanding_amount) as value'], - filters = filters, - group_by = 'customer', - order_by = 'value desc', - limit = limit + filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]]) + return frappe.db.get_all( + "Sales Invoice", + fields=["customer as name", "sum(outstanding_amount) as value"], + filters=filters, + group_by="customer", + order_by="value desc", + limit=limit, ) else: if field == "total_sales_amount": @@ -75,9 +73,10 @@ def get_all_customers(date_range, company, field, limit = None): elif field == "total_qty_sold": select_field = "sum(so_item.stock_qty)" - date_condition = get_date_condition(date_range, 'so.transaction_date') + date_condition = get_date_condition(date_range, "so.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select so.customer as name, {0} as value FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item ON so.name = so_item.parent @@ -85,17 +84,24 @@ def get_all_customers(date_range, company, field, limit = None): group by so.customer order by value DESC limit %s - """.format(select_field, date_condition), (company, cint(limit)), as_dict=1) + """.format( + select_field, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) + @frappe.whitelist() -def get_all_items(date_range, company, field, limit = None): +def get_all_items(date_range, company, field, limit=None): if field in ("available_stock_qty", "available_stock_value"): - select_field = "sum(actual_qty)" if field=="available_stock_qty" else "sum(stock_value)" - return frappe.db.get_all('Bin', - fields = ['item_code as name', '{0} as value'.format(select_field)], - group_by = 'item_code', - order_by = 'value desc', - limit = limit + select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)" + return frappe.db.get_all( + "Bin", + fields=["item_code as name", "{0} as value".format(select_field)], + group_by="item_code", + order_by="value desc", + limit=limit, ) else: if field == "total_sales_amount": @@ -111,9 +117,10 @@ def get_all_items(date_range, company, field, limit = None): select_field = "sum(order_item.stock_qty)" select_doctype = "Purchase Order" - date_condition = get_date_condition(date_range, 'sales_order.transaction_date') + date_condition = get_date_condition(date_range, "sales_order.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select order_item.item_code as name, {0} as value from `tab{1}` sales_order join `tab{1} Item` as order_item on sales_order.name = order_item.parent @@ -122,21 +129,28 @@ def get_all_items(date_range, company, field, limit = None): group by order_item.item_code order by value desc limit %s - """.format(select_field, select_doctype, date_condition), (company, cint(limit)), as_dict=1) #nosec + """.format( + select_field, select_doctype, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) # nosec + @frappe.whitelist() -def get_all_suppliers(date_range, company, field, limit = None): +def get_all_suppliers(date_range, company, field, limit=None): if field == "outstanding_amount": - filters = [['docstatus', '=', '1'], ['company', '=', company]] + filters = [["docstatus", "=", "1"], ["company", "=", company]] if date_range: date_range = frappe.parse_json(date_range) - filters.append(['posting_date', 'between', [date_range[0], date_range[1]]]) - return frappe.db.get_all('Purchase Invoice', - fields = ['supplier as name', 'sum(outstanding_amount) as value'], - filters = filters, - group_by = 'supplier', - order_by = 'value desc', - limit = limit + filters.append(["posting_date", "between", [date_range[0], date_range[1]]]) + return frappe.db.get_all( + "Purchase Invoice", + fields=["supplier as name", "sum(outstanding_amount) as value"], + filters=filters, + group_by="supplier", + order_by="value desc", + limit=limit, ) else: if field == "total_purchase_amount": @@ -144,9 +158,10 @@ def get_all_suppliers(date_range, company, field, limit = None): elif field == "total_qty_purchased": select_field = "sum(purchase_order_item.stock_qty)" - date_condition = get_date_condition(date_range, 'purchase_order.modified') + date_condition = get_date_condition(date_range, "purchase_order.modified") - return frappe.db.sql(""" + return frappe.db.sql( + """ select purchase_order.supplier as name, {0} as value FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item` as purchase_order_item ON purchase_order.name = purchase_order_item.parent @@ -156,34 +171,45 @@ def get_all_suppliers(date_range, company, field, limit = None): and purchase_order.company = %s group by purchase_order.supplier order by value DESC - limit %s""".format(select_field, date_condition), (company, cint(limit)), as_dict=1) #nosec + limit %s""".format( + select_field, date_condition + ), + (company, cint(limit)), + as_dict=1, + ) # nosec + @frappe.whitelist() -def get_all_sales_partner(date_range, company, field, limit = None): +def get_all_sales_partner(date_range, company, field, limit=None): if field == "total_sales_amount": select_field = "sum(`base_net_total`)" elif field == "total_commission": select_field = "sum(`total_commission`)" - filters = { - 'sales_partner': ['!=', ''], - 'docstatus': 1, - 'company': company - } + filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company} if date_range: date_range = frappe.parse_json(date_range) - filters['transaction_date'] = ['between', [date_range[0], date_range[1]]] + filters["transaction_date"] = ["between", [date_range[0], date_range[1]]] + + return frappe.get_list( + "Sales Order", + fields=[ + "`sales_partner` as name", + "{} as value".format(select_field), + ], + filters=filters, + group_by="sales_partner", + order_by="value DESC", + limit=limit, + ) - return frappe.get_list('Sales Order', fields=[ - '`sales_partner` as name', - '{} as value'.format(select_field), - ], filters=filters, group_by='sales_partner', order_by='value DESC', limit=limit) @frappe.whitelist() -def get_all_sales_person(date_range, company, field = None, limit = 0): - date_condition = get_date_condition(date_range, 'sales_order.transaction_date') +def get_all_sales_person(date_range, company, field=None, limit=0): + date_condition = get_date_condition(date_range, "sales_order.transaction_date") - return frappe.db.sql(""" + return frappe.db.sql( + """ select sales_team.sales_person as name, sum(sales_order.base_net_total) as value from `tabSales Order` as sales_order join `tabSales Team` as sales_team on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order' @@ -193,10 +219,16 @@ def get_all_sales_person(date_range, company, field = None, limit = 0): group by sales_team.sales_person order by value DESC limit %s - """.format(date_condition=date_condition), (company, cint(limit)), as_dict=1) + """.format( + date_condition=date_condition + ), + (company, cint(limit)), + as_dict=1, + ) + def get_date_condition(date_range, field): - date_condition = '' + date_condition = "" if date_range: date_range = frappe.parse_json(date_range) from_date, to_date = date_range diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 0965ead57c6..76cb91a4634 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -6,8 +6,8 @@ import frappe def get_notification_config(): - notifications = { "for_doctype": - { + notifications = { + "for_doctype": { "Issue": {"status": "Open"}, "Warranty Claim": {"status": "Open"}, "Task": {"status": ("in", ("Open", "Overdue"))}, @@ -16,66 +16,46 @@ def get_notification_config(): "Contact": {"status": "Open"}, "Opportunity": {"status": "Open"}, "Quotation": {"docstatus": 0}, - "Sales Order": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, + "Sales Order": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, "Journal Entry": {"docstatus": 0}, - "Sales Invoice": { - "outstanding_amount": (">", 0), - "docstatus": ("<", 2) - }, - "Purchase Invoice": { - "outstanding_amount": (">", 0), - "docstatus": ("<", 2) - }, + "Sales Invoice": {"outstanding_amount": (">", 0), "docstatus": ("<", 2)}, + "Purchase Invoice": {"outstanding_amount": (">", 0), "docstatus": ("<", 2)}, "Payment Entry": {"docstatus": 0}, "Leave Application": {"docstatus": 0}, "Expense Claim": {"docstatus": 0}, "Job Applicant": {"status": "Open"}, - "Delivery Note": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, + "Delivery Note": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, "Stock Entry": {"docstatus": 0}, "Material Request": { "docstatus": ("<", 2), "status": ("not in", ("Stopped",)), - "per_ordered": ("<", 100) + "per_ordered": ("<", 100), }, - "Request for Quotation": { "docstatus": 0 }, + "Request for Quotation": {"docstatus": 0}, "Supplier Quotation": {"docstatus": 0}, - "Purchase Order": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, - "Purchase Receipt": { - "status": ("not in", ("Completed", "Closed")), - "docstatus": ("<", 2) - }, - "Work Order": { "status": ("in", ("Draft", "Not Started", "In Process")) }, + "Purchase Order": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, + "Purchase Receipt": {"status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2)}, + "Work Order": {"status": ("in", ("Draft", "Not Started", "In Process"))}, "BOM": {"docstatus": 0}, - "Timesheet": {"status": "Draft"}, - "Lab Test": {"docstatus": 0}, "Sample Collection": {"docstatus": 0}, "Patient Appointment": {"status": "Open"}, - "Patient Encounter": {"docstatus": 0} + "Patient Encounter": {"docstatus": 0}, }, - "targets": { "Company": { - "filters" : { "monthly_sales_target": ( ">", 0 ) }, - "target_field" : "monthly_sales_target", - "value_field" : "total_monthly_sales" + "filters": {"monthly_sales_target": (">", 0)}, + "target_field": "monthly_sales_target", + "value_field": "total_monthly_sales", } - } + }, } - doctype = [d for d in notifications.get('for_doctype')] - for doc in frappe.get_all('DocType', - fields= ["name"], filters = {"name": ("not in", doctype), 'is_submittable': 1}): + doctype = [d for d in notifications.get("for_doctype")] + for doc in frappe.get_all( + "DocType", fields=["name"], filters={"name": ("not in", doctype), "is_submittable": 1} + ): notifications["for_doctype"][doc.name] = {"docstatus": 0} return notifications diff --git a/erpnext/startup/report_data_map.py b/erpnext/startup/report_data_map.py index 65b48bf043b..f8c1b6cca07 100644 --- a/erpnext/startup/report_data_map.py +++ b/erpnext/startup/report_data_map.py @@ -6,90 +6,98 @@ # "remember to add indexes!" data_map = { - "Company": { - "columns": ["name"], - "conditions": ["docstatus < 2"] - }, + "Company": {"columns": ["name"], "conditions": ["docstatus < 2"]}, "Fiscal Year": { "columns": ["name", "year_start_date", "year_end_date"], "conditions": ["docstatus < 2"], }, - # Accounts "Account": { - "columns": ["name", "parent_account", "lft", "rgt", "report_type", - "company", "is_group"], + "columns": ["name", "parent_account", "lft", "rgt", "report_type", "company", "is_group"], "conditions": ["docstatus < 2"], "order_by": "lft", "links": { "company": ["Company", "name"], - } - + }, }, "Cost Center": { "columns": ["name", "lft", "rgt"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "GL Entry": { - "columns": ["name", "account", "posting_date", "cost_center", "debit", "credit", - "is_opening", "company", "voucher_type", "voucher_no", "remarks"], + "columns": [ + "name", + "account", + "posting_date", + "cost_center", + "debit", + "credit", + "is_opening", + "company", + "voucher_type", + "voucher_no", + "remarks", + ], "order_by": "posting_date, account", "links": { "account": ["Account", "name"], "company": ["Company", "name"], - "cost_center": ["Cost Center", "name"] - } + "cost_center": ["Cost Center", "name"], + }, }, - # Stock "Item": { - "columns": ["name", "if(item_name=name, '', item_name) as item_name", "description", - "item_group as parent_item_group", "stock_uom", "brand", "valuation_method"], + "columns": [ + "name", + "if(item_name=name, '', item_name) as item_name", + "description", + "item_group as parent_item_group", + "stock_uom", + "brand", + "valuation_method", + ], # "conditions": ["docstatus < 2"], "order_by": "name", - "links": { - "parent_item_group": ["Item Group", "name"], - "brand": ["Brand", "name"] - } + "links": {"parent_item_group": ["Item Group", "name"], "brand": ["Brand", "name"]}, }, "Item Group": { "columns": ["name", "parent_item_group"], # "conditions": ["docstatus < 2"], - "order_by": "lft" - }, - "Brand": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" - }, - "Project": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" - }, - "Warehouse": { - "columns": ["name"], - "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "lft", }, + "Brand": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, + "Project": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, + "Warehouse": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, "Stock Ledger Entry": { - "columns": ["name", "posting_date", "posting_time", "item_code", "warehouse", - "actual_qty as qty", "voucher_type", "voucher_no", "project", - "incoming_rate as incoming_rate", "stock_uom", "serial_no", - "qty_after_transaction", "valuation_rate"], + "columns": [ + "name", + "posting_date", + "posting_time", + "item_code", + "warehouse", + "actual_qty as qty", + "voucher_type", + "voucher_no", + "project", + "incoming_rate as incoming_rate", + "stock_uom", + "serial_no", + "qty_after_transaction", + "valuation_rate", + ], "order_by": "posting_date, posting_time, creation", "links": { "item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"], - "project": ["Project", "name"] + "project": ["Project", "name"], }, - "force_index": "posting_sort_index" + "force_index": "posting_sort_index", }, "Serial No": { "columns": ["name", "purchase_rate as incoming_rate"], "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "name", }, "Stock Entry": { "columns": ["name", "purpose"], @@ -97,227 +105,223 @@ data_map = { "order_by": "posting_date, posting_time, name", }, "Material Request Item": { - "columns": ["item.name as name", "item_code", "warehouse", - "(qty - ordered_qty) as qty"], + "columns": ["item.name as name", "item_code", "warehouse", "(qty - ordered_qty) as qty"], "from": "`tabMaterial Request Item` item, `tabMaterial Request` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > ordered_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > ordered_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, "Purchase Order Item": { - "columns": ["item.name as name", "item_code", "warehouse", - "(qty - received_qty)*conversion_factor as qty"], + "columns": [ + "item.name as name", + "item_code", + "warehouse", + "(qty - received_qty)*conversion_factor as qty", + ], "from": "`tabPurchase Order Item` item, `tabPurchase Order` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > received_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > received_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, - "Sales Order Item": { - "columns": ["item.name as name", "item_code", "(qty - delivered_qty)*conversion_factor as qty", "warehouse"], + "columns": [ + "item.name as name", + "item_code", + "(qty - delivered_qty)*conversion_factor as qty", + "warehouse", + ], "from": "`tabSales Order Item` item, `tabSales Order` main", - "conditions": ["item.parent = main.name", "main.docstatus=1", "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", "qty > delivered_qty"], - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"] - }, + "conditions": [ + "item.parent = main.name", + "main.docstatus=1", + "main.status != 'Stopped'", + "ifnull(warehouse, '')!=''", + "qty > delivered_qty", + ], + "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, }, - # Sales "Customer": { - "columns": ["name", "if(customer_name=name, '', customer_name) as customer_name", - "customer_group as parent_customer_group", "territory as parent_territory"], + "columns": [ + "name", + "if(customer_name=name, '', customer_name) as customer_name", + "customer_group as parent_customer_group", + "territory as parent_territory", + ], "conditions": ["docstatus < 2"], "order_by": "name", "links": { "parent_customer_group": ["Customer Group", "name"], "parent_territory": ["Territory", "name"], - } + }, }, "Customer Group": { "columns": ["name", "parent_customer_group"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "Territory": { "columns": ["name", "parent_territory"], "conditions": ["docstatus < 2"], - "order_by": "lft" + "order_by": "lft", }, "Sales Invoice": { "columns": ["name", "customer", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Sales Invoice Item": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Sales Invoice", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Sales Invoice", "name"], "item_code": ["Item", "name"]}, }, "Sales Order": { "columns": ["name", "customer", "transaction_date as posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "transaction_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Sales Order Item[Sales Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Sales Order", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Sales Order", "name"], "item_code": ["Item", "name"]}, }, "Delivery Note": { "columns": ["name", "customer", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "customer": ["Customer", "name"], - "company":["Company", "name"] - } + "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, }, "Delivery Note Item[Sales Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Delivery Note", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Delivery Note", "name"], "item_code": ["Item", "name"]}, }, "Supplier": { - "columns": ["name", "if(supplier_name=name, '', supplier_name) as supplier_name", - "supplier_group as parent_supplier_group"], + "columns": [ + "name", + "if(supplier_name=name, '', supplier_name) as supplier_name", + "supplier_group as parent_supplier_group", + ], "conditions": ["docstatus < 2"], "order_by": "name", "links": { "parent_supplier_group": ["Supplier Group", "name"], - } + }, }, "Supplier Group": { "columns": ["name", "parent_supplier_group"], "conditions": ["docstatus < 2"], - "order_by": "name" + "order_by": "name", }, "Purchase Invoice": { "columns": ["name", "supplier", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Invoice Item": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Invoice", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Invoice", "name"], "item_code": ["Item", "name"]}, }, "Purchase Order": { "columns": ["name", "supplier", "transaction_date as posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Order Item[Purchase Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Order", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Order", "name"], "item_code": ["Item", "name"]}, }, "Purchase Receipt": { "columns": ["name", "supplier", "posting_date", "company"], "conditions": ["docstatus=1"], "order_by": "posting_date", - "links": { - "supplier": ["Supplier", "name"], - "company":["Company", "name"] - } + "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, }, "Purchase Receipt Item[Purchase Analytics]": { "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], "order_by": "parent", - "links": { - "parent": ["Purchase Receipt", "name"], - "item_code": ["Item", "name"] - } + "links": {"parent": ["Purchase Receipt", "name"], "item_code": ["Item", "name"]}, }, # Support "Issue": { - "columns": ["name","status","creation","resolution_date","first_responded_on"], + "columns": ["name", "status", "creation", "resolution_date", "first_responded_on"], "conditions": ["docstatus < 2"], - "order_by": "creation" + "order_by": "creation", }, - # Manufacturing "Work Order": { - "columns": ["name","status","creation","planned_start_date","planned_end_date","status","actual_start_date","actual_end_date", "modified"], + "columns": [ + "name", + "status", + "creation", + "planned_start_date", + "planned_end_date", + "status", + "actual_start_date", + "actual_end_date", + "modified", + ], "conditions": ["docstatus = 1"], - "order_by": "creation" + "order_by": "creation", }, - - #Medical + # Medical "Patient": { - "columns": ["name", "creation", "owner", "if(patient_name=name, '', patient_name) as patient_name"], + "columns": [ + "name", + "creation", + "owner", + "if(patient_name=name, '', patient_name) as patient_name", + ], "conditions": ["docstatus < 2"], "order_by": "name", - "links": { - "owner" : ["User", "name"] - } + "links": {"owner": ["User", "name"]}, }, "Patient Appointment": { - "columns": ["name", "appointment_type", "patient", "practitioner", "appointment_date", "department", "status", "company"], + "columns": [ + "name", + "appointment_type", + "patient", + "practitioner", + "appointment_date", + "department", + "status", + "company", + ], "order_by": "name", "links": { "practitioner": ["Healthcare Practitioner", "name"], - "appointment_type": ["Appointment Type", "name"] - } + "appointment_type": ["Appointment Type", "name"], + }, }, "Healthcare Practitioner": { "columns": ["name", "department"], "order_by": "name", "links": { "department": ["Department", "name"], - } - + }, }, - "Appointment Type": { - "columns": ["name"], - "order_by": "name" - }, - "Medical Department": { - "columns": ["name"], - "order_by": "name" - } + "Appointment Type": {"columns": ["name"], "order_by": "name"}, + "Medical Department": {"columns": ["name"], "order_by": "name"}, } diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index 9fd1f0e8ce1..45bf012be85 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -1,19 +1,25 @@ - import frappe from frappe import _ install_docs = [ - {"doctype":"Role", "role_name":"Stock Manager", "name":"Stock Manager"}, - {"doctype":"Role", "role_name":"Item Manager", "name":"Item Manager"}, - {"doctype":"Role", "role_name":"Stock User", "name":"Stock User"}, - {"doctype":"Role", "role_name":"Quality Manager", "name":"Quality Manager"}, - {"doctype":"Item Group", "item_group_name":"All Item Groups", "is_group": 1}, - {"doctype":"Item Group", "item_group_name":"Default", - "parent_item_group":"All Item Groups", "is_group": 0}, + {"doctype": "Role", "role_name": "Stock Manager", "name": "Stock Manager"}, + {"doctype": "Role", "role_name": "Item Manager", "name": "Item Manager"}, + {"doctype": "Role", "role_name": "Stock User", "name": "Stock User"}, + {"doctype": "Role", "role_name": "Quality Manager", "name": "Quality Manager"}, + {"doctype": "Item Group", "item_group_name": "All Item Groups", "is_group": 1}, + { + "doctype": "Item Group", + "item_group_name": "Default", + "parent_item_group": "All Item Groups", + "is_group": 0, + }, ] + def get_warehouse_account_map(company=None): - company_warehouse_account_map = company and frappe.flags.setdefault('warehouse_account_map', {}).get(company) + company_warehouse_account_map = company and frappe.flags.setdefault( + "warehouse_account_map", {} + ).get(company) warehouse_account_map = frappe.flags.warehouse_account_map if not warehouse_account_map or not company_warehouse_account_map or frappe.flags.in_test: @@ -21,18 +27,20 @@ def get_warehouse_account_map(company=None): filters = {} if company: - filters['company'] = company - frappe.flags.setdefault('warehouse_account_map', {}).setdefault(company, {}) + filters["company"] = company + frappe.flags.setdefault("warehouse_account_map", {}).setdefault(company, {}) - for d in frappe.get_all('Warehouse', - fields = ["name", "account", "parent_warehouse", "company", "is_group"], - filters = filters, - order_by="lft, rgt"): + for d in frappe.get_all( + "Warehouse", + fields=["name", "account", "parent_warehouse", "company", "is_group"], + filters=filters, + order_by="lft, rgt", + ): if not d.account: d.account = get_warehouse_account(d, warehouse_account) if d.account: - d.account_currency = frappe.db.get_value('Account', d.account, 'account_currency', cache=True) + d.account_currency = frappe.db.get_value("Account", d.account, "account_currency", cache=True) warehouse_account.setdefault(d.name, d) if company: frappe.flags.warehouse_account_map[company] = warehouse_account @@ -41,6 +49,7 @@ def get_warehouse_account_map(company=None): return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map + def get_warehouse_account(warehouse, warehouse_account=None): account = warehouse.account if not account and warehouse.parent_warehouse: @@ -49,15 +58,20 @@ def get_warehouse_account(warehouse, warehouse_account=None): account = warehouse_account.get(warehouse.parent_warehouse).account else: from frappe.utils.nestedset import rebuild_tree + rebuild_tree("Warehouse", "parent_warehouse") else: - account = frappe.db.sql(""" + account = frappe.db.sql( + """ select account from `tabWarehouse` where lft <= %s and rgt >= %s and company = %s and account is not null and ifnull(account, '') !='' - order by lft desc limit 1""", (warehouse.lft, warehouse.rgt, warehouse.company), as_list=1) + order by lft desc limit 1""", + (warehouse.lft, warehouse.rgt, warehouse.company), + as_list=1, + ) account = account[0][0] if account else None @@ -65,13 +79,18 @@ def get_warehouse_account(warehouse, warehouse_account=None): account = get_company_default_inventory_account(warehouse.company) if not account and warehouse.company: - account = frappe.db.get_value('Account', - {'account_type': 'Stock', 'is_group': 0, 'company': warehouse.company}, 'name') + account = frappe.db.get_value( + "Account", {"account_type": "Stock", "is_group": 0, "company": warehouse.company}, "name" + ) if not account and warehouse.company and not warehouse.is_group: - frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}") - .format(warehouse.name, warehouse.company)) + frappe.throw( + _("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}").format( + warehouse.name, warehouse.company + ) + ) return account + def get_company_default_inventory_account(company): - return frappe.get_cached_value('Company', company, 'default_inventory_account') + return frappe.get_cached_value("Company", company, "default_inventory_account") diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index 9a83372f3e3..e1c535f8d58 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -1,62 +1,75 @@ - import frappe from frappe.model.db_query import DatabaseQuery from frappe.utils import cint, flt @frappe.whitelist() -def get_data(item_code=None, warehouse=None, item_group=None, - start=0, sort_by='actual_qty', sort_order='desc'): - '''Return data to render the item dashboard''' +def get_data( + item_code=None, warehouse=None, item_group=None, start=0, sort_by="actual_qty", sort_order="desc" +): + """Return data to render the item dashboard""" filters = [] if item_code: - filters.append(['item_code', '=', item_code]) + filters.append(["item_code", "=", item_code]) if warehouse: - filters.append(['warehouse', '=', warehouse]) + filters.append(["warehouse", "=", warehouse]) if item_group: lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) - items = frappe.db.sql_list(""" + items = frappe.db.sql_list( + """ select i.name from `tabItem` i where exists(select name from `tabItem Group` where name=i.item_group and lft >=%s and rgt<=%s) - """, (lft, rgt)) - filters.append(['item_code', 'in', items]) + """, + (lft, rgt), + ) + filters.append(["item_code", "in", items]) try: # check if user has any restrictions based on user permissions on warehouse - if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): - filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + if DatabaseQuery("Warehouse", user=frappe.session.user).build_match_conditions(): + filters.append(["warehouse", "in", [w.name for w in frappe.get_list("Warehouse")]]) except frappe.PermissionError: # user does not have access on warehouse return [] - items = frappe.db.get_all('Bin', fields=['item_code', 'warehouse', 'projected_qty', - 'reserved_qty', 'reserved_qty_for_production', 'reserved_qty_for_sub_contract', 'actual_qty', 'valuation_rate'], + items = frappe.db.get_all( + "Bin", + fields=[ + "item_code", + "warehouse", + "projected_qty", + "reserved_qty", + "reserved_qty_for_production", + "reserved_qty_for_sub_contract", + "actual_qty", + "valuation_rate", + ], or_filters={ - 'projected_qty': ['!=', 0], - 'reserved_qty': ['!=', 0], - 'reserved_qty_for_production': ['!=', 0], - 'reserved_qty_for_sub_contract': ['!=', 0], - 'actual_qty': ['!=', 0], + "projected_qty": ["!=", 0], + "reserved_qty": ["!=", 0], + "reserved_qty_for_production": ["!=", 0], + "reserved_qty_for_sub_contract": ["!=", 0], + "actual_qty": ["!=", 0], }, filters=filters, - order_by=sort_by + ' ' + sort_order, + order_by=sort_by + " " + sort_order, limit_start=start, - limit_page_length='21') + limit_page_length="21", + ) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) for item in items: - item.update({ - 'item_name': frappe.get_cached_value( - "Item", item.item_code, 'item_name'), - 'disable_quick_entry': frappe.get_cached_value( - "Item", item.item_code, 'has_batch_no') - or frappe.get_cached_value( - "Item", item.item_code, 'has_serial_no'), - 'projected_qty': flt(item.projected_qty, precision), - 'reserved_qty': flt(item.reserved_qty, precision), - 'reserved_qty_for_production': flt(item.reserved_qty_for_production, precision), - 'reserved_qty_for_sub_contract': flt(item.reserved_qty_for_sub_contract, precision), - 'actual_qty': flt(item.actual_qty, precision), - }) + item.update( + { + "item_name": frappe.get_cached_value("Item", item.item_code, "item_name"), + "disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no") + or frappe.get_cached_value("Item", item.item_code, "has_serial_no"), + "projected_qty": flt(item.projected_qty, precision), + "reserved_qty": flt(item.reserved_qty, precision), + "reserved_qty_for_production": flt(item.reserved_qty_for_production, precision), + "reserved_qty_for_sub_contract": flt(item.reserved_qty_for_sub_contract, precision), + "actual_qty": flt(item.actual_qty, precision), + } + ) return items diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py index e8b0bea468e..24e0ef11ffa 100644 --- a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py +++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py @@ -1,4 +1,3 @@ - import frappe from frappe.model.db_query import DatabaseQuery from frappe.utils import flt, nowdate @@ -7,8 +6,15 @@ from erpnext.stock.utils import get_stock_balance @frappe.whitelist() -def get_data(item_code=None, warehouse=None, parent_warehouse=None, - company=None, start=0, sort_by="stock_capacity", sort_order="desc"): +def get_data( + item_code=None, + warehouse=None, + parent_warehouse=None, + company=None, + start=0, + sort_by="stock_capacity", + sort_order="desc", +): """Return data to render the warehouse capacity dashboard.""" filters = get_filters(item_code, warehouse, parent_warehouse, company) @@ -19,51 +25,59 @@ def get_data(item_code=None, warehouse=None, parent_warehouse=None, capacity_data = get_warehouse_capacity_data(filters, start) asc_desc = -1 if sort_order == "desc" else 1 - capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc)) + capacity_data = sorted(capacity_data, key=lambda i: (i[sort_by] * asc_desc)) return capacity_data -def get_filters(item_code=None, warehouse=None, parent_warehouse=None, - company=None): - filters = [['disable', '=', 0]] + +def get_filters(item_code=None, warehouse=None, parent_warehouse=None, company=None): + filters = [["disable", "=", 0]] if item_code: - filters.append(['item_code', '=', item_code]) + filters.append(["item_code", "=", item_code]) if warehouse: - filters.append(['warehouse', '=', warehouse]) + filters.append(["warehouse", "=", warehouse]) if company: - filters.append(['company', '=', company]) + filters.append(["company", "=", company]) if parent_warehouse: lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"]) - warehouses = frappe.db.sql_list(""" + warehouses = frappe.db.sql_list( + """ select name from `tabWarehouse` where lft >=%s and rgt<=%s - """, (lft, rgt)) - filters.append(['warehouse', 'in', warehouses]) + """, + (lft, rgt), + ) + filters.append(["warehouse", "in", warehouses]) return filters + def get_warehouse_filter_based_on_permissions(filters): try: # check if user has any restrictions based on user permissions on warehouse - if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions(): - filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]]) + if DatabaseQuery("Warehouse", user=frappe.session.user).build_match_conditions(): + filters.append(["warehouse", "in", [w.name for w in frappe.get_list("Warehouse")]]) return False, filters except frappe.PermissionError: # user does not have access on warehouse return True, [] + def get_warehouse_capacity_data(filters, start): - capacity_data = frappe.db.get_all('Putaway Rule', - fields=['item_code', 'warehouse','stock_capacity', 'company'], + capacity_data = frappe.db.get_all( + "Putaway Rule", + fields=["item_code", "warehouse", "stock_capacity", "company"], filters=filters, limit_start=start, - limit_page_length='11' + limit_page_length="11", ) for entry in capacity_data: balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0 - entry.update({ - 'actual_qty': balance_qty, - 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0) - }) + entry.update( + { + "actual_qty": balance_qty, + "percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0), + } + ) return capacity_data diff --git a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py index d835420b9e2..dbf6cf05e79 100644 --- a/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py +++ b/erpnext/stock/dashboard_chart_source/warehouse_wise_stock_value/warehouse_wise_stock_value.py @@ -11,27 +11,38 @@ from erpnext.stock.utils import get_stock_value_from_bin @frappe.whitelist() @cache_source -def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None, heatmap_year = None): +def get( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): labels, datapoints = [], [] filters = frappe.parse_json(filters) - warehouse_filters = [['is_group', '=', 0]] + warehouse_filters = [["is_group", "=", 0]] if filters and filters.get("company"): - warehouse_filters.append(['company', '=', filters.get("company")]) + warehouse_filters.append(["company", "=", filters.get("company")]) - warehouses = frappe.get_list("Warehouse", fields=['name'], filters=warehouse_filters, order_by='name') + warehouses = frappe.get_list( + "Warehouse", fields=["name"], filters=warehouse_filters, order_by="name" + ) for wh in warehouses: balance = get_stock_value_from_bin(warehouse=wh.name) wh["balance"] = balance[0][0] - warehouses = [x for x in warehouses if not (x.get('balance') == None)] + warehouses = [x for x in warehouses if not (x.get("balance") == None)] if not warehouses: return [] - sorted_warehouse_map = sorted(warehouses, key = lambda i: i['balance'], reverse=True) + sorted_warehouse_map = sorted(warehouses, key=lambda i: i["balance"], reverse=True) if len(sorted_warehouse_map) > 10: sorted_warehouse_map = sorted_warehouse_map[:10] @@ -40,11 +51,8 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d labels.append(_(warehouse.get("name"))) datapoints.append(warehouse.get("balance")) - return{ + return { "labels": labels, - "datasets": [{ - "name": _("Stock Value"), - "values": datapoints - }], - "type": "bar" + "datasets": [{"name": _("Stock Value"), "values": datapoints}], + "type": "bar", } diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index c0cb30b5edb..295a65ef8e6 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -24,7 +24,7 @@ def get_name_from_hash(): temp = None while not temp: temp = frappe.generate_hash()[:7].upper() - if frappe.db.exists('Batch', temp): + if frappe.db.exists("Batch", temp): temp = None return temp @@ -35,7 +35,7 @@ def batch_uses_naming_series(): Verify if the Batch is to be named using a naming series :return: bool """ - use_naming_series = cint(frappe.db.get_single_value('Stock Settings', 'use_naming_series')) + use_naming_series = cint(frappe.db.get_single_value("Stock Settings", "use_naming_series")) return bool(use_naming_series) @@ -47,9 +47,9 @@ def _get_batch_prefix(): is set to use naming series. :return: The naming series. """ - naming_series_prefix = frappe.db.get_single_value('Stock Settings', 'naming_series_prefix') + naming_series_prefix = frappe.db.get_single_value("Stock Settings", "naming_series_prefix") if not naming_series_prefix: - naming_series_prefix = 'BATCH-' + naming_series_prefix = "BATCH-" return naming_series_prefix @@ -63,9 +63,9 @@ def _make_naming_series_key(prefix): :return: The derived key. If no prefix is given, an empty string is returned """ if not text_type(prefix): - return '' + return "" else: - return prefix.upper() + '.#####' + return prefix.upper() + ".#####" def get_batch_naming_series(): @@ -75,7 +75,7 @@ def get_batch_naming_series(): Naming series key is in the format [prefix].[#####] :return: The naming series or empty string if not available """ - series = '' + series = "" if batch_uses_naming_series(): prefix = _get_batch_prefix() key = _make_naming_series_key(prefix) @@ -88,8 +88,9 @@ class Batch(Document): def autoname(self): """Generate random ID for batch if not specified""" if not self.batch_id: - create_new_batch, batch_number_series = frappe.db.get_value('Item', self.item, - ['create_new_batch', 'batch_number_series']) + create_new_batch, batch_number_series = frappe.db.get_value( + "Item", self.item, ["create_new_batch", "batch_number_series"] + ) if create_new_batch: if batch_number_series: @@ -99,12 +100,12 @@ class Batch(Document): else: self.batch_id = get_name_from_hash() else: - frappe.throw(_('Batch ID is mandatory'), frappe.MandatoryError) + frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError) self.name = self.batch_id def onload(self): - self.image = frappe.db.get_value('Item', self.item, 'image') + self.image = frappe.db.get_value("Item", self.item, "image") def after_delete(self): revert_series_if_last(get_batch_naming_series(), self.name) @@ -117,16 +118,21 @@ class Batch(Document): frappe.throw(_("The selected item cannot have Batch")) def before_save(self): - has_expiry_date, shelf_life_in_days = frappe.db.get_value('Item', self.item, ['has_expiry_date', 'shelf_life_in_days']) + has_expiry_date, shelf_life_in_days = frappe.db.get_value( + "Item", self.item, ["has_expiry_date", "shelf_life_in_days"] + ) if not self.expiry_date and has_expiry_date and shelf_life_in_days: self.expiry_date = add_days(self.manufacturing_date, shelf_life_in_days) if has_expiry_date and not self.expiry_date: - frappe.throw(msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.") \ - .format(frappe.bold("Shelf Life in Days"), + frappe.throw( + msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.").format( + frappe.bold("Shelf Life in Days"), get_link_to_form("Item", self.item), - frappe.bold("Batch Expiry Date")), - title=_("Expiry Date Mandatory")) + frappe.bold("Batch Expiry Date"), + ), + title=_("Expiry Date Mandatory"), + ) def get_name_from_naming_series(self): """ @@ -143,9 +149,11 @@ class Batch(Document): @frappe.whitelist() -def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None): +def get_batch_qty( + batch_no=None, warehouse=None, item_code=None, posting_date=None, posting_time=None +): """Returns batch actual qty if warehouse is passed, - or returns dict of qty by warehouse if warehouse is None + or returns dict of qty by warehouse if warehouse is None The user must pass either batch_no or batch_no + warehouse or item_code + warehouse @@ -157,25 +165,41 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No if batch_no and warehouse: cond = "" if posting_date and posting_time: - cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format(posting_date, - posting_time) + cond = " and timestamp(posting_date, posting_time) <= timestamp('{0}', '{1}')".format( + posting_date, posting_time + ) - out = float(frappe.db.sql("""select sum(actual_qty) + out = float( + frappe.db.sql( + """select sum(actual_qty) from `tabStock Ledger Entry` - where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond), - (warehouse, batch_no))[0][0] or 0) + where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format( + cond + ), + (warehouse, batch_no), + )[0][0] + or 0 + ) if batch_no and not warehouse: - out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty + out = frappe.db.sql( + """select warehouse, sum(actual_qty) as qty from `tabStock Ledger Entry` where is_cancelled = 0 and batch_no=%s - group by warehouse''', batch_no, as_dict=1) + group by warehouse""", + batch_no, + as_dict=1, + ) if not batch_no and item_code and warehouse: - out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty + out = frappe.db.sql( + """select batch_no, sum(actual_qty) as qty from `tabStock Ledger Entry` where is_cancelled = 0 and item_code = %s and warehouse=%s - group by batch_no''', (item_code, warehouse), as_dict=1) + group by batch_no""", + (item_code, warehouse), + as_dict=1, + ) return out @@ -184,7 +208,9 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No def get_batches_by_oldest(item_code, warehouse): """Returns the oldest batch and qty for the given item_code and warehouse""" batches = get_batch_qty(item_code=item_code, warehouse=warehouse) - batches_dates = [[batch, frappe.get_value('Batch', batch.batch_no, 'expiry_date')] for batch in batches] + batches_dates = [ + [batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches + ] batches_dates.sort(key=lambda tup: tup[1]) return batches_dates @@ -192,33 +218,25 @@ def get_batches_by_oldest(item_code, warehouse): @frappe.whitelist() def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): """Split the batch into a new batch""" - batch = frappe.get_doc(dict(doctype='Batch', item=item_code, batch_id=new_batch_id)).insert() + batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert() - company = frappe.db.get_value('Stock Ledger Entry', dict( - item_code=item_code, - batch_no=batch_no, - warehouse=warehouse - ), ['company']) + company = frappe.db.get_value( + "Stock Ledger Entry", + dict(item_code=item_code, batch_no=batch_no, warehouse=warehouse), + ["company"], + ) - stock_entry = frappe.get_doc(dict( - doctype='Stock Entry', - purpose='Repack', - company=company, - items=[ - dict( - item_code=item_code, - qty=float(qty or 0), - s_warehouse=warehouse, - batch_no=batch_no - ), - dict( - item_code=item_code, - qty=float(qty or 0), - t_warehouse=warehouse, - batch_no=batch.name - ), - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Repack", + company=company, + items=[ + dict(item_code=item_code, qty=float(qty or 0), s_warehouse=warehouse, batch_no=batch_no), + dict(item_code=item_code, qty=float(qty or 0), t_warehouse=warehouse, batch_no=batch.name), + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() stock_entry.submit() @@ -229,15 +247,20 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"): """Automatically select `batch_no` for outgoing items in item table""" for d in doc.get(child_table): - qty = d.get('stock_qty') or d.get('transfer_qty') or d.get('qty') or 0 + qty = d.get("stock_qty") or d.get("transfer_qty") or d.get("qty") or 0 warehouse = d.get(warehouse_field, None) - if warehouse and qty > 0 and frappe.db.get_value('Item', d.item_code, 'has_batch_no'): + if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"): if not d.batch_no: d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no) else: batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse) if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")): - frappe.throw(_("Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches").format(d.idx, d.batch_no, batch_qty, qty)) + frappe.throw( + _( + "Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches" + ).format(d.idx, d.batch_no, batch_qty, qty) + ) + @frappe.whitelist() def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): @@ -258,7 +281,11 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): break if not batch_no: - frappe.msgprint(_('Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement').format(frappe.bold(item_code))) + frappe.msgprint( + _( + "Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement" + ).format(frappe.bold(item_code)) + ) if throw: raise UnableToSelectBatchError @@ -267,16 +294,14 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - cond = '' - if serial_no and frappe.get_cached_value('Item', item_code, 'has_batch_no'): + + cond = "" + if serial_no and frappe.get_cached_value("Item", item_code, "has_batch_no"): serial_nos = get_serial_nos(serial_no) - batch = frappe.get_all("Serial No", - fields = ["distinct batch_no"], - filters= { - "item_code": item_code, - "warehouse": warehouse, - "name": ("in", serial_nos) - } + batch = frappe.get_all( + "Serial No", + fields=["distinct batch_no"], + filters={"item_code": item_code, "warehouse": warehouse, "name": ("in", serial_nos)}, ) if not batch: @@ -285,9 +310,10 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): if batch and len(batch) > 1: return [] - cond = " and `tabBatch`.name = %s" %(frappe.db.escape(batch[0].batch_no)) + cond = " and `tabBatch`.name = %s" % (frappe.db.escape(batch[0].batch_no)) - return frappe.db.sql(""" + return frappe.db.sql( + """ select batch_id, sum(`tabStock Ledger Entry`.actual_qty) as qty from `tabBatch` join `tabStock Ledger Entry` ignore index (item_code, warehouse) @@ -297,24 +323,34 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC - """.format(cond), (item_code, warehouse), as_dict=True) + """.format( + cond + ), + (item_code, warehouse), + as_dict=True, + ) + def validate_serial_no_with_batch(serial_nos, item_code): if frappe.get_cached_value("Serial No", serial_nos[0], "item_code") != item_code: - frappe.throw(_("The serial no {0} does not belong to item {1}") - .format(get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code))) + frappe.throw( + _("The serial no {0} does not belong to item {1}").format( + get_link_to_form("Serial No", serial_nos[0]), get_link_to_form("Item", item_code) + ) + ) - serial_no_link = ','.join(get_link_to_form("Serial No", sn) for sn in serial_nos) + serial_no_link = ",".join(get_link_to_form("Serial No", sn) for sn in serial_nos) message = "Serial Nos" if len(serial_nos) > 1 else "Serial No" - frappe.throw(_("There is no batch found against the {0}: {1}") - .format(message, serial_no_link)) + frappe.throw(_("There is no batch found against the {0}: {1}").format(message, serial_no_link)) + def make_batch(args): if frappe.db.get_value("Item", args.item, "has_batch_no"): args.doctype = "Batch" frappe.get_doc(args).insert().name + @frappe.whitelist() def get_pos_reserved_batch_qty(filters): import json @@ -328,16 +364,22 @@ def get_pos_reserved_batch_qty(filters): item = frappe.qb.DocType("POS Invoice Item").as_("item") sum_qty = Sum(item.qty).as_("qty") - reserved_batch_qty = frappe.qb.from_(p).from_(item).select(sum_qty).where( - (p.name == item.parent) & - (p.consolidated_invoice.isnull()) & - (p.status != "Consolidated") & - (p.docstatus == 1) & - (item.docstatus == 1) & - (item.item_code == filters.get('item_code')) & - (item.warehouse == filters.get('warehouse')) & - (item.batch_no == filters.get('batch_no')) - ).run() + reserved_batch_qty = ( + frappe.qb.from_(p) + .from_(item) + .select(sum_qty) + .where( + (p.name == item.parent) + & (p.consolidated_invoice.isnull()) + & (p.status != "Consolidated") + & (p.docstatus == 1) + & (item.docstatus == 1) + & (item.item_code == filters.get("item_code")) + & (item.warehouse == filters.get("warehouse")) + & (item.batch_no == filters.get("batch_no")) + ) + .run() + ) flt_reserved_batch_qty = flt(reserved_batch_qty[0][0]) return flt_reserved_batch_qty diff --git a/erpnext/stock/doctype/batch/batch_dashboard.py b/erpnext/stock/doctype/batch/batch_dashboard.py index afa0fca99a0..84b64f36f40 100644 --- a/erpnext/stock/doctype/batch/batch_dashboard.py +++ b/erpnext/stock/doctype/batch/batch_dashboard.py @@ -1,26 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'batch_no', - 'transactions': [ - { - 'label': _('Buy'), - 'items': ['Purchase Invoice', 'Purchase Receipt'] - }, - { - 'label': _('Sell'), - 'items': ['Sales Invoice', 'Delivery Note'] - }, - { - 'label': _('Move'), - 'items': ['Stock Entry'] - }, - { - 'label': _('Quality'), - 'items': ['Quality Inspection'] - } - ] + "fieldname": "batch_no", + "transactions": [ + {"label": _("Buy"), "items": ["Purchase Invoice", "Purchase Receipt"]}, + {"label": _("Sell"), "items": ["Sales Invoice", "Delivery Note"]}, + {"label": _("Move"), "items": ["Stock Entry"]}, + {"label": _("Quality"), "items": ["Quality Inspection"]}, + ], } diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 8d7a2cf8d8c..c1190c8fc57 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -13,134 +13,127 @@ from erpnext.stock.get_item_details import get_item_details class TestBatch(FrappeTestCase): def test_item_has_batch_enabled(self): - self.assertRaises(ValidationError, frappe.get_doc({ - "doctype": "Batch", - "name": "_test Batch", - "item": "_Test Item" - }).save) + self.assertRaises( + ValidationError, + frappe.get_doc({"doctype": "Batch", "name": "_test Batch", "item": "_Test Item"}).save, + ) @classmethod def make_batch_item(cls, item_name): from erpnext.stock.doctype.item.test_item import make_item + if not frappe.db.exists(item_name): - return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1)) + return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) - def test_purchase_receipt(self, batch_qty = 100): - '''Test automated batch creation from Purchase Receipt''' - self.make_batch_item('ITEM-BATCH-1') + def test_purchase_receipt(self, batch_qty=100): + """Test automated batch creation from Purchase Receipt""" + self.make_batch_item("ITEM-BATCH-1") - receipt = frappe.get_doc(dict( - doctype='Purchase Receipt', - supplier='_Test Supplier', - company='_Test Company', - items=[ - dict( - item_code='ITEM-BATCH-1', - qty=batch_qty, - rate=10, - warehouse= 'Stores - _TC' - ) - ] - )).insert() + receipt = frappe.get_doc( + dict( + doctype="Purchase Receipt", + supplier="_Test Supplier", + company="_Test Company", + items=[dict(item_code="ITEM-BATCH-1", qty=batch_qty, rate=10, warehouse="Stores - _TC")], + ) + ).insert() receipt.submit() self.assertTrue(receipt.items[0].batch_no) - self.assertEqual(get_batch_qty(receipt.items[0].batch_no, - receipt.items[0].warehouse), batch_qty) + self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), batch_qty) return receipt def test_stock_entry_incoming(self): - '''Test batch creation via Stock Entry (Work Order)''' + """Test batch creation via Stock Entry (Work Order)""" - self.make_batch_item('ITEM-BATCH-1') + self.make_batch_item("ITEM-BATCH-1") - stock_entry = frappe.get_doc(dict( - doctype = 'Stock Entry', - purpose = 'Material Receipt', - company = '_Test Company', - items = [ - dict( - item_code = 'ITEM-BATCH-1', - qty = 90, - t_warehouse = '_Test Warehouse - _TC', - cost_center = 'Main - _TC', - rate = 10 - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Receipt", + company="_Test Company", + items=[ + dict( + item_code="ITEM-BATCH-1", + qty=90, + t_warehouse="_Test Warehouse - _TC", + cost_center="Main - _TC", + rate=10, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() stock_entry.submit() self.assertTrue(stock_entry.items[0].batch_no) - self.assertEqual(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90) + self.assertEqual( + get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90 + ) def test_delivery_note(self): - '''Test automatic batch selection for outgoing items''' + """Test automatic batch selection for outgoing items""" batch_qty = 15 receipt = self.test_purchase_receipt(batch_qty) - item_code = 'ITEM-BATCH-1' + item_code = "ITEM-BATCH-1" - delivery_note = frappe.get_doc(dict( - doctype='Delivery Note', - customer='_Test Customer', - company=receipt.company, - items=[ - dict( - item_code=item_code, - qty=batch_qty, - rate=10, - warehouse=receipt.items[0].warehouse - ) - ] - )).insert() + delivery_note = frappe.get_doc( + dict( + doctype="Delivery Note", + customer="_Test Customer", + company=receipt.company, + items=[ + dict(item_code=item_code, qty=batch_qty, rate=10, warehouse=receipt.items[0].warehouse) + ], + ) + ).insert() delivery_note.submit() # shipped from FEFO batch self.assertEqual( - delivery_note.items[0].batch_no, - get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) ) def test_delivery_note_fail(self): - '''Test automatic batch selection for outgoing items''' + """Test automatic batch selection for outgoing items""" receipt = self.test_purchase_receipt(100) - delivery_note = frappe.get_doc(dict( - doctype = 'Delivery Note', - customer = '_Test Customer', - company = receipt.company, - items = [ - dict( - item_code = 'ITEM-BATCH-1', - qty = 5000, - rate = 10, - warehouse = receipt.items[0].warehouse - ) - ] - )) + delivery_note = frappe.get_doc( + dict( + doctype="Delivery Note", + customer="_Test Customer", + company=receipt.company, + items=[ + dict(item_code="ITEM-BATCH-1", qty=5000, rate=10, warehouse=receipt.items[0].warehouse) + ], + ) + ) self.assertRaises(UnableToSelectBatchError, delivery_note.insert) def test_stock_entry_outgoing(self): - '''Test automatic batch selection for outgoing stock entry''' + """Test automatic batch selection for outgoing stock entry""" batch_qty = 16 receipt = self.test_purchase_receipt(batch_qty) - item_code = 'ITEM-BATCH-1' + item_code = "ITEM-BATCH-1" - stock_entry = frappe.get_doc(dict( - doctype='Stock Entry', - purpose='Material Issue', - company=receipt.company, - items=[ - dict( - item_code=item_code, - qty=batch_qty, - s_warehouse=receipt.items[0].warehouse, - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Issue", + company=receipt.company, + items=[ + dict( + item_code=item_code, + qty=batch_qty, + s_warehouse=receipt.items[0].warehouse, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() @@ -148,35 +141,38 @@ class TestBatch(FrappeTestCase): # assert same batch is selected self.assertEqual( - stock_entry.items[0].batch_no, - get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) + stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty) ) def test_batch_split(self): - '''Test batch splitting''' + """Test batch splitting""" receipt = self.test_purchase_receipt() from erpnext.stock.doctype.batch.batch import split_batch - new_batch = split_batch(receipt.items[0].batch_no, 'ITEM-BATCH-1', receipt.items[0].warehouse, 22) + new_batch = split_batch( + receipt.items[0].batch_no, "ITEM-BATCH-1", receipt.items[0].warehouse, 22 + ) self.assertEqual(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 78) self.assertEqual(get_batch_qty(new_batch, receipt.items[0].warehouse), 22) def test_get_batch_qty(self): - '''Test getting batch quantities by batch_numbers, item_code or warehouse''' - self.make_batch_item('ITEM-BATCH-2') - self.make_new_batch_and_entry('ITEM-BATCH-2', 'batch a', '_Test Warehouse - _TC') - self.make_new_batch_and_entry('ITEM-BATCH-2', 'batch b', '_Test Warehouse - _TC') + """Test getting batch quantities by batch_numbers, item_code or warehouse""" + self.make_batch_item("ITEM-BATCH-2") + self.make_new_batch_and_entry("ITEM-BATCH-2", "batch a", "_Test Warehouse - _TC") + self.make_new_batch_and_entry("ITEM-BATCH-2", "batch b", "_Test Warehouse - _TC") - self.assertEqual(get_batch_qty(item_code = 'ITEM-BATCH-2', warehouse = '_Test Warehouse - _TC'), - [{'batch_no': u'batch a', 'qty': 90.0}, {'batch_no': u'batch b', 'qty': 90.0}]) + self.assertEqual( + get_batch_qty(item_code="ITEM-BATCH-2", warehouse="_Test Warehouse - _TC"), + [{"batch_no": "batch a", "qty": 90.0}, {"batch_no": "batch b", "qty": 90.0}], + ) - self.assertEqual(get_batch_qty('batch a', '_Test Warehouse - _TC'), 90) + self.assertEqual(get_batch_qty("batch a", "_Test Warehouse - _TC"), 90) def test_total_batch_qty(self): - self.make_batch_item('ITEM-BATCH-3') + self.make_batch_item("ITEM-BATCH-3") existing_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) - stock_entry = self.make_new_batch_and_entry('ITEM-BATCH-3', 'B100', '_Test Warehouse - _TC') + stock_entry = self.make_new_batch_and_entry("ITEM-BATCH-3", "B100", "_Test Warehouse - _TC") current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty")) self.assertEqual(current_batch_qty, existing_batch_qty + 90) @@ -187,32 +183,32 @@ class TestBatch(FrappeTestCase): @classmethod def make_new_batch_and_entry(cls, item_name, batch_name, warehouse): - '''Make a new stock entry for given target warehouse and batch name of item''' + """Make a new stock entry for given target warehouse and batch name of item""" if not frappe.db.exists("Batch", batch_name): - batch = frappe.get_doc(dict( - doctype = 'Batch', - item = item_name, - batch_id = batch_name - )).insert(ignore_permissions=True) + batch = frappe.get_doc(dict(doctype="Batch", item=item_name, batch_id=batch_name)).insert( + ignore_permissions=True + ) batch.save() - stock_entry = frappe.get_doc(dict( - doctype = 'Stock Entry', - purpose = 'Material Receipt', - company = '_Test Company', - items = [ - dict( - item_code = item_name, - qty = 90, - t_warehouse = warehouse, - cost_center = 'Main - _TC', - rate = 10, - batch_no = batch_name, - allow_zero_valuation_rate = 1 - ) - ] - )) + stock_entry = frappe.get_doc( + dict( + doctype="Stock Entry", + purpose="Material Receipt", + company="_Test Company", + items=[ + dict( + item_code=item_name, + qty=90, + t_warehouse=warehouse, + cost_center="Main - _TC", + rate=10, + batch_no=batch_name, + allow_zero_valuation_rate=1, + ) + ], + ) + ) stock_entry.set_stock_entry_type() stock_entry.insert() @@ -221,28 +217,28 @@ class TestBatch(FrappeTestCase): return stock_entry def test_batch_name_with_naming_series(self): - stock_settings = frappe.get_single('Stock Settings') + stock_settings = frappe.get_single("Stock Settings") use_naming_series = cint(stock_settings.use_naming_series) if not use_naming_series: - frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 1) + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 1) - batch = self.make_new_batch('_Test Stock Item For Batch Test1') + batch = self.make_new_batch("_Test Stock Item For Batch Test1") batch_name = batch.name - self.assertTrue(batch_name.startswith('BATCH-')) + self.assertTrue(batch_name.startswith("BATCH-")) batch.delete() - batch = self.make_new_batch('_Test Stock Item For Batch Test2') + batch = self.make_new_batch("_Test Stock Item For Batch Test2") self.assertEqual(batch_name, batch.name) # reset Stock Settings if not use_naming_series: - frappe.set_value('Stock Settings', 'Stock Settings', 'use_naming_series', 0) + frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0) def make_new_batch(self, item_name, batch_id=None, do_not_insert=0): - batch = frappe.new_doc('Batch') + batch = frappe.new_doc("Batch") item = self.make_batch_item(item_name) batch.item = item.name @@ -255,57 +251,67 @@ class TestBatch(FrappeTestCase): return batch def test_batch_wise_item_price(self): - if not frappe.db.get_value('Item', '_Test Batch Price Item'): - frappe.get_doc({ - 'doctype': 'Item', - 'is_stock_item': 1, - 'item_code': '_Test Batch Price Item', - 'item_group': 'Products', - 'has_batch_no': 1, - 'create_new_batch': 1 - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Item", "_Test Batch Price Item"): + frappe.get_doc( + { + "doctype": "Item", + "is_stock_item": 1, + "item_code": "_Test Batch Price Item", + "item_group": "Products", + "has_batch_no": 1, + "create_new_batch": 1, + } + ).insert(ignore_permissions=True) - batch1 = create_batch('_Test Batch Price Item', 200, 1) - batch2 = create_batch('_Test Batch Price Item', 300, 1) - batch3 = create_batch('_Test Batch Price Item', 400, 0) + batch1 = create_batch("_Test Batch Price Item", 200, 1) + batch2 = create_batch("_Test Batch Price Item", 300, 1) + batch3 = create_batch("_Test Batch Price Item", 400, 0) company = "_Test Company with perpetual inventory" - currency = frappe.get_cached_value("Company", company, "default_currency") + currency = frappe.get_cached_value("Company", company, "default_currency") - args = frappe._dict({ - "item_code": "_Test Batch Price Item", - "company": company, - "price_list": "_Test Price List", - "currency": currency, - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "customer": "_Test Customer", - "name": None - }) + args = frappe._dict( + { + "item_code": "_Test Batch Price Item", + "company": company, + "price_list": "_Test Price List", + "currency": currency, + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + "name": None, + } + ) - #test price for batch1 - args.update({'batch_no': batch1}) + # test price for batch1 + args.update({"batch_no": batch1}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 200) + self.assertEqual(details.get("price_list_rate"), 200) - #test price for batch2 - args.update({'batch_no': batch2}) + # test price for batch2 + args.update({"batch_no": batch2}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 300) + self.assertEqual(details.get("price_list_rate"), 300) - #test price for batch3 - args.update({'batch_no': batch3}) + # test price for batch3 + args.update({"batch_no": batch3}) details = get_item_details(args) - self.assertEqual(details.get('price_list_rate'), 400) + self.assertEqual(details.get("price_list_rate"), 400) + def create_batch(item_code, rate, create_item_price_for_batch): - pi = make_purchase_invoice(company="_Test Company", - warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1, - expense_account ="_Test Account Cost for Goods Sold - _TC", item_code=item_code) + pi = make_purchase_invoice( + company="_Test Company", + warehouse="Stores - _TC", + cost_center="Main - _TC", + update_stock=1, + expense_account="_Test Account Cost for Goods Sold - _TC", + item_code=item_code, + ) - batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) + batch = frappe.db.get_value("Batch", {"item": item_code, "reference_name": pi.name}) if not create_item_price_for_batch: create_price_list_for_batch(item_code, None, rate) @@ -314,24 +320,30 @@ def create_batch(item_code, rate, create_item_price_for_batch): return batch + def create_price_list_for_batch(item_code, batch, rate): - frappe.get_doc({ - 'doctype': 'Item Price', - 'item_code': '_Test Batch Price Item', - 'price_list': '_Test Price List', - 'batch_no': batch, - 'price_list_rate': rate - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": "_Test Batch Price Item", + "price_list": "_Test Price List", + "batch_no": batch, + "price_list_rate": rate, + } + ).insert() + def make_new_batch(**args): args = frappe._dict(args) try: - batch = frappe.get_doc({ - "doctype": "Batch", - "batch_id": args.batch_id, - "item": args.item_code, - }).insert() + batch = frappe.get_doc( + { + "doctype": "Batch", + "batch_id": args.batch_id, + "item": args.item_code, + } + ).insert() except frappe.DuplicateEntryError: batch = frappe.get_doc("Batch", args.batch_id) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index b2ec15690c2..4e49ac800eb 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -10,38 +10,51 @@ from frappe.utils import flt class Bin(Document): def before_save(self): if self.get("__islocal") or not self.stock_uom: - self.stock_uom = frappe.get_cached_value('Item', self.item_code, 'stock_uom') + self.stock_uom = frappe.get_cached_value("Item", self.item_code, "stock_uom") self.set_projected_qty() def set_projected_qty(self): - self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty) - + flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) - - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract)) + self.projected_qty = ( + flt(self.actual_qty) + + flt(self.ordered_qty) + + flt(self.indented_qty) + + flt(self.planned_qty) + - flt(self.reserved_qty) + - flt(self.reserved_qty_for_production) + - flt(self.reserved_qty_for_sub_contract) + ) def get_first_sle(self): - sle = frappe.db.sql(""" + sle = frappe.db.sql( + """ select * from `tabStock Ledger Entry` where item_code = %s and warehouse = %s order by timestamp(posting_date, posting_time) asc, creation asc limit 1 - """, (self.item_code, self.warehouse), as_dict=1) + """, + (self.item_code, self.warehouse), + as_dict=1, + ) return sle and sle[0] or None def update_reserved_qty_for_production(self): - '''Update qty reserved for production from Production Item tables - in open work orders''' + """Update qty reserved for production from Production Item tables + in open work orders""" from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production - self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse) + self.reserved_qty_for_production = get_reserved_qty_for_production( + self.item_code, self.warehouse + ) self.set_projected_qty() - self.db_set('reserved_qty_for_production', flt(self.reserved_qty_for_production)) - self.db_set('projected_qty', self.projected_qty) + self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) + self.db_set("projected_qty", self.projected_qty) def update_reserved_qty_for_sub_contracting(self): - #reserved qty - reserved_qty_for_sub_contract = frappe.db.sql(''' + # reserved qty + reserved_qty_for_sub_contract = frappe.db.sql( + """ select ifnull(sum(itemsup.required_qty),0) from `tabPurchase Order` po, `tabPurchase Order Item Supplied` itemsup where @@ -51,10 +64,13 @@ class Bin(Document): and po.is_subcontracted = 'Yes' and po.status != 'Closed' and po.per_received < 100 - and itemsup.reserve_warehouse = %s''', (self.item_code, self.warehouse))[0][0] + and itemsup.reserve_warehouse = %s""", + (self.item_code, self.warehouse), + )[0][0] - #Get Transferred Entries - materials_transferred = frappe.db.sql(""" + # Get Transferred Entries + materials_transferred = frappe.db.sql( + """ select ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0) from @@ -70,16 +86,19 @@ class Bin(Document): and po.is_subcontracted = 'Yes' and po.status != 'Closed' and po.per_received < 100 - """, {'item': self.item_code})[0][0] + """, + {"item": self.item_code}, + )[0][0] if reserved_qty_for_sub_contract > materials_transferred: reserved_qty_for_sub_contract = reserved_qty_for_sub_contract - materials_transferred else: reserved_qty_for_sub_contract = 0 - self.db_set('reserved_qty_for_sub_contract', reserved_qty_for_sub_contract) + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract) self.set_projected_qty() - self.db_set('projected_qty', self.projected_qty) + self.db_set("projected_qty", self.projected_qty) + def on_doctype_update(): frappe.db.add_unique("Bin", ["item_code", "warehouse"], constraint_name="unique_item_warehouse") @@ -92,10 +111,23 @@ def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_vou repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) update_qty(bin_name, args) + def get_bin_details(bin_name): - return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', - 'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production', - 'reserved_qty_for_sub_contract'], as_dict=1) + return frappe.db.get_value( + "Bin", + bin_name, + [ + "actual_qty", + "ordered_qty", + "reserved_qty", + "indented_qty", + "planned_qty", + "reserved_qty_for_production", + "reserved_qty_for_sub_contract", + ], + as_dict=1, + ) + def update_qty(bin_name, args): from erpnext.controllers.stock_controller import future_sle_exists @@ -106,32 +138,45 @@ def update_qty(bin_name, args): # actual qty is not up to date in case of backdated transaction if future_sle_exists(args): - actual_qty = frappe.db.get_value("Stock Ledger Entry", + actual_qty = ( + frappe.db.get_value( + "Stock Ledger Entry", filters={ "item_code": args.get("item_code"), "warehouse": args.get("warehouse"), - "is_cancelled": 0 + "is_cancelled": 0, }, fieldname="qty_after_transaction", order_by="posting_date desc, posting_time desc, creation desc", - ) or 0.0 + ) + or 0.0 + ) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty")) indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty")) planned_qty = flt(bin_details.planned_qty) + flt(args.get("planned_qty")) - # compute projected qty - projected_qty = (flt(actual_qty) + flt(ordered_qty) - + flt(indented_qty) + flt(planned_qty) - flt(reserved_qty) - - flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract)) + projected_qty = ( + flt(actual_qty) + + flt(ordered_qty) + + flt(indented_qty) + + flt(planned_qty) + - flt(reserved_qty) + - flt(bin_details.reserved_qty_for_production) + - flt(bin_details.reserved_qty_for_sub_contract) + ) - frappe.db.set_value('Bin', bin_name, { - 'actual_qty': actual_qty, - 'ordered_qty': ordered_qty, - 'reserved_qty': reserved_qty, - 'indented_qty': indented_qty, - 'planned_qty': planned_qty, - 'projected_qty': projected_qty - }) + frappe.db.set_value( + "Bin", + bin_name, + { + "actual_qty": actual_qty, + "ordered_qty": ordered_qty, + "reserved_qty": reserved_qty, + "indented_qty": indented_qty, + "planned_qty": planned_qty, + "projected_qty": projected_qty, + }, + ) diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index ec0d8a88e3f..b79dee81e21 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -9,10 +9,8 @@ from erpnext.stock.utils import _create_bin class TestBin(FrappeTestCase): - - def test_concurrent_inserts(self): - """ Ensure no duplicates are possible in case of concurrent inserts""" + """Ensure no duplicates are possible in case of concurrent inserts""" item_code = "_TestConcurrentBin" make_item(item_code) warehouse = "_Test Warehouse - _TC" diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7b1489c40d2..304857dd0f8 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -15,72 +15,76 @@ from erpnext.controllers.selling_controller import SellingController from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class DeliveryNote(SellingController): def __init__(self, *args, **kwargs): super(DeliveryNote, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'delivered_qty', - 'target_parent_dt': 'Sales Order', - 'target_parent_field': 'per_delivered', - 'target_ref_field': 'qty', - 'source_field': 'qty', - 'percent_join_field': 'against_sales_order', - 'status_field': 'delivery_status', - 'keyword': 'Delivered', - 'second_source_dt': 'Sales Invoice Item', - 'second_source_field': 'qty', - 'second_join_field': 'so_detail', - 'overflow_type': 'delivery', - 'second_source_extra_cond': """ and exists(select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and update_stock = 1)""" - }, - { - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Invoice Item', - 'join_field': 'si_detail', - 'target_field': 'delivered_qty', - 'target_parent_dt': 'Sales Invoice', - 'target_ref_field': 'qty', - 'source_field': 'qty', - 'percent_join_field': 'against_sales_invoice', - 'overflow_type': 'delivery', - 'no_allowance': 1 - }] - if cint(self.is_return): - self.status_updater.extend([{ - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Sales Order', - 'source_field': '-1 * qty', - 'second_source_dt': 'Sales Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'so_detail', - 'extra_cond': """ and exists (select name from `tabDelivery Note` - where name=`tabDelivery Note Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""" + self.status_updater = [ + { + "source_dt": "Delivery Note Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "delivered_qty", + "target_parent_dt": "Sales Order", + "target_parent_field": "per_delivered", + "target_ref_field": "qty", + "source_field": "qty", + "percent_join_field": "against_sales_order", + "status_field": "delivery_status", + "keyword": "Delivered", + "second_source_dt": "Sales Invoice Item", + "second_source_field": "qty", + "second_join_field": "so_detail", + "overflow_type": "delivery", + "second_source_extra_cond": """ and exists(select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and update_stock = 1)""", }, { - 'source_dt': 'Delivery Note Item', - 'target_dt': 'Delivery Note Item', - 'join_field': 'dn_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Delivery Note', - 'target_parent_field': 'per_returned', - 'target_ref_field': 'stock_qty', - 'source_field': '-1 * stock_qty', - 'percent_join_field_parent': 'return_against' - } - ]) + "source_dt": "Delivery Note Item", + "target_dt": "Sales Invoice Item", + "join_field": "si_detail", + "target_field": "delivered_qty", + "target_parent_dt": "Sales Invoice", + "target_ref_field": "qty", + "source_field": "qty", + "percent_join_field": "against_sales_invoice", + "overflow_type": "delivery", + "no_allowance": 1, + }, + ] + if cint(self.is_return): + self.status_updater.extend( + [ + { + "source_dt": "Delivery Note Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "returned_qty", + "target_parent_dt": "Sales Order", + "source_field": "-1 * qty", + "second_source_dt": "Sales Invoice Item", + "second_source_field": "-1 * qty", + "second_join_field": "so_detail", + "extra_cond": """ and exists (select name from `tabDelivery Note` + where name=`tabDelivery Note Item`.parent and is_return=1)""", + "second_source_extra_cond": """ and exists (select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)""", + }, + { + "source_dt": "Delivery Note Item", + "target_dt": "Delivery Note Item", + "join_field": "dn_detail", + "target_field": "returned_qty", + "target_parent_dt": "Delivery Note", + "target_parent_field": "per_returned", + "target_ref_field": "stock_qty", + "source_field": "-1 * stock_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) def before_print(self, settings=None): def toggle_print_hide(meta, fieldname): @@ -93,7 +97,7 @@ class DeliveryNote(SellingController): item_meta = frappe.get_meta("Delivery Note Item") print_hide_fields = { "parent": ["grand_total", "rounded_total", "in_words", "currency", "total", "taxes"], - "items": ["rate", "amount", "discount_amount", "price_list_rate", "discount_percentage"] + "items": ["rate", "amount", "discount_amount", "price_list_rate", "discount_percentage"], } for key, fieldname in print_hide_fields.items(): @@ -103,16 +107,19 @@ class DeliveryNote(SellingController): super(DeliveryNote, self).before_print(settings) def set_actual_qty(self): - for d in self.get('items'): + for d in self.get("items"): if d.item_code and d.warehouse: - actual_qty = frappe.db.sql("""select actual_qty from `tabBin` - where item_code = %s and warehouse = %s""", (d.item_code, d.warehouse)) + actual_qty = frappe.db.sql( + """select actual_qty from `tabBin` + where item_code = %s and warehouse = %s""", + (d.item_code, d.warehouse), + ) d.actual_qty = actual_qty and flt(actual_qty[0][0]) or 0 def so_required(self): """check in manage account if sales order required or not""" - if frappe.db.get_value("Selling Settings", None, 'so_required') == 'Yes': - for d in self.get('items'): + if frappe.db.get_value("Selling Settings", None, "so_required") == "Yes": + for d in self.get("items"): if not d.against_sales_order: frappe.throw(_("Sales Order required for Item {0}").format(d.item_code)) @@ -129,71 +136,91 @@ class DeliveryNote(SellingController): self.validate_with_previous_doc() from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) - if self._action != 'submit' and not self.is_return: - set_batch_nos(self, 'warehouse', throw=True) - set_batch_nos(self, 'warehouse', throw=True, child_table="packed_items") + if self._action != "submit" and not self.is_return: + set_batch_nos(self, "warehouse", throw=True) + set_batch_nos(self, "warehouse", throw=True, child_table="packed_items") self.update_current_stock() - if not self.installation_status: self.installation_status = 'Not Installed' + 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({ - "Sales Order": { - "ref_dn_field": "against_sales_order", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Order Item": { - "ref_dn_field": "so_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - "Sales Invoice": { - "ref_dn_field": "against_sales_invoice", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Invoice Item": { - "ref_dn_field": "si_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - }) + super(DeliveryNote, self).validate_with_previous_doc( + { + "Sales Order": { + "ref_dn_field": "against_sales_order", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Order Item": { + "ref_dn_field": "so_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Sales Invoice": { + "ref_dn_field": "against_sales_invoice", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Invoice Item": { + "ref_dn_field": "si_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) - if cint(frappe.db.get_single_value('Selling Settings', 'maintain_same_sales_rate')) \ - and not self.is_return: - self.validate_rate_with_reference_doc([["Sales Order", "against_sales_order", "so_detail"], - ["Sales Invoice", "against_sales_invoice", "si_detail"]]) + if ( + cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) + and not self.is_return + ): + self.validate_rate_with_reference_doc( + [ + ["Sales Order", "against_sales_order", "so_detail"], + ["Sales Invoice", "against_sales_invoice", "si_detail"], + ] + ) def validate_proj_cust(self): """check for does customer belong to same project as entered..""" if self.project and self.customer: - res = frappe.db.sql("""select name from `tabProject` + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or - ifnull(customer,'')='')""", (self.project, self.customer)) + ifnull(customer,'')='')""", + (self.project, self.customer), + ) if not res: - frappe.throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) + frappe.throw( + _("Customer {0} does not belong to project {1}").format(self.customer, self.project) + ) def validate_warehouse(self): super(DeliveryNote, self).validate_warehouse() for d in self.get_item_list(): - if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1: + if not d["warehouse"] and frappe.db.get_value("Item", d["item_code"], "is_stock_item") == 1: frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) def update_current_stock(self): if self.get("_action") and self._action != "update_after_submit": - for d in self.get('items'): - d.actual_qty = frappe.db.get_value("Bin", {"item_code": d.item_code, - "warehouse": d.warehouse}, "actual_qty") + for d in self.get("items"): + d.actual_qty = frappe.db.get_value( + "Bin", {"item_code": d.item_code, "warehouse": d.warehouse}, "actual_qty" + ) - for d in self.get('packed_items'): - bin_qty = frappe.db.get_value("Bin", {"item_code": d.item_code, - "warehouse": d.warehouse}, ["actual_qty", "projected_qty"], as_dict=True) + for d in self.get("packed_items"): + bin_qty = frappe.db.get_value( + "Bin", + {"item_code": d.item_code, "warehouse": d.warehouse}, + ["actual_qty", "projected_qty"], + as_dict=True, + ) if bin_qty: d.actual_qty = flt(bin_qty.actual_qty) d.projected_qty = flt(bin_qty.projected_qty) @@ -202,7 +229,9 @@ class DeliveryNote(SellingController): self.validate_packed_qty() # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) # update delivered qty in sales order self.update_prevdoc_status() @@ -235,16 +264,20 @@ class DeliveryNote(SellingController): self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit extra_amount = 0 validate_against_credit_limit = False - bypass_credit_limit_check_at_sales_order = cint(frappe.db.get_value("Customer Credit Limit", - filters={'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - fieldname="bypass_credit_limit_check")) + bypass_credit_limit_check_at_sales_order = cint( + frappe.db.get_value( + "Customer Credit Limit", + filters={"parent": self.customer, "parenttype": "Customer", "company": self.company}, + fieldname="bypass_credit_limit_check", + ) + ) if bypass_credit_limit_check_at_sales_order: validate_against_credit_limit = True @@ -256,48 +289,58 @@ class DeliveryNote(SellingController): break if validate_against_credit_limit: - check_credit_limit(self.customer, self.company, - bypass_credit_limit_check_at_sales_order, extra_amount) + check_credit_limit( + self.customer, self.company, bypass_credit_limit_check_at_sales_order, extra_amount + ) def validate_packed_qty(self): """ - Validate that if packed qty exists, it should be equal to qty + Validate that if packed qty exists, it should be equal to qty """ - if not any(flt(d.get('packed_qty')) for d in self.get("items")): + if not any(flt(d.get("packed_qty")) for d in self.get("items")): return has_error = False for d in self.get("items"): - if flt(d.get('qty')) != flt(d.get('packed_qty')): - frappe.msgprint(_("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx)) + if flt(d.get("qty")) != flt(d.get("packed_qty")): + frappe.msgprint( + _("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx) + ) has_error = True if has_error: raise frappe.ValidationError def check_next_docstatus(self): - submit_rv = frappe.db.sql("""select t1.name + submit_rv = frappe.db.sql( + """select t1.name from `tabSales Invoice` t1,`tabSales Invoice Item` t2 where t1.name = t2.parent and t2.delivery_note = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_rv: frappe.throw(_("Sales Invoice {0} has already been submitted").format(submit_rv[0][0])) - submit_in = frappe.db.sql("""select t1.name + submit_in = frappe.db.sql( + """select t1.name from `tabInstallation Note` t1, `tabInstallation Note Item` t2 where t1.name = t2.parent and t2.prevdoc_docname = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_in: frappe.throw(_("Installation Note {0} has already been submitted").format(submit_in[0][0])) def cancel_packing_slips(self): """ - Cancel submitted packing slips related to this delivery note + Cancel submitted packing slips related to this delivery note """ - res = frappe.db.sql("""SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s - AND docstatus = 1""", self.name) + res = frappe.db.sql( + """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %s + AND docstatus = 1""", + self.name, + ) if res: for r in res: - ps = frappe.get_doc('Packing Slip', r[0]) + ps = frappe.get_doc("Packing Slip", r[0]) ps.cancel() frappe.msgprint(_("Packing Slip(s) cancelled")) @@ -310,7 +353,7 @@ class DeliveryNote(SellingController): updated_delivery_notes = [self.name] for d in self.get("items"): if d.si_detail and not d.so_detail: - d.db_set('billed_amt', d.amount, update_modified=update_modified) + d.db_set("billed_amt", d.amount, update_modified=update_modified) elif d.so_detail: updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) @@ -327,11 +370,16 @@ class DeliveryNote(SellingController): return_invoice.save() return_invoice.submit() - credit_note_link = frappe.utils.get_link_to_form('Sales Invoice', return_invoice.name) + credit_note_link = frappe.utils.get_link_to_form("Sales Invoice", return_invoice.name) frappe.msgprint(_("Credit Note {0} has been created automatically").format(credit_note_link)) except Exception: - frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again")) + frappe.throw( + _( + "Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again" + ) + ) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum @@ -340,25 +388,35 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") sum_amount = Sum(si_item.amount).as_("amount") - billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where( - (si_item.so_detail == so_detail) & - ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & - (si_item.docstatus == 1) - ).run() + billed_against_so = ( + frappe.qb.from_(si_item) + .select(sum_amount) + .where( + (si_item.so_detail == so_detail) + & ((si_item.dn_detail.isnull()) | (si_item.dn_detail == "")) + & (si_item.docstatus == 1) + ) + .run() + ) billed_against_so = billed_against_so and billed_against_so[0][0] or 0 # Get all Delivery Note Item rows against the Sales Order Item row dn = frappe.qb.DocType("Delivery Note").as_("dn") dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") - dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where( - (dn.name == dn_item.parent) & - (dn_item.so_detail == so_detail) & - (dn.docstatus == 1) & - (dn.is_return == 0) - ).orderby( - dn.posting_date, dn.posting_time, dn.name - ).run(as_dict=True) + dn_details = ( + frappe.qb.from_(dn) + .from_(dn_item) + .select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent) + .where( + (dn.name == dn_item.parent) + & (dn_item.so_detail == so_detail) + & (dn.docstatus == 1) + & (dn.is_return == 0) + ) + .orderby(dn.posting_date, dn.posting_time, dn.name) + .run(as_dict=True) + ) updated_dn = [] for dnd in dn_details: @@ -370,8 +428,11 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): billed_against_so -= billed_amt_agianst_dn else: # Get billed amount directly against Delivery Note - billed_amt_agianst_dn = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where dn_detail=%s and docstatus=1""", dnd.name) + billed_amt_agianst_dn = frappe.db.sql( + """select sum(amount) from `tabSales Invoice Item` + where dn_detail=%s and docstatus=1""", + dnd.name, + ) billed_amt_agianst_dn = billed_amt_agianst_dn and billed_amt_agianst_dn[0][0] or 0 # Distribute billed amount directly against SO between DNs based on FIFO @@ -384,50 +445,71 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True): billed_amt_agianst_dn += billed_against_so billed_against_so = 0 - frappe.db.set_value("Delivery Note Item", dnd.name, "billed_amt", billed_amt_agianst_dn, update_modified=update_modified) + frappe.db.set_value( + "Delivery Note Item", + dnd.name, + "billed_amt", + billed_amt_agianst_dn, + update_modified=update_modified, + ) updated_dn.append(dnd.parent) return updated_dn + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Shipments'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Shipments"), + } + ) return list_context + def get_invoiced_qty_map(delivery_note): """returns a map: {dn_detail: invoiced_qty}""" invoiced_qty_map = {} - for dn_detail, qty in frappe.db.sql("""select dn_detail, qty from `tabSales Invoice Item` - where delivery_note=%s and docstatus=1""", delivery_note): - if not invoiced_qty_map.get(dn_detail): - invoiced_qty_map[dn_detail] = 0 - invoiced_qty_map[dn_detail] += qty + for dn_detail, qty in frappe.db.sql( + """select dn_detail, qty from `tabSales Invoice Item` + where delivery_note=%s and docstatus=1""", + delivery_note, + ): + if not invoiced_qty_map.get(dn_detail): + invoiced_qty_map[dn_detail] = 0 + invoiced_qty_map[dn_detail] += qty return invoiced_qty_map + def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.dn_detail, abs(dn_item.qty) as qty + returned_qty_map = frappe._dict( + frappe.db.sql( + """select dn_item.dn_detail, abs(dn_item.qty) as qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn where dn.name = dn_item.parent and dn.docstatus = 1 and dn.is_return = 1 and dn.return_against = %s - """, delivery_note)) + """, + delivery_note, + ) + ) return returned_qty_map + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): - doc = frappe.get_doc('Delivery Note', source_name) + doc = frappe.get_doc("Delivery Note", source_name) to_make_invoice_qty_map = {} returned_qty_map = get_returned_qty_map(source_name) @@ -444,20 +526,21 @@ def make_sales_invoice(source_name, target_doc=None): # set company address if source.company_address: - target.update({'company_address': source.company_address}) + target.update({"company_address": source.company_address}) else: # set company address target.update(get_company_address(target.company)) if target.company_address: - target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) + target.update(get_fetch_values("Sales Invoice", "company_address", target.company_address)) def update_item(source_doc, target_doc, source_parent): target_doc.qty = to_make_invoice_qty_map[source_doc.name] if source_doc.serial_no and source_parent.per_billed > 0 and not source_parent.is_return: - target_doc.serial_no = get_delivery_note_serial_no(source_doc.item_code, - target_doc.qty, source_parent.name) + target_doc.serial_no = get_delivery_note_serial_no( + source_doc.item_code, target_doc.qty, source_parent.name + ) def get_pending_qty(item_row): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) @@ -479,50 +562,52 @@ def make_sales_invoice(source_name, target_doc=None): return pending_qty - doc = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Sales Invoice", - "field_map": { - "is_return": "is_return" + doc = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Sales Invoice", + "field_map": {"is_return": "is_return"}, + "validation": {"docstatus": ["=", 1]}, }, - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Sales Invoice Item", - "field_map": { - "name": "dn_detail", - "parent": "delivery_note", - "so_detail": "so_detail", - "against_sales_order": "sales_order", - "serial_no": "serial_no", - "cost_center": "cost_center" + "Delivery Note Item": { + "doctype": "Sales Invoice Item", + "field_map": { + "name": "dn_detail", + "parent": "delivery_note", + "so_detail": "so_detail", + "against_sales_order": "sales_order", + "serial_no": "serial_no", + "cost_center": "cost_center", + }, + "postprocess": update_item, + "filter": lambda d: get_pending_qty(d) <= 0 + if not doc.get("is_return") + else get_pending_qty(d) > 0, }, - "postprocess": update_item, - "filter": lambda d: get_pending_qty(d) <= 0 if not doc.get("is_return") else get_pending_qty(d) > 0 - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "field_map": { - "incentives": "incentives" + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": { + "doctype": "Sales Team", + "field_map": {"incentives": "incentives"}, + "add_if_empty": True, }, - "add_if_empty": True - } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) - automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms')) + automatically_fetch_payment_terms = cint( + frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) if automatically_fetch_payment_terms: doc.set_payment_schedule() - doc.set_onload('ignore_price_list', True) + doc.set_onload("ignore_price_list", True) return doc + @frappe.whitelist() def make_delivery_trip(source_name, target_doc=None): def update_stop_details(source_doc, target_doc, source_parent): @@ -538,96 +623,101 @@ def make_delivery_trip(source_name, target_doc=None): delivery_notes = [] - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Delivery Trip", - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Delivery Stop", - "field_map": { - "parent": "delivery_note" + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": {"doctype": "Delivery Trip", "validation": {"docstatus": ["=", 1]}}, + "Delivery Note Item": { + "doctype": "Delivery Stop", + "field_map": {"parent": "delivery_note"}, + "condition": lambda item: item.parent not in delivery_notes, + "postprocess": update_stop_details, }, - "condition": lambda item: item.parent not in delivery_notes, - "postprocess": update_stop_details - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_installation_note(source_name, target_doc=None): def update_item(obj, target, source_parent): target.qty = flt(obj.qty) - flt(obj.installed_qty) target.serial_no = obj.serial_no - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Installation Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Delivery Note Item": { - "doctype": "Installation Note Item", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": {"doctype": "Installation Note", "validation": {"docstatus": ["=", 1]}}, + "Delivery Note Item": { + "doctype": "Installation Note Item", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + }, + "postprocess": update_item, + "condition": lambda doc: doc.installed_qty < doc.qty, }, - "postprocess": update_item, - "condition": lambda doc: doc.installed_qty < doc.qty - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_packing_slip(source_name, target_doc=None): - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Packing Slip", - "field_map": { - "name": "delivery_note", - "letter_head": "letter_head" - }, - "validation": { - "docstatus": ["=", 0] + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Packing Slip", + "field_map": {"name": "delivery_note", "letter_head": "letter_head"}, + "validation": {"docstatus": ["=", 0]}, } - } - }, target_doc) + }, + target_doc, + ) return doclist + @frappe.whitelist() def make_shipment(source_name, target_doc=None): def postprocess(source, target): - user = frappe.db.get_value("User", frappe.session.user, ['email', 'full_name', 'phone', 'mobile_no'], as_dict=1) + user = frappe.db.get_value( + "User", frappe.session.user, ["email", "full_name", "phone", "mobile_no"], as_dict=1 + ) target.pickup_contact_email = user.email - pickup_contact_display = '{}'.format(user.full_name) + pickup_contact_display = "{}".format(user.full_name) if user: if user.email: - pickup_contact_display += '
    ' + user.email + pickup_contact_display += "
    " + user.email if user.phone: - pickup_contact_display += '
    ' + user.phone + pickup_contact_display += "
    " + user.phone if user.mobile_no and not user.phone: - pickup_contact_display += '
    ' + user.mobile_no + pickup_contact_display += "
    " + user.mobile_no target.pickup_contact = pickup_contact_display # As we are using session user details in the pickup_contact then pickup_contact_person will be session user target.pickup_contact_person = frappe.session.user - contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) - delivery_contact_display = '{}'.format(source.contact_display) + contact = frappe.db.get_value( + "Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1 + ) + delivery_contact_display = "{}".format(source.contact_display) if contact: if contact.email_id: - delivery_contact_display += '
    ' + contact.email_id + delivery_contact_display += "
    " + contact.email_id if contact.phone: - delivery_contact_display += '
    ' + contact.phone + delivery_contact_display += "
    " + contact.phone if contact.mobile_no and not contact.phone: - delivery_contact_display += '
    ' + contact.mobile_no + delivery_contact_display += "
    " + contact.mobile_no target.delivery_contact = delivery_contact_display if source.shipping_address_name: @@ -637,38 +727,44 @@ def make_shipment(source_name, target_doc=None): target.delivery_address_name = source.customer_address target.delivery_address = source.address_display - doclist = get_mapped_doc("Delivery Note", source_name, { - "Delivery Note": { - "doctype": "Shipment", - "field_map": { - "grand_total": "value_of_goods", - "company": "pickup_company", - "company_address": "pickup_address_name", - "company_address_display": "pickup_address", - "customer": "delivery_customer", - "contact_person": "delivery_contact_name", - "contact_email": "delivery_contact_email" + doclist = get_mapped_doc( + "Delivery Note", + source_name, + { + "Delivery Note": { + "doctype": "Shipment", + "field_map": { + "grand_total": "value_of_goods", + "company": "pickup_company", + "company_address": "pickup_address_name", + "company_address_display": "pickup_address", + "customer": "delivery_customer", + "contact_person": "delivery_contact_name", + "contact_email": "delivery_contact_email", + }, + "validation": {"docstatus": ["=", 1]}, + }, + "Delivery Note Item": { + "doctype": "Shipment Delivery Note", + "field_map": { + "name": "prevdoc_detail_docname", + "parent": "prevdoc_docname", + "parenttype": "prevdoc_doctype", + "base_amount": "grand_total", + }, }, - "validation": { - "docstatus": ["=", 1] - } }, - "Delivery Note Item": { - "doctype": "Shipment Delivery Note", - "field_map": { - "name": "prevdoc_detail_docname", - "parent": "prevdoc_docname", - "parenttype": "prevdoc_doctype", - "base_amount": "grand_total" - } - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Delivery Note", source_name, target_doc) @@ -677,10 +773,12 @@ def update_delivery_note_status(docname, status): dn = frappe.get_doc("Delivery Note", docname) dn.update_status(status) + @frappe.whitelist() def make_inter_company_purchase_receipt(source_name, target_doc=None): return make_inter_company_transaction("Delivery Note", source_name, target_doc) + def make_inter_company_transaction(doctype, source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( get_inter_company_details, @@ -690,16 +788,16 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): validate_inter_company_transaction, ) - if doctype == 'Delivery Note': + if doctype == "Delivery Note": source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Receipt" - source_document_warehouse_field = 'target_warehouse' - target_document_warehouse_field = 'from_warehouse' + source_document_warehouse_field = "target_warehouse" + target_document_warehouse_field = "from_warehouse" else: source_doc = frappe.get_doc(doctype, source_name) - target_doctype = 'Delivery Note' - source_document_warehouse_field = 'from_warehouse' - target_document_warehouse_field = 'target_warehouse' + target_doctype = "Delivery Note" + source_document_warehouse_field = "from_warehouse" + target_document_warehouse_field = "target_warehouse" validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -708,18 +806,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target.run_method("set_missing_values") set_purchase_references(target) - if target.doctype == 'Purchase Receipt': - master_doctype = 'Purchase Taxes and Charges Template' + if target.doctype == "Purchase Receipt": + master_doctype = "Purchase Taxes and Charges Template" else: - master_doctype = 'Sales Taxes and Charges Template' + master_doctype = "Sales Taxes and Charges Template" - if not target.get('taxes') and target.get('taxes_and_charges'): - for tax in get_taxes_and_charges(master_doctype, target.get('taxes_and_charges')): - target.append('taxes', tax) + if not target.get("taxes") and target.get("taxes_and_charges"): + for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): + target.append("taxes", tax) def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name - if target_doc.doctype == 'Purchase Receipt': + if target_doc.doctype == "Purchase Receipt": target_doc.company = details.get("company") target_doc.supplier = details.get("party") target_doc.buying_price_list = source_doc.selling_price_list @@ -727,12 +825,20 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.inter_company_reference = source_doc.name # Invert the address on target doc creation - update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) - update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + ) - update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.supplier_address, - company_address=target_doc.shipping_address) + update_taxes( + target_doc, + party=target_doc.supplier, + party_type="Supplier", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address, + ) else: target_doc.company = details.get("company") target_doc.customer = details.get("party") @@ -742,39 +848,52 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.inter_company_reference = source_doc.name # Invert the address on target doc creation - update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) - update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) - update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + update_address( + target_doc, "company_address", "company_address_display", source_doc.supplier_address + ) + update_address( + target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address + ) + update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address) - update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.customer_address, - company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + update_taxes( + target_doc, + party=target_doc.customer, + party_type="Customer", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.customer_address, + company_address=target_doc.company_address, + shipping_address_name=target_doc.shipping_address_name, + ) - doclist = get_mapped_doc(doctype, source_name, { - doctype: { - "doctype": target_doctype, - "postprocess": update_details, - "field_no_map": [ - "taxes_and_charges", - "set_warehouse" - ] - }, - doctype +" Item": { - "doctype": target_doctype + " Item", - "field_map": { - source_document_warehouse_field: target_document_warehouse_field, - 'name': 'delivery_note_item', - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' + doclist = get_mapped_doc( + doctype, + source_name, + { + doctype: { + "doctype": target_doctype, + "postprocess": update_details, + "field_no_map": ["taxes_and_charges", "set_warehouse"], }, - "field_no_map": [ - "warehouse" - ] - } - - }, target_doc, set_missing_values) + doctype + + " Item": { + "doctype": target_doctype + " Item", + "field_map": { + source_document_warehouse_field: target_document_warehouse_field, + "name": "delivery_note_item", + "batch_no": "batch_no", + "serial_no": "serial_no", + }, + "field_no_map": ["warehouse"], + }, + }, + target_doc, + set_missing_values, + ) return doclist + def on_doctype_update(): frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index 61d8de4a06d..fd44e9cee5c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -1,34 +1,21 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'delivery_note', - 'non_standard_fieldnames': { - 'Stock Entry': 'delivery_note_no', - 'Quality Inspection': 'reference_name', - 'Auto Repeat': 'reference_document', + "fieldname": "delivery_note", + "non_standard_fieldnames": { + "Stock Entry": "delivery_note_no", + "Quality Inspection": "reference_name", + "Auto Repeat": "reference_document", }, - 'internal_links': { - 'Sales Order': ['items', 'against_sales_order'], + "internal_links": { + "Sales Order": ["items", "against_sales_order"], }, - 'transactions': [ - { - 'label': _('Related'), - 'items': ['Sales Invoice', 'Packing Slip', 'Delivery Trip'] - }, - { - 'label': _('Reference'), - 'items': ['Sales Order', 'Shipment', 'Quality Inspection'] - }, - { - 'label': _('Returns'), - 'items': ['Stock Entry'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]}, + {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, + {"label": _("Returns"), "items": ["Stock Entry"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 82f4e7dd294..b5a45578c6d 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import json import frappe @@ -54,21 +53,28 @@ class TestDeliveryNote(FrappeTestCase): self.assertRaises(frappe.ValidationError, frappe.get_doc(si).insert) def test_delivery_note_no_gl_entry(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') + company = frappe.db.get_value("Warehouse", "_Test Warehouse - _TC", "company") make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100) - stock_queue = json.loads(get_previous_sle({ - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "posting_date": nowdate(), - "posting_time": nowtime() - }).stock_queue or "[]") + stock_queue = json.loads( + get_previous_sle( + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "posting_date": nowdate(), + "posting_time": nowtime(), + } + ).stock_queue + or "[]" + ) dn = create_delivery_note() - sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name}) + sle = frappe.get_doc( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name} + ) - self.assertEqual(sle.stock_value_difference, flt(-1*stock_queue[0][1], 2)) + self.assertEqual(sle.stock_value_difference, flt(-1 * stock_queue[0][1], 2)) self.assertFalse(get_gl_entries("Delivery Note", dn.name)) @@ -123,24 +129,43 @@ class TestDeliveryNote(FrappeTestCase): # set_perpetual_inventory(0, company) def test_delivery_note_gl_entry_packing_item(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=10, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - TCP1", qty=10, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - TCP1", qty=10, basic_rate=100 + ) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") prev_bal = get_balance_on(stock_in_hand_account) - dn = create_delivery_note(item_code="_Test Product Bundle Item", company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + ) - stock_value_diff_rm1 = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, - "stock_value_difference")) + stock_value_diff_rm1 = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + ) - stock_value_diff_rm2 = abs(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item Home Desktop 100"}, "stock_value_difference")) + stock_value_diff_rm2 = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item Home Desktop 100", + }, + "stock_value_difference", + ) + ) stock_value_diff = stock_value_diff_rm1 + stock_value_diff_rm2 @@ -149,7 +174,7 @@ class TestDeliveryNote(FrappeTestCase): expected_values = { stock_in_hand_account: [0.0, stock_value_diff], - "Cost of Goods Sold - TCP1": [stock_value_diff, 0.0] + "Cost of Goods Sold - TCP1": [stock_value_diff, 0.0], } for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) @@ -166,10 +191,7 @@ class TestDeliveryNote(FrappeTestCase): dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) si = make_sales_invoice(dn.name) si.insert(ignore_permissions=True) @@ -177,17 +199,18 @@ class TestDeliveryNote(FrappeTestCase): dn.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "" - }) + self.check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""} + ) def test_serialized_partial_sales_invoice(self): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no) - serial_no = '\n'.join(serial_no) + serial_no = "\n".join(serial_no) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=2, serial_no=serial_no + ) si = make_sales_invoice(dn.name) si.items[0].qty = 1 @@ -200,15 +223,19 @@ class TestDeliveryNote(FrappeTestCase): def test_serialize_status(self): from frappe.model.naming import make_autoname - serial_no = frappe.get_doc({ - "doctype": "Serial No", - "item_code": "_Test Serialized Item With Series", - "serial_no": make_autoname("SR", "Serial No") - }) + + serial_no = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": "_Test Serialized Item With Series", + "serial_no": make_autoname("SR", "Serial No"), + } + ) serial_no.save() - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - serial_no=serial_no.name, do_not_submit=True) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", serial_no=serial_no.name, do_not_submit=True + ) self.assertRaises(SerialNoWarehouseError, dn.submit) @@ -218,26 +245,46 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(cstr(serial_no.get(field)), value) def test_sales_return_for_non_bundled_items_partial(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) actual_qty_0 = get_qty_after_transaction(warehouse="Stores - TCP1") - dn = create_delivery_note(qty=5, rate=500, warehouse="Stores - TCP1", company=company, - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) actual_qty_1 = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty_0 - 5, actual_qty_1) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 5 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 5 + ) # return entry - dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", - cost_center="Main - TCP1", do_not_submit=1) + dn1 = create_delivery_note( + is_return=1, + return_against=dn.name, + qty=-2, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_submit=1, + ) dn1.items[0].dn_detail = dn.items[0].name dn1.submit() @@ -245,15 +292,20 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(actual_qty_1 + 2, actual_qty_2) - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) stock_in_hand_account = get_inventory_account(company, dn1.items[0].warehouse) - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) @@ -267,6 +319,7 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(dn.per_returned, 40) from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_dn_2 = make_return_doc("Delivery Note", dn.name) # Check if unreturned amount is mapped in 2nd return @@ -281,7 +334,7 @@ class TestDeliveryNote(FrappeTestCase): # DN should be completed on billing all unreturned amount self.assertEqual(dn.items[0].billed_amt, 1500) self.assertEqual(dn.per_billed, 100) - self.assertEqual(dn.status, 'Completed') + self.assertEqual(dn.status, "Completed") si.load_from_db() si.cancel() @@ -293,19 +346,35 @@ class TestDeliveryNote(FrappeTestCase): dn.cancel() def test_sales_return_for_non_bundled_items_full(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - make_item("Box", {'is_stock_item': 1}) + make_item("Box", {"is_stock_item": 1}) make_stock_entry(item_code="Box", target="Stores - TCP1", qty=10, basic_rate=100) - dn = create_delivery_note(item_code="Box", qty=5, rate=500, warehouse="Stores - TCP1", company=company, - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="Box", + qty=5, + rate=500, + warehouse="Stores - TCP1", + company=company, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - #return entry - dn1 = create_delivery_note(item_code="Box", is_return=1, return_against=dn.name, qty=-5, rate=500, - company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", - cost_center="Main - TCP1", do_not_submit=1) + # return entry + dn1 = create_delivery_note( + item_code="Box", + is_return=1, + return_against=dn.name, + qty=-5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_submit=1, + ) dn1.items[0].dn_detail = dn.items[0].name dn1.submit() @@ -317,93 +386,157 @@ class TestDeliveryNote(FrappeTestCase): # Check if Original DN updated self.assertEqual(dn.items[0].returned_qty, 5) self.assertEqual(dn.per_returned, 100) - self.assertEqual(dn.status, 'Return Issued') + self.assertEqual(dn.status, "Return Issued") def test_return_single_item_from_bundled_items(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - create_stock_reconciliation(item_code="_Test Item", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") + create_stock_reconciliation( + item_code="_Test Item", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) - dn = create_delivery_note(item_code="_Test Product Bundle Item", qty=5, rate=500, - company=company, warehouse="Stores - TCP1", - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + qty=5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # Qty after delivery actual_qty_1 = get_qty_after_transaction(warehouse="Stores - TCP1") - self.assertEqual(actual_qty_1, 25) + self.assertEqual(actual_qty_1, 25) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name, "item_code": "_Test Item"}, "stock_value_difference") / 25 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + / 25 + ) # return 'test item' from packed items - dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-10, rate=500, - company=company, warehouse="Stores - TCP1", - expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn1 = create_delivery_note( + is_return=1, + return_against=dn.name, + qty=-10, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty_2 = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty_2, 35) # Check incoming rate for return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) stock_in_hand_account = get_inventory_account(company, dn1.items[0].warehouse) # Check gl entry for warehouse - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) - def test_return_entire_bundled_items(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - create_stock_reconciliation(item_code="_Test Item", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", - warehouse="Stores - TCP1", qty=50, rate=100, - company=company, expense_account = "Stock Adjustment - TCP1") + create_stock_reconciliation( + item_code="_Test Item", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + warehouse="Stores - TCP1", + qty=50, + rate=100, + company=company, + expense_account="Stock Adjustment - TCP1", + ) actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 50) - dn = create_delivery_note(item_code="_Test Product Bundle Item", - qty=5, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn = create_delivery_note( + item_code="_Test Product Bundle Item", + qty=5, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 25) # return bundled item - dn1 = create_delivery_note(item_code='_Test Product Bundle Item', is_return=1, - return_against=dn.name, qty=-2, rate=500, company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1") + dn1 = create_delivery_note( + item_code="_Test Product Bundle Item", + is_return=1, + return_against=dn.name, + qty=-2, + rate=500, + company=company, + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) # qty after return actual_qty = get_qty_after_transaction(warehouse="Stores - TCP1") self.assertEqual(actual_qty, 35) # Check incoming rate for return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 100) - stock_in_hand_account = get_inventory_account('_Test Company', dn1.items[0].warehouse) + stock_in_hand_account = get_inventory_account("_Test Company", dn1.items[0].warehouse) # Check gl entry for warehouse - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, 1400) @@ -411,69 +544,88 @@ class TestDeliveryNote(FrappeTestCase): se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] - dn = create_delivery_note(item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", rate=500, serial_no=serial_no + ) - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) # return entry - dn1 = create_delivery_note(item_code="_Test Serialized Item With Series", - is_return=1, return_against=dn.name, qty=-1, rate=500, serial_no=serial_no) + dn1 = create_delivery_note( + item_code="_Test Serialized Item With Series", + is_return=1, + return_against=dn.name, + qty=-1, + rate=500, + serial_no=serial_no, + ) - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "" - }) + self.check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "delivery_document_no": ""} + ) dn1.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "", - "delivery_document_no": dn.name - }) + self.check_serial_no_values(serial_no, {"warehouse": "", "delivery_document_no": dn.name}) dn.cancel() - self.check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "delivery_document_no": "", - "purchase_document_no": se.name - }) + self.check_serial_no_values( + serial_no, + { + "warehouse": "_Test Warehouse - _TC", + "delivery_document_no": "", + "purchase_document_no": se.name, + }, + ) def test_delivery_of_bundled_items_to_target_warehouse(self): from erpnext.selling.doctype.customer.test_customer import create_internal_customer - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") customer_name = create_internal_customer( customer_name="_Test Internal Customer 2", represents_company="_Test Company with perpetual inventory", - allowed_to_interact_with="_Test Company with perpetual inventory" + allowed_to_interact_with="_Test Company with perpetual inventory", ) set_valuation_method("_Test Item", "FIFO") set_valuation_method("_Test Item Home Desktop 100", "FIFO") - target_warehouse = get_warehouse(company=company, abbr="TCP1", - warehouse_name="_Test Customer Warehouse").name + target_warehouse = get_warehouse( + company=company, abbr="TCP1", warehouse_name="_Test Customer Warehouse" + ).name for warehouse in ("Stores - TCP1", target_warehouse): - create_stock_reconciliation(item_code="_Test Item", warehouse=warehouse, company = company, - expense_account = "Stock Adjustment - TCP1", qty=500, rate=100) - create_stock_reconciliation(item_code="_Test Item Home Desktop 100", company = company, - expense_account = "Stock Adjustment - TCP1", warehouse=warehouse, qty=500, rate=100) + create_stock_reconciliation( + item_code="_Test Item", + warehouse=warehouse, + company=company, + expense_account="Stock Adjustment - TCP1", + qty=500, + rate=100, + ) + create_stock_reconciliation( + item_code="_Test Item Home Desktop 100", + company=company, + expense_account="Stock Adjustment - TCP1", + warehouse=warehouse, + qty=500, + rate=100, + ) dn = create_delivery_note( item_code="_Test Product Bundle Item", company="_Test Company with perpetual inventory", customer=customer_name, - cost_center = 'Main - TCP1', - expense_account = "Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", do_not_submit=True, - qty=5, rate=500, + qty=5, + rate=500, warehouse="Stores - TCP1", - target_warehouse=target_warehouse) + target_warehouse=target_warehouse, + ) dn.submit() @@ -485,16 +637,28 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(actual_qty_at_target, 525) # stock value diff for source warehouse for "_Test Item" - stock_value_difference = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item", "warehouse": "Stores - TCP1"}, - "stock_value_difference") + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + }, + "stock_value_difference", + ) # stock value diff for target warehouse - stock_value_difference1 = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Delivery Note", "voucher_no": dn.name, - "item_code": "_Test Item", "warehouse": target_warehouse}, - "stock_value_difference") + stock_value_difference1 = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": dn.name, + "item_code": "_Test Item", + "warehouse": target_warehouse, + }, + "stock_value_difference", + ) self.assertEqual(abs(stock_value_difference), stock_value_difference1) @@ -502,13 +666,18 @@ class TestDeliveryNote(FrappeTestCase): gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) - stock_value_difference = abs(frappe.db.sql("""select sum(stock_value_difference) + stock_value_difference = abs( + frappe.db.sql( + """select sum(stock_value_difference) from `tabStock Ledger Entry` where voucher_type='Delivery Note' and voucher_no=%s - and warehouse='Stores - TCP1'""", dn.name)[0][0]) + and warehouse='Stores - TCP1'""", + dn.name, + )[0][0] + ) expected_values = { "Stock In Hand - TCP1": [0.0, stock_value_difference], - target_warehouse: [stock_value_difference, 0.0] + target_warehouse: [stock_value_difference, 0.0], } for i, gle in enumerate(gl_entries): self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) @@ -521,8 +690,13 @@ class TestDeliveryNote(FrappeTestCase): make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', - cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + do_not_submit=True, + ) dn.submit() @@ -599,6 +773,7 @@ class TestDeliveryNote(FrappeTestCase): from erpnext.selling.doctype.sales_order.sales_order import ( make_sales_invoice as make_sales_invoice_from_so, ) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) so = make_sales_order() @@ -672,28 +847,33 @@ class TestDeliveryNote(FrappeTestCase): def test_delivery_note_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center - cost_center = "_Test Cost Center for BS Account - TCP1" - create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company with perpetual inventory") - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + cost_center = "_Test Cost Center for BS Account - TCP1" + create_cost_center( + cost_center_name="_Test Cost Center for BS Account", + company="_Test Company with perpetual inventory", + ) + + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', expense_account = "Cost of Goods Sold - TCP1", cost_center=cost_center) + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center=cost_center, + ) gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) expected_values = { - "Cost of Goods Sold - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -701,29 +881,30 @@ class TestDeliveryNote(FrappeTestCase): def test_delivery_note_cost_center_with_balance_sheet_account(self): cost_center = "Main - TCP1" - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", - do_not_submit=1) + stock_in_hand_account = get_inventory_account("_Test Company with perpetual inventory") + dn = create_delivery_note( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + do_not_submit=1, + ) - dn.get('items')[0].cost_center = None + dn.get("items")[0].cost_center = None dn.submit() gl_entries = get_gl_entries("Delivery Note", dn.name) self.assertTrue(gl_entries) expected_values = { - "Cost of Goods Sold - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Cost of Goods Sold - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -751,15 +932,18 @@ class TestDeliveryNote(FrappeTestCase): from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice dn = create_delivery_note(qty=8, do_not_submit=True) - dn.append("items", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": 100, - "conversion_factor": 1.0, - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + dn.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": 100, + "conversion_factor": 1.0, + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) dn.submit() si1 = make_sales_invoice(dn.name) @@ -776,14 +960,21 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(si2.items[0].qty, 2) self.assertEqual(si2.items[1].qty, 1) - def test_delivery_note_bundle_with_batched_item(self): batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) - batched_item = make_item("_Test Batched Item", - {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TESTBATCH.#####"} - ) + batched_item = make_item( + "_Test Batched Item", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TESTBATCH.#####", + }, + ) make_product_bundle(parent=batched_bundle.name, items=[batched_item.name]) - make_stock_entry(item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42) + make_stock_entry( + item_code=batched_item.name, target="_Test Warehouse - _TC", qty=10, basic_rate=42 + ) try: dn = create_delivery_note(item_code=batched_bundle.name, qty=1) @@ -792,7 +983,9 @@ class TestDeliveryNote(FrappeTestCase): self.fail("Batch numbers not getting added to bundled items in DN.") raise e - self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + self.assertTrue( + "TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item" + ) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( @@ -804,13 +997,13 @@ class TestDeliveryNote(FrappeTestCase): so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() - so.payment_terms_template = 'Test Receivable Template' + so.payment_terms_template = "Test Receivable Template" so.submit() dn = create_dn_against_so(so.name, delivered_qty=10) si = create_sales_invoice(qty=10, do_not_save=1) - si.items[0].delivery_note= dn.name + si.items[0].delivery_note = dn.name si.items[0].dn_detail = dn.items[0].name si.items[0].sales_order = so.name si.items[0].so_detail = so.items[0].name @@ -823,6 +1016,7 @@ class TestDeliveryNote(FrappeTestCase): automatically_fetch_payment_terms(enable=0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") args = frappe._dict(args) @@ -836,18 +1030,21 @@ def create_delivery_note(**args): dn.is_return = args.is_return dn.return_against = args.return_against - dn.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "rate": args.rate if args.get("rate") is not None else 100, - "conversion_factor": 1.0, - "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, - "expense_account": args.expense_account or "Cost of Goods Sold - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, - "target_warehouse": args.target_warehouse - }) + dn.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "rate": args.rate if args.get("rate") is not None else 100, + "conversion_factor": 1.0, + "allow_zero_valuation_rate": args.allow_zero_valuation_rate or 1, + "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + "target_warehouse": args.target_warehouse, + }, + ) if not args.do_not_save: dn.insert() @@ -855,4 +1052,5 @@ def create_delivery_note(**args): dn.submit() return dn + test_dependencies = ["Product Bundle"] diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index c749b2e6706..73b250db54b 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -17,9 +17,12 @@ class DeliveryTrip(Document): super(DeliveryTrip, self).__init__(*args, **kwargs) # Google Maps returns distances in meters by default - self.default_distance_uom = frappe.db.get_single_value("Global Defaults", "default_distance_unit") or "Meter" - self.uom_conversion_factor = frappe.db.get_value("UOM Conversion Factor", - {"from_uom": "Meter", "to_uom": self.default_distance_uom}, "value") + self.default_distance_uom = ( + frappe.db.get_single_value("Global Defaults", "default_distance_unit") or "Meter" + ) + self.uom_conversion_factor = frappe.db.get_value( + "UOM Conversion Factor", {"from_uom": "Meter", "to_uom": self.default_distance_uom}, "value" + ) def validate(self): self.validate_stop_addresses() @@ -41,11 +44,7 @@ class DeliveryTrip(Document): stop.customer_address = get_address_display(frappe.get_doc("Address", stop.address).as_dict()) def update_status(self): - status = { - 0: "Draft", - 1: "Scheduled", - 2: "Cancelled" - }[self.docstatus] + status = {0: "Draft", 1: "Scheduled", 2: "Cancelled"}[self.docstatus] if self.docstatus == 1: visited_stops = [stop.visited for stop in self.delivery_stops] @@ -63,17 +62,19 @@ class DeliveryTrip(Document): are removed. Args: - delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. + delete (bool, optional): Defaults to `False`. `True` if driver details need to be emptied, else `False`. """ - delivery_notes = list(set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note)) + delivery_notes = list( + set(stop.delivery_note for stop in self.delivery_stops if stop.delivery_note) + ) update_fields = { "driver": self.driver, "driver_name": self.driver_name, "vehicle_no": self.vehicle, "lr_no": self.name, - "lr_date": self.departure_time + "lr_date": self.departure_time, } for delivery_note in delivery_notes: @@ -97,7 +98,7 @@ class DeliveryTrip(Document): on the optimized order, before estimating the arrival times. Args: - optimize (bool): True if route needs to be optimized, else False + optimize (bool): True if route needs to be optimized, else False """ departure_datetime = get_datetime(self.departure_time) @@ -134,8 +135,9 @@ class DeliveryTrip(Document): # Include last leg in the final distance calculation self.uom = self.default_distance_uom - total_distance = sum(leg.get("distance", {}).get("value", 0.0) - for leg in directions.get("legs")) # in meters + total_distance = sum( + leg.get("distance", {}).get("value", 0.0) for leg in directions.get("legs") + ) # in meters self.total_distance = total_distance * self.uom_conversion_factor else: idx += len(route) - 1 @@ -149,10 +151,10 @@ class DeliveryTrip(Document): split into sublists at the specified lock position(s). Args: - optimize (bool): `True` if route needs to be optimized, else `False` + optimize (bool): `True` if route needs to be optimized, else `False` Returns: - (list of list of str): List of address routes split at locks, if optimize is `True` + (list of list of str): List of address routes split at locks, if optimize is `True` """ if not self.driver_address: frappe.throw(_("Cannot Calculate Arrival Time as Driver Address is Missing.")) @@ -186,8 +188,8 @@ class DeliveryTrip(Document): for vehicle routing problems. Args: - optimized_order (list of int): The index-based optimized order of the route - start (int): The index at which to start the rearrangement + optimized_order (list of int): The index-based optimized order of the route + start (int): The index at which to start the rearrangement """ stops_order = [] @@ -200,7 +202,7 @@ class DeliveryTrip(Document): self.delivery_stops[old_idx].idx = new_idx stops_order.append(self.delivery_stops[old_idx]) - self.delivery_stops[start:start + len(stops_order)] = stops_order + self.delivery_stops[start : start + len(stops_order)] = stops_order def get_directions(self, route, optimize): """ @@ -212,11 +214,11 @@ class DeliveryTrip(Document): but it only works for routes without any waypoints. Args: - route (list of str): Route addresses (origin -> waypoint(s), if any -> destination) - optimize (bool): `True` if route needs to be optimized, else `False` + route (list of str): Route addresses (origin -> waypoint(s), if any -> destination) + optimize (bool): `True` if route needs to be optimized, else `False` Returns: - (dict): Route legs and, if `optimize` is `True`, optimized waypoint order + (dict): Route legs and, if `optimize` is `True`, optimized waypoint order """ if not frappe.db.get_single_value("Google Settings", "api_key"): frappe.throw(_("Enter API key in Google Settings.")) @@ -231,8 +233,8 @@ class DeliveryTrip(Document): directions_data = { "origin": route[0], "destination": route[-1], - "waypoints": route[1: -1], - "optimize_waypoints": optimize + "waypoints": route[1:-1], + "optimize_waypoints": optimize, } try: @@ -243,7 +245,6 @@ class DeliveryTrip(Document): return directions[0] if directions else False - @frappe.whitelist() def get_contact_and_address(name): out = frappe._dict() @@ -265,7 +266,10 @@ def get_default_contact(out, name): dl.link_doctype="Customer" AND dl.link_name=%s AND dl.parenttype = "Contact" - """, (name), as_dict=1) + """, + (name), + as_dict=1, + ) if contact_persons: for out.contact_person in contact_persons: @@ -288,7 +292,10 @@ def get_default_address(out, name): dl.link_doctype="Customer" AND dl.link_name=%s AND dl.parenttype = "Address" - """, (name), as_dict=1) + """, + (name), + as_dict=1, + ) if shipping_addresses: for out.shipping_address in shipping_addresses: @@ -303,16 +310,18 @@ def get_default_address(out, name): @frappe.whitelist() def get_contact_display(contact): contact_info = frappe.db.get_value( - "Contact", contact, - ["first_name", "last_name", "phone", "mobile_no"], - as_dict=1) + "Contact", contact, ["first_name", "last_name", "phone", "mobile_no"], as_dict=1 + ) - contact_info.html = """ %(first_name)s %(last_name)s
    %(phone)s
    %(mobile_no)s""" % { - "first_name": contact_info.first_name, - "last_name": contact_info.last_name or "", - "phone": contact_info.phone or "", - "mobile_no": contact_info.mobile_no or "" - } + contact_info.html = ( + """ %(first_name)s %(last_name)s
    %(phone)s
    %(mobile_no)s""" + % { + "first_name": contact_info.first_name, + "last_name": contact_info.last_name or "", + "phone": contact_info.phone or "", + "mobile_no": contact_info.mobile_no or "", + } + ) return contact_info.html @@ -322,19 +331,19 @@ def sanitize_address(address): Remove HTML breaks in a given address Args: - address (str): Address to be sanitized + address (str): Address to be sanitized Returns: - (str): Sanitized address + (str): Sanitized address """ if not address: return - address = address.split('
    ') + address = address.split("
    ") # Only get the first 3 blocks of the address - return ', '.join(address[:3]) + return ", ".join(address[:3]) @frappe.whitelist() @@ -349,11 +358,15 @@ def notify_customers(delivery_trip): email_recipients = [] for stop in delivery_trip.delivery_stops: - contact_info = frappe.db.get_value("Contact", stop.contact, ["first_name", "last_name", "email_id"], as_dict=1) + contact_info = frappe.db.get_value( + "Contact", stop.contact, ["first_name", "last_name", "email_id"], as_dict=1 + ) context.update({"items": []}) if stop.delivery_note: - items = frappe.get_all("Delivery Note Item", filters={"parent": stop.delivery_note, "docstatus": 1}, fields=["*"]) + items = frappe.get_all( + "Delivery Note Item", filters={"parent": stop.delivery_note, "docstatus": 1}, fields=["*"] + ) context.update({"items": items}) if contact_info and contact_info.email_id: @@ -363,10 +376,12 @@ def notify_customers(delivery_trip): dispatch_template_name = frappe.db.get_single_value("Delivery Settings", "dispatch_template") dispatch_template = frappe.get_doc("Email Template", dispatch_template_name) - frappe.sendmail(recipients=contact_info.email_id, + frappe.sendmail( + recipients=contact_info.email_id, subject=dispatch_template.subject, message=frappe.render_template(dispatch_template.response, context), - attachments=get_attachments(stop)) + attachments=get_attachments(stop), + ) stop.db_set("email_sent_to", contact_info.email_id) email_recipients.append(contact_info.email_id) @@ -379,29 +394,37 @@ def notify_customers(delivery_trip): def get_attachments(delivery_stop): - if not (frappe.db.get_single_value("Delivery Settings", "send_with_attachment") and delivery_stop.delivery_note): + if not ( + frappe.db.get_single_value("Delivery Settings", "send_with_attachment") + and delivery_stop.delivery_note + ): return [] dispatch_attachment = frappe.db.get_single_value("Delivery Settings", "dispatch_attachment") - attachments = frappe.attach_print("Delivery Note", delivery_stop.delivery_note, - file_name="Delivery Note", print_format=dispatch_attachment) + attachments = frappe.attach_print( + "Delivery Note", + delivery_stop.delivery_note, + file_name="Delivery Note", + print_format=dispatch_attachment, + ) return [attachments] + @frappe.whitelist() def get_driver_email(driver): employee = frappe.db.get_value("Driver", driver, "employee") email = frappe.db.get_value("Employee", employee, "prefered_email") return {"email": email} + @frappe.whitelist() def make_expense_claim(source_name, target_doc=None): - doc = get_mapped_doc("Delivery Trip", source_name, - {"Delivery Trip": { - "doctype": "Expense Claim", - "field_map": { - "name" : "delivery_trip" - } - }}, target_doc) + doc = get_mapped_doc( + "Delivery Trip", + source_name, + {"Delivery Trip": {"doctype": "Expense Claim", "field_map": {"name": "delivery_trip"}}}, + target_doc, + ) return doc diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index dcdff4a0f1e..555361afbcd 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -106,23 +106,21 @@ class TestDeliveryTrip(FrappeTestCase): self.delivery_trip.save() self.assertEqual(self.delivery_trip.status, "Completed") + def create_address(driver): if not frappe.db.exists("Address", {"address_title": "_Test Address for Driver"}): - address = frappe.get_doc({ - "doctype": "Address", - "address_title": "_Test Address for Driver", - "address_type": "Office", - "address_line1": "Station Road", - "city": "_Test City", - "state": "Test State", - "country": "India", - "links":[ - { - "link_doctype": "Driver", - "link_name": driver.name - } - ] - }).insert(ignore_permissions=True) + address = frappe.get_doc( + { + "doctype": "Address", + "address_title": "_Test Address for Driver", + "address_type": "Office", + "address_line1": "Station Road", + "city": "_Test City", + "state": "Test State", + "country": "India", + "links": [{"link_doctype": "Driver", "link_name": driver.name}], + } + ).insert(ignore_permissions=True) frappe.db.set_value("Driver", driver.name, "address", address.name) @@ -130,49 +128,57 @@ def create_address(driver): return frappe.get_doc("Address", {"address_title": "_Test Address for Driver"}) + def create_driver(): if not frappe.db.exists("Driver", {"full_name": "Newton Scmander"}): - driver = frappe.get_doc({ - "doctype": "Driver", - "full_name": "Newton Scmander", - "cell_number": "98343424242", - "license_number": "B809", - }).insert(ignore_permissions=True) + driver = frappe.get_doc( + { + "doctype": "Driver", + "full_name": "Newton Scmander", + "cell_number": "98343424242", + "license_number": "B809", + } + ).insert(ignore_permissions=True) return driver return frappe.get_doc("Driver", {"full_name": "Newton Scmander"}) + def create_delivery_notification(): if not frappe.db.exists("Email Template", "Delivery Notification"): - dispatch_template = frappe.get_doc({ - 'doctype': 'Email Template', - 'name': 'Delivery Notification', - 'response': 'Test Delivery Trip', - 'subject': 'Test Subject', - 'owner': frappe.session.user - }) + dispatch_template = frappe.get_doc( + { + "doctype": "Email Template", + "name": "Delivery Notification", + "response": "Test Delivery Trip", + "subject": "Test Subject", + "owner": frappe.session.user, + } + ) dispatch_template.insert() delivery_settings = frappe.get_single("Delivery Settings") - delivery_settings.dispatch_template = 'Delivery Notification' + delivery_settings.dispatch_template = "Delivery Notification" delivery_settings.save() def create_vehicle(): if not frappe.db.exists("Vehicle", "JB 007"): - vehicle = frappe.get_doc({ - "doctype": "Vehicle", - "license_plate": "JB 007", - "make": "Maruti", - "model": "PCM", - "last_odometer": 5000, - "acquisition_date": nowdate(), - "location": "Mumbai", - "chassis_no": "1234ABCD", - "uom": "Litre", - "vehicle_value": flt(500000) - }) + vehicle = frappe.get_doc( + { + "doctype": "Vehicle", + "license_plate": "JB 007", + "make": "Maruti", + "model": "PCM", + "last_odometer": 5000, + "acquisition_date": nowdate(), + "location": "Mumbai", + "chassis_no": "1234ABCD", + "uom": "Litre", + "vehicle_value": flt(500000), + } + ) vehicle.insert() @@ -180,23 +186,27 @@ def create_delivery_trip(driver, address, contact=None): if not contact: contact = get_contact_and_address("_Test Customer") - delivery_trip = frappe.get_doc({ - "doctype": "Delivery Trip", - "company": erpnext.get_default_company(), - "departure_time": add_days(now_datetime(), 5), - "driver": driver.name, - "driver_address": address.name, - "vehicle": "JB 007", - "delivery_stops": [{ - "customer": "_Test Customer", - "address": contact.shipping_address.parent, - "contact": contact.contact_person.parent - }, + delivery_trip = frappe.get_doc( { - "customer": "_Test Customer", - "address": contact.shipping_address.parent, - "contact": contact.contact_person.parent - }] - }).insert(ignore_permissions=True) + "doctype": "Delivery Trip", + "company": erpnext.get_default_company(), + "departure_time": add_days(now_datetime(), 5), + "driver": driver.name, + "driver_address": address.name, + "vehicle": "JB 007", + "delivery_stops": [ + { + "customer": "_Test Customer", + "address": contact.shipping_address.parent, + "contact": contact.contact_person.parent, + }, + { + "customer": "_Test Customer", + "address": contact.shipping_address.parent, + "contact": contact.contact_person.parent, + }, + ], + } + ).insert(ignore_permissions=True) return delivery_trip diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index c7835930021..4ee429191ba 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -44,13 +44,15 @@ class StockExistsForTemplate(frappe.ValidationError): class InvalidBarcode(frappe.ValidationError): pass + class DataValidationError(frappe.ValidationError): pass + class Item(Document): def onload(self): - self.set_onload('stock_exists', self.stock_ledger_created()) - self.set_onload('asset_naming_series', get_asset_naming_series()) + self.set_onload("stock_exists", self.stock_ledger_created()) + self.set_onload("asset_naming_series", get_asset_naming_series()) def autoname(self): if frappe.db.get_default("item_naming_by") == "Naming Series": @@ -60,6 +62,7 @@ class Item(Document): make_variant_item_code(self.variant_of, template_item_name, self) else: from frappe.model.naming import set_name_by_naming_series + set_name_by_naming_series(self) self.item_code = self.name @@ -70,9 +73,8 @@ class Item(Document): if not self.description: self.description = self.item_name - def after_insert(self): - '''set opening stock and item price''' + """set opening stock and item price""" if self.standard_rate: for default in self.item_defaults or [frappe._dict()]: self.add_price(default.default_price_list) @@ -129,8 +131,8 @@ class Item(Document): self.update_website_item() def validate_description(self): - '''Clean HTML description if set''' - if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): + """Clean HTML description if set""" + if cint(frappe.db.get_single_value("Stock Settings", "clean_description_html")): self.description = clean_html(self.description) def validate_customer_provided_part(self): @@ -142,24 +144,27 @@ class Item(Document): self.default_material_request_type = "Customer Provided" def add_price(self, price_list=None): - '''Add a new price''' + """Add a new price""" if not price_list: - price_list = (frappe.db.get_single_value('Selling Settings', 'selling_price_list') - or frappe.db.get_value('Price List', _('Standard Selling'))) + price_list = frappe.db.get_single_value( + "Selling Settings", "selling_price_list" + ) or frappe.db.get_value("Price List", _("Standard Selling")) if price_list: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list, - "item_code": self.name, - "uom": self.stock_uom, - "brand": self.brand, - "currency": erpnext.get_default_currency(), - "price_list_rate": self.standard_rate - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list, + "item_code": self.name, + "uom": self.stock_uom, + "brand": self.brand, + "currency": erpnext.get_default_currency(), + "price_list_rate": self.standard_rate, + } + ) item_price.insert() def set_opening_stock(self): - '''set opening stock''' + """set opening stock""" if not self.is_stock_item or self.has_serial_no or self.has_batch_no: return @@ -172,19 +177,30 @@ class Item(Document): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry # default warehouse, or Stores - for default in self.item_defaults or [frappe._dict({'company': frappe.defaults.get_defaults().company})]: - default_warehouse = (default.default_warehouse - or frappe.db.get_single_value('Stock Settings', 'default_warehouse')) + for default in self.item_defaults or [ + frappe._dict({"company": frappe.defaults.get_defaults().company}) + ]: + default_warehouse = default.default_warehouse or frappe.db.get_single_value( + "Stock Settings", "default_warehouse" + ) if default_warehouse: warehouse_company = frappe.db.get_value("Warehouse", default_warehouse, "company") if not default_warehouse or warehouse_company != default.company: - default_warehouse = frappe.db.get_value('Warehouse', - {'warehouse_name': _('Stores'), 'company': default.company}) + default_warehouse = frappe.db.get_value( + "Warehouse", {"warehouse_name": _("Stores"), "company": default.company} + ) if default_warehouse: - stock_entry = make_stock_entry(item_code=self.name, target=default_warehouse, qty=self.opening_stock, - rate=self.valuation_rate, company=default.company, posting_date=getdate(), posting_time=nowtime()) + stock_entry = make_stock_entry( + item_code=self.name, + target=default_warehouse, + qty=self.opening_stock, + rate=self.valuation_rate, + company=default.company, + posting_date=getdate(), + posting_time=nowtime(), + ) stock_entry.add_comment("Comment", _("Opening Stock")) @@ -202,14 +218,21 @@ class Item(Document): if not self.is_fixed_asset: asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1) if asset: - frappe.throw(_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')) + frappe.throw( + _('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item') + ) def validate_retain_sample(self): - if self.retain_sample and not frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse'): + if self.retain_sample and not frappe.db.get_single_value( + "Stock Settings", "sample_retention_warehouse" + ): frappe.throw(_("Please select Sample Retention Warehouse in Stock Settings first")) if self.retain_sample and not self.has_batch_no: - frappe.throw(_("{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item").format( - self.item_code)) + frappe.throw( + _( + "{0} Retain Sample is based on batch, please check Has Batch No to retain sample of item" + ).format(self.item_code) + ) def clear_retain_sample(self): if not self.has_batch_no: @@ -229,10 +252,7 @@ class Item(Document): uoms_list = [d.uom for d in self.get("uoms")] if self.stock_uom not in uoms_list: - self.append("uoms", { - "uom": self.stock_uom, - "conversion_factor": 1 - }) + self.append("uoms", {"uom": self.stock_uom, "conversion_factor": 1}) def update_website_item(self): """Update Website Item if change in Item impacts it.""" @@ -240,8 +260,7 @@ class Item(Document): if web_item: changed = {} - editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", - "disabled"] + editable_fields = ["item_name", "item_group", "stock_uom", "brand", "description", "disabled"] doc_before_save = self.get_doc_before_save() for field in editable_fields: @@ -259,7 +278,7 @@ class Item(Document): web_item_doc.save() def validate_item_tax_net_rate_range(self): - for tax in self.get('taxes'): + for tax in self.get("taxes"): if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate): frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate")) @@ -274,23 +293,31 @@ class Item(Document): if not self.get("reorder_levels"): for d in template.get("reorder_levels"): n = {} - for k in ("warehouse", "warehouse_reorder_level", - "warehouse_reorder_qty", "material_request_type"): + for k in ( + "warehouse", + "warehouse_reorder_level", + "warehouse_reorder_qty", + "material_request_type", + ): n[k] = d.get(k) self.append("reorder_levels", n) def validate_conversion_factor(self): check_list = [] - for d in self.get('uoms'): + for d in self.get("uoms"): if cstr(d.uom) in check_list: frappe.throw( - _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format(d.uom)) + _("Unit of Measure {0} has been entered more than once in Conversion Factor Table").format( + d.uom + ) + ) else: check_list.append(cstr(d.uom)) if d.uom and cstr(d.uom) == cstr(self.stock_uom) and flt(d.conversion_factor) != 1: frappe.throw( - _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx)) + _("Conversion factor for default Unit of Measure must be 1 in row {0}").format(d.idx) + ) def validate_item_type(self): if self.has_serial_no == 1 and self.is_stock_item == 0 and not self.is_fixed_asset: @@ -303,28 +330,32 @@ class Item(Document): for field in ["serial_no_series", "batch_number_series"]: series = self.get(field) if series and "#" in series and "." not in series: - frappe.throw(_("Invalid naming series (. missing) for {0}") - .format(frappe.bold(self.meta.get_field(field).label))) + frappe.throw( + _("Invalid naming series (. missing) for {0}").format( + frappe.bold(self.meta.get_field(field).label) + ) + ) def check_for_active_boms(self): if self.default_bom: bom_item = frappe.db.get_value("BOM", self.default_bom, "item") if bom_item not in (self.name, self.variant_of): frappe.throw( - _("Default BOM ({0}) must be active for this item or its template").format(bom_item)) + _("Default BOM ({0}) must be active for this item or its template").format(bom_item) + ) def fill_customer_code(self): """ - Append all the customer codes and insert into "customer_code" field of item table. - Used to search Item by customer code. + Append all the customer codes and insert into "customer_code" field of item table. + Used to search Item by customer code. """ customer_codes = set(d.ref_code for d in self.get("customer_items", [])) - self.customer_code = ','.join(customer_codes) + self.customer_code = ",".join(customer_codes) def check_item_tax(self): """Check whether Tax Rate is not entered twice for same Tax Type""" check_list = [] - for d in self.get('taxes'): + for d in self.get("taxes"): if d.item_tax_template: if d.item_tax_template in check_list: frappe.throw(_("{0} entered twice in Item Tax").format(d.item_tax_template)) @@ -333,24 +364,39 @@ class Item(Document): def validate_barcode(self): from stdnum import ean + if len(self.barcodes) > 0: for item_barcode in self.barcodes: - options = frappe.get_meta("Item Barcode").get_options("barcode_type").split('\n') + options = frappe.get_meta("Item Barcode").get_options("barcode_type").split("\n") if item_barcode.barcode: duplicate = frappe.db.sql( - """select parent from `tabItem Barcode` where barcode = %s and parent != %s""", (item_barcode.barcode, self.name)) + """select parent from `tabItem Barcode` where barcode = %s and parent != %s""", + (item_barcode.barcode, self.name), + ) if duplicate: - frappe.throw(_("Barcode {0} already used in Item {1}").format( - item_barcode.barcode, duplicate[0][0])) + frappe.throw( + _("Barcode {0} already used in Item {1}").format(item_barcode.barcode, duplicate[0][0]) + ) - item_barcode.barcode_type = "" if item_barcode.barcode_type not in options else item_barcode.barcode_type - if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ('EAN', 'UPC-A', 'EAN-13', 'EAN-8'): + item_barcode.barcode_type = ( + "" if item_barcode.barcode_type not in options else item_barcode.barcode_type + ) + if item_barcode.barcode_type and item_barcode.barcode_type.upper() in ( + "EAN", + "UPC-A", + "EAN-13", + "EAN-8", + ): if not ean.is_valid(item_barcode.barcode): - frappe.throw(_("Barcode {0} is not a valid {1} code").format( - item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode) + frappe.throw( + _("Barcode {0} is not a valid {1} code").format( + item_barcode.barcode, item_barcode.barcode_type + ), + InvalidBarcode, + ) def validate_warehouse_for_reorder(self): - '''Validate Reorder level table for duplicate and conditional mandatory''' + """Validate Reorder level table for duplicate and conditional mandatory""" warehouse = [] for d in self.get("reorder_levels"): if not d.warehouse_group: @@ -358,20 +404,30 @@ class Item(Document): if d.get("warehouse") and d.get("warehouse") not in warehouse: warehouse += [d.get("warehouse")] else: - frappe.throw(_("Row {0}: An Reorder entry already exists for this warehouse {1}") - .format(d.idx, d.warehouse), DuplicateReorderRows) + frappe.throw( + _("Row {0}: An Reorder entry already exists for this warehouse {1}").format( + d.idx, d.warehouse + ), + DuplicateReorderRows, + ) if d.warehouse_reorder_level and not d.warehouse_reorder_qty: frappe.throw(_("Row #{0}: Please set reorder quantity").format(d.idx)) def stock_ledger_created(self): - if not hasattr(self, '_stock_ledger_created'): - self._stock_ledger_created = len(frappe.db.sql("""select name from `tabStock Ledger Entry` - where item_code = %s and is_cancelled = 0 limit 1""", self.name)) + if not hasattr(self, "_stock_ledger_created"): + self._stock_ledger_created = len( + frappe.db.sql( + """select name from `tabStock Ledger Entry` + where item_code = %s and is_cancelled = 0 limit 1""", + self.name, + ) + ) return self._stock_ledger_created def update_item_price(self): - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabItem Price` SET item_name=%(item_name)s, @@ -383,8 +439,8 @@ class Item(Document): item_name=self.item_name, item_description=self.description, brand=self.brand, - item_code=self.name - ) + item_code=self.name, + ), ) def on_trash(self): @@ -406,8 +462,11 @@ class Item(Document): def after_rename(self, old_name, new_name, merge): if merge: self.validate_duplicate_item_in_stock_reconciliation(old_name, new_name) - frappe.msgprint(_("It can take upto few hours for accurate stock values to be visible after merging items."), - indicator="orange", title="Note") + frappe.msgprint( + _("It can take upto few hours for accurate stock values to be visible after merging items."), + indicator="orange", + title="Note", + ) if self.published_in_website: invalidate_cache_for_item(self) @@ -419,39 +478,54 @@ class Item(Document): self.recalculate_bin_qty(new_name) for dt in ("Sales Taxes and Charges", "Purchase Taxes and Charges"): - for d in frappe.db.sql("""select name, item_wise_tax_detail from `tab{0}` - where ifnull(item_wise_tax_detail, '') != ''""".format(dt), as_dict=1): + for d in frappe.db.sql( + """select name, item_wise_tax_detail from `tab{0}` + where ifnull(item_wise_tax_detail, '') != ''""".format( + dt + ), + as_dict=1, + ): item_wise_tax_detail = json.loads(d.item_wise_tax_detail) if isinstance(item_wise_tax_detail, dict) and old_name in item_wise_tax_detail: item_wise_tax_detail[new_name] = item_wise_tax_detail[old_name] item_wise_tax_detail.pop(old_name) - frappe.db.set_value(dt, d.name, "item_wise_tax_detail", - json.dumps(item_wise_tax_detail), update_modified=False) + frappe.db.set_value( + dt, d.name, "item_wise_tax_detail", json.dumps(item_wise_tax_detail), update_modified=False + ) def delete_old_bins(self, old_name): frappe.db.delete("Bin", {"item_code": old_name}) def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name): - records = frappe.db.sql(""" SELECT parent, COUNT(*) as records + records = frappe.db.sql( + """ SELECT parent, COUNT(*) as records FROM `tabStock Reconciliation Item` WHERE item_code = %s and docstatus = 1 GROUP By item_code, warehouse, parent HAVING records > 1 - """, new_name, as_dict=1) + """, + new_name, + as_dict=1, + ) - if not records: return + if not records: + return document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations") msg = _("The items {0} and {1} are present in the following {2} :").format( - frappe.bold(old_name), frappe.bold(new_name), document) + frappe.bold(old_name), frappe.bold(new_name), document + ) - msg += '
    ' - msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

    " + msg += "
    " + msg += ( + ", ".join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "

    " + ) - msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format( - frappe.bold(old_name)) + msg += _( + "Note: To merge the items, create a separate Stock Reconciliation for the old item {0}" + ).format(frappe.bold(old_name)) frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) @@ -470,8 +544,8 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) - new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name}) + old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name}) + new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name}) if old_bundle and new_bundle: bundle_link = get_link_to_form("Product Bundle", old_bundle) @@ -484,15 +558,14 @@ class Item(Document): def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ - Block merge if both old and new items have website items against them. - This is to avoid duplicate website items after merging. + Block merge if both old and new items have website items against them. + This is to avoid duplicate website items after merging. """ web_items = frappe.get_all( "Website Item", - filters={ - "item_code": ["in", [old_name, new_name]] - }, - fields=["item_code", "name"]) + filters={"item_code": ["in", [old_name, new_name]]}, + fields=["item_code", "name"], + ) if len(web_items) <= 1: return @@ -510,11 +583,19 @@ class Item(Document): def recalculate_bin_qty(self, new_name): from erpnext.stock.stock_balance import repost_stock - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + + 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_warehouses = frappe.get_all("Stock Ledger Entry", - "warehouse", filters={"item_code": new_name}, pluck="warehouse", distinct=True) + repost_stock_for_warehouses = frappe.get_all( + "Stock Ledger Entry", + "warehouse", + filters={"item_code": new_name}, + pluck="warehouse", + distinct=True, + ) # Delete all existing bins to avoid duplicate bins for the same item and warehouse frappe.db.delete("Bin", {"item_code": new_name}) @@ -522,30 +603,41 @@ class Item(Document): for warehouse in repost_stock_for_warehouses: repost_stock(new_name, warehouse) - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.set_value( + "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock + ) def update_bom_item_desc(self): if self.is_new(): return - if self.db_get('description') != self.description: - frappe.db.sql(""" + if self.db_get("description") != self.description: + frappe.db.sql( + """ update `tabBOM` set description = %s where item = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabBOM Item` set description = %s where item_code = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabBOM Explosion Item` set description = %s where item_code = %s and docstatus < 2 - """, (self.description, self.name)) + """, + (self.description, self.name), + ) def validate_item_defaults(self): companies = {row.company for row in self.item_defaults} @@ -555,41 +647,61 @@ class Item(Document): validate_item_default_company_links(self.item_defaults) - def update_defaults_from_item_group(self): """Get defaults from Item Group""" if self.item_defaults or not self.item_group: return - item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group}, - ['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier', - 'expense_account','selling_cost_center','income_account'], as_dict = 1) + item_defaults = frappe.db.get_values( + "Item Default", + {"parent": self.item_group}, + [ + "company", + "default_warehouse", + "default_price_list", + "buying_cost_center", + "default_supplier", + "expense_account", + "selling_cost_center", + "income_account", + ], + as_dict=1, + ) if item_defaults: for item in item_defaults: - self.append('item_defaults', { - 'company': item.company, - 'default_warehouse': item.default_warehouse, - 'default_price_list': item.default_price_list, - 'buying_cost_center': item.buying_cost_center, - 'default_supplier': item.default_supplier, - 'expense_account': item.expense_account, - 'selling_cost_center': item.selling_cost_center, - 'income_account': item.income_account - }) + self.append( + "item_defaults", + { + "company": item.company, + "default_warehouse": item.default_warehouse, + "default_price_list": item.default_price_list, + "buying_cost_center": item.buying_cost_center, + "default_supplier": item.default_supplier, + "expense_account": item.expense_account, + "selling_cost_center": item.selling_cost_center, + "income_account": item.income_account, + }, + ) else: defaults = frappe.defaults.get_defaults() or {} # To check default warehouse is belong to the default company - if defaults.get("default_warehouse") and defaults.company and frappe.db.exists("Warehouse", - {'name': defaults.default_warehouse, 'company': defaults.company}): - self.append("item_defaults", { - "company": defaults.get("company"), - "default_warehouse": defaults.default_warehouse - }) + if ( + defaults.get("default_warehouse") + and defaults.company + and frappe.db.exists( + "Warehouse", {"name": defaults.default_warehouse, "company": defaults.company} + ) + ): + self.append( + "item_defaults", + {"company": defaults.get("company"), "default_warehouse": defaults.default_warehouse}, + ) def update_variants(self): - if self.flags.dont_update_variants or \ - frappe.db.get_single_value('Item Variant Settings', 'do_not_update_variants'): + if self.flags.dont_update_variants or frappe.db.get_single_value( + "Item Variant Settings", "do_not_update_variants" + ): return if self.has_variants: variants = frappe.db.get_all("Item", fields=["item_code"], filters={"variant_of": self.name}) @@ -598,8 +710,13 @@ class Item(Document): update_variants(variants, self, publish_progress=False) frappe.msgprint(_("Item Variants updated")) else: - frappe.enqueue("erpnext.stock.doctype.item.item.update_variants", - variants=variants, template=self, now=frappe.flags.in_test, timeout=600) + frappe.enqueue( + "erpnext.stock.doctype.item.item.update_variants", + variants=variants, + template=self, + now=frappe.flags.in_test, + timeout=600, + ) def validate_has_variants(self): if not self.has_variants and frappe.db.get_value("Item", self.name, "has_variants"): @@ -631,11 +748,8 @@ class Item(Document): # fetch all attributes of these items item_attributes = frappe.get_all( "Item Variant Attribute", - filters={ - "parent": ["in", items], - "attribute": ["in", deleted_attribute] - }, - fields=["attribute", "parent"] + filters={"parent": ["in", items], "attribute": ["in", deleted_attribute]}, + fields=["attribute", "parent"], ) not_included = defaultdict(list) @@ -654,14 +768,18 @@ class Item(Document): return """ {0} {1} - """.format(title, body) + """.format( + title, body + ) - rows = '' + rows = "" for docname, attr_list in not_included.items(): link = "{0}".format(frappe.bold(_(docname))) rows += table_row(link, body(attr_list)) - error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + error_description = _( + "The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template." + ) message = """
    {0}

    @@ -672,25 +790,37 @@ class Item(Document): {3} - """.format(error_description, _('Variant Items'), _('Attributes'), rows) + """.format( + error_description, _("Variant Items"), _("Attributes"), rows + ) frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True) - def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: - if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) - or self._doc_before_save.variant_of != self.variant_of): - frappe.throw(_("Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.").format(self.name), - StockExistsForTemplate) + if ( + cint(self._doc_before_save.has_variants) != cint(self.has_variants) + or self._doc_before_save.variant_of != self.variant_of + ): + frappe.throw( + _( + "Cannot change Variant properties after stock transaction. You will have to make a new Item to do this." + ).format(self.name), + StockExistsForTemplate, + ) if self.has_variants or self.variant_of: - if not self.is_child_table_same('attributes'): + if not self.is_child_table_same("attributes"): frappe.throw( - _('Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item')) + _( + "Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item" + ) + ) def validate_variant_based_on_change(self): - if not self.is_new() and (self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name}))): + if not self.is_new() and ( + self.variant_of or (self.has_variants and frappe.get_all("Item", {"variant_of": self.name})) + ): if self.variant_based_on != frappe.db.get_value("Item", self.name, "variant_based_on"): frappe.throw(_("Variant Based On cannot be changed")) @@ -703,8 +833,11 @@ class Item(Document): if self.variant_of: template_uom = frappe.db.get_value("Item", self.variant_of, "stock_uom") if template_uom != self.stock_uom: - frappe.throw(_("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'") - .format(self.stock_uom, template_uom)) + frappe.throw( + _("Default Unit of Measure for Variant '{0}' must be same as in Template '{1}'").format( + self.stock_uom, template_uom + ) + ) def validate_uom_conversion_factor(self): if self.uoms: @@ -718,21 +851,22 @@ class Item(Document): return if not self.variant_based_on: - self.variant_based_on = 'Item Attribute' + self.variant_based_on = "Item Attribute" - if self.variant_based_on == 'Item Attribute': + if self.variant_based_on == "Item Attribute": attributes = [] if not self.attributes: frappe.throw(_("Attribute table is mandatory")) for d in self.attributes: if d.attribute in attributes: frappe.throw( - _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute)) + _("Attribute {0} selected multiple times in Attributes Table").format(d.attribute) + ) else: attributes.append(d.attribute) def validate_variant_attributes(self): - if self.is_new() and self.variant_of and self.variant_based_on == 'Item Attribute': + if self.is_new() and self.variant_of and self.variant_based_on == "Item Attribute": # remove attributes with no attribute_value set self.attributes = [d for d in self.attributes if cstr(d.attribute_value).strip()] @@ -743,8 +877,9 @@ class Item(Document): variant = get_variant(self.variant_of, args, self.name) if variant: - frappe.throw(_("Item variant {0} exists with same attributes") - .format(variant), ItemVariantExistsError) + frappe.throw( + _("Item variant {0} exists with same attributes").format(variant), ItemVariantExistsError + ) validate_item_variant_attributes(self, args) @@ -759,31 +894,52 @@ class Item(Document): fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no") values = frappe.db.get_value("Item", self.name, fields, as_dict=True) - if not values.get('valuation_method') and self.get('valuation_method'): - values['valuation_method'] = frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" + if not values.get("valuation_method") and self.get("valuation_method"): + values["valuation_method"] = ( + frappe.db.get_single_value("Stock Settings", "valuation_method") or "FIFO" + ) if values: for field in fields: if cstr(self.get(field)) != cstr(values.get(field)): if self.check_if_linked_document_exists(field): - frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field)))) + frappe.throw( + _( + "As there are existing transactions against item {0}, you can not change the value of {1}" + ).format(self.name, frappe.bold(self.meta.get_label(field))) + ) def check_if_linked_document_exists(self, field): - linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item", - "Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"] + linked_doctypes = [ + "Delivery Note Item", + "Sales Invoice Item", + "POS Invoice Item", + "Purchase Receipt Item", + "Purchase Invoice Item", + "Stock Entry Detail", + "Stock Reconciliation Item", + ] # For "Is Stock Item", following doctypes is important # because reserved_qty, ordered_qty and requested_qty updated from these doctypes if field == "is_stock_item": - linked_doctypes += ["Sales Order Item", "Purchase Order Item", "Material Request Item", "Product Bundle"] + linked_doctypes += [ + "Sales Order Item", + "Purchase Order Item", + "Material Request Item", + "Product Bundle", + ] for doctype in linked_doctypes: - filters={"item_code": self.name, "docstatus": 1} + filters = {"item_code": self.name, "docstatus": 1} if doctype == "Product Bundle": - filters={"new_item_code": self.name} + filters = {"new_item_code": self.name} - if doctype in ("Purchase Invoice Item", "Sales Invoice Item",): + if doctype in ( + "Purchase Invoice Item", + "Sales Invoice Item", + ): # If Invoice has Stock impact, only then consider it. if self.stock_ledger_created(): return True @@ -793,37 +949,48 @@ class Item(Document): def validate_auto_reorder_enabled_in_stock_settings(self): if self.reorder_levels: - enabled = frappe.db.get_single_value('Stock Settings', 'auto_indent') + enabled = frappe.db.get_single_value("Stock Settings", "auto_indent") if not enabled: - frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange") + frappe.msgprint( + msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), + title=_("Enable Auto Re-Order"), + indicator="orange", + ) def make_item_price(item, price_list_name, item_price): - frappe.get_doc({ - 'doctype': 'Item Price', - 'price_list': price_list_name, - 'item_code': item, - 'price_list_rate': item_price - }).insert() + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": price_list_name, + "item_code": item, + "price_list_rate": item_price, + } + ).insert() + def get_timeline_data(doctype, name): """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page.""" - items = frappe.db.sql("""select unix_timestamp(posting_date), count(*) + items = frappe.db.sql( + """select unix_timestamp(posting_date), count(*) from `tabStock Ledger Entry` where item_code=%s and posting_date > date_sub(curdate(), interval 1 year) - group by posting_date""", name) + group by posting_date""", + name, + ) return dict(items) - def validate_end_of_life(item_code, end_of_life=None, disabled=None): if (not end_of_life) or (disabled is None): end_of_life, disabled = frappe.db.get_value("Item", item_code, ["end_of_life", "disabled"]) if end_of_life and end_of_life != "0000-00-00" and getdate(end_of_life) <= now_datetime().date(): - frappe.throw(_("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life))) + frappe.throw( + _("Item {0} has reached its end of life on {1}").format(item_code, formatdate(end_of_life)) + ) if disabled: frappe.throw(_("Item {0} is disabled").format(item_code)) @@ -844,11 +1011,13 @@ def validate_cancelled_item(item_code, docstatus=None): if docstatus == 2: frappe.throw(_("Item {0} is cancelled").format(item_code)) + def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details - last_purchase_order = frappe.db.sql("""\ + last_purchase_order = frappe.db.sql( + """\ select po.name, po.transaction_date, po.conversion_rate, po_item.conversion_factor, po_item.base_price_list_rate, po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate @@ -856,11 +1025,14 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and po.name = po_item.parent order by po.transaction_date desc, po.name desc - limit 1""", (item_code, cstr(doc_name)), as_dict=1) - + limit 1""", + (item_code, cstr(doc_name)), + as_dict=1, + ) # get last purchase receipt item details - last_purchase_receipt = frappe.db.sql("""\ + last_purchase_receipt = frappe.db.sql( + """\ select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, pr_item.base_rate, pr_item.base_net_rate @@ -868,20 +1040,29 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and pr.name = pr_item.parent order by pr.posting_date desc, pr.posting_time desc, pr.name desc - limit 1""", (item_code, cstr(doc_name)), as_dict=1) + limit 1""", + (item_code, cstr(doc_name)), + as_dict=1, + ) - purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date - or "1900-01-01") - purchase_receipt_date = getdate(last_purchase_receipt and - last_purchase_receipt[0].posting_date or "1900-01-01") + purchase_order_date = getdate( + last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01" + ) + purchase_receipt_date = getdate( + last_purchase_receipt and last_purchase_receipt[0].posting_date or "1900-01-01" + ) - if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt): + if last_purchase_order and ( + purchase_order_date >= purchase_receipt_date or not last_purchase_receipt + ): # use purchase order last_purchase = last_purchase_order[0] purchase_date = purchase_order_date - elif last_purchase_receipt and (purchase_receipt_date > purchase_order_date or not last_purchase_order): + elif last_purchase_receipt and ( + purchase_receipt_date > purchase_order_date or not last_purchase_order + ): # use purchase receipt last_purchase = last_purchase_receipt[0] purchase_date = purchase_receipt_date @@ -890,22 +1071,25 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): return frappe._dict() conversion_factor = flt(last_purchase.conversion_factor) - out = frappe._dict({ - "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, - "base_rate": flt(last_purchase.base_rate) / conversion_factor, - "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor, - "discount_percentage": flt(last_purchase.discount_percentage), - "purchase_date": purchase_date - }) - + out = frappe._dict( + { + "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, + "base_rate": flt(last_purchase.base_rate) / conversion_factor, + "base_net_rate": flt(last_purchase.base_net_rate) / conversion_factor, + "discount_percentage": flt(last_purchase.discount_percentage), + "purchase_date": purchase_date, + } + ) conversion_rate = flt(conversion_rate) or 1.0 - out.update({ - "price_list_rate": out.base_price_list_rate / conversion_rate, - "rate": out.base_rate / conversion_rate, - "base_rate": out.base_rate, - "base_net_rate": out.base_net_rate - }) + out.update( + { + "price_list_rate": out.base_price_list_rate / conversion_rate, + "rate": out.base_rate / conversion_rate, + "base_rate": out.base_rate, + "base_net_rate": out.base_net_rate, + } + ) return out @@ -928,39 +1112,51 @@ def invalidate_item_variants_cache_for_website(doc): is_web_item = doc.get("published_in_website") or doc.get("published") if doc.has_variants and is_web_item: item_code = doc.item_code - elif doc.variant_of and frappe.db.get_value('Item', doc.variant_of, 'published_in_website'): + elif doc.variant_of and frappe.db.get_value("Item", doc.variant_of, "published_in_website"): item_code = doc.variant_of if item_code: item_cache = ItemVariantsCacheManager(item_code) item_cache.rebuild_cache() + def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): return - ref_uom = frappe.db.get_value("Stock Ledger Entry", - {"item_code": item}, "stock_uom") + ref_uom = frappe.db.get_value("Stock Ledger Entry", {"item_code": item}, "stock_uom") if ref_uom: if cstr(ref_uom) != cstr(stock_uom): - frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.").format(item)) + frappe.throw( + _( + "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM." + ).format(item) + ) - bin_list = frappe.db.sql(""" + bin_list = frappe.db.sql( + """ select * from tabBin where item_code = %s and (reserved_qty > 0 or ordered_qty > 0 or indented_qty > 0 or planned_qty > 0) and stock_uom != %s - """, (item, stock_uom), as_dict=1) + """, + (item, stock_uom), + as_dict=1, + ) if bin_list: - frappe.throw(_("Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.").format(item)) + frappe.throw( + _( + "Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item." + ).format(item) + ) # No SLE or documents against item. Bin UOM can be changed safely. frappe.db.sql("""update tabBin set stock_uom=%s where item_code=%s""", (stock_uom, item)) def get_item_defaults(item_code, company): - item = frappe.get_cached_doc('Item', item_code) + item = frappe.get_cached_doc("Item", item_code) out = item.as_dict() @@ -971,8 +1167,9 @@ def get_item_defaults(item_code, company): out.update(row) return out + def set_item_default(item_code, company, fieldname, value): - item = frappe.get_cached_doc('Item', item_code) + item = frappe.get_cached_doc("Item", item_code) for d in item.item_defaults: if d.company == company: @@ -981,10 +1178,11 @@ def set_item_default(item_code, company, fieldname, value): return # no row found, add a new row for the company - d = item.append('item_defaults', {fieldname: value, "company": company}) + d = item.append("item_defaults", {fieldname: value, "company": company}) d.db_insert() item.clear_cache() + @frappe.whitelist() def get_item_details(item_code, company=None): out = frappe._dict() @@ -996,30 +1194,36 @@ def get_item_details(item_code, company=None): return out + @frappe.whitelist() def get_uom_conv_factor(uom, stock_uom): - """ Get UOM conversion factor from uom to stock_uom - e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 + """Get UOM conversion factor from uom to stock_uom + e.g. uom = "Kg", stock_uom = "Gram" then returns 1000.0 """ if uom == stock_uom: return 1.0 - from_uom, to_uom = uom, stock_uom # renaming for readability + from_uom, to_uom = uom, stock_uom # renaming for readability - exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1) + exact_match = frappe.db.get_value( + "UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1 + ) if exact_match: return exact_match.value - inverse_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1) + inverse_match = frappe.db.get_value( + "UOM Conversion Factor", {"to_uom": from_uom, "from_uom": to_uom}, ["value"], as_dict=1 + ) if inverse_match: return 1 / inverse_match.value # This attempts to try and get conversion from intermediate UOM. # case: - # g -> mg = 1000 - # g -> kg = 0.001 + # g -> mg = 1000 + # g -> kg = 0.001 # therefore kg -> mg = 1000 / 0.001 = 1,000,000 - intermediate_match = frappe.db.sql(""" + intermediate_match = frappe.db.sql( + """ select (first.value / second.value) as value from `tabUOM Conversion Factor` first join `tabUOM Conversion Factor` second @@ -1028,7 +1232,10 @@ def get_uom_conv_factor(uom, stock_uom): first.to_uom = %(to_uom)s and second.to_uom = %(from_uom)s limit 1 - """, {"to_uom": to_uom, "from_uom": from_uom}, as_dict=1) + """, + {"to_uom": to_uom, "from_uom": from_uom}, + as_dict=1, + ) if intermediate_match: return intermediate_match[0].value @@ -1040,8 +1247,12 @@ def get_item_attribute(parent, attribute_value=""): if not frappe.has_permission("Item"): frappe.throw(_("No Permission")) - return frappe.get_all("Item Attribute Value", fields = ["attribute_value"], - filters = {'parent': parent, 'attribute_value': ("like", f"%{attribute_value}%")}) + return frappe.get_all( + "Item Attribute Value", + fields=["attribute_value"], + filters={"parent": parent, "attribute_value": ("like", f"%{attribute_value}%")}, + ) + def update_variants(variants, template, publish_progress=True): total = len(variants) @@ -1052,6 +1263,7 @@ def update_variants(variants, template, publish_progress=True): if publish_progress: frappe.publish_progress(count / total * 100, title=_("Updating Variants...")) + @erpnext.allow_regional def set_item_tax_from_hsn_code(item): pass @@ -1060,23 +1272,25 @@ def set_item_tax_from_hsn_code(item): def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None: for item_default in item_defaults: for doctype, field in [ - ['Warehouse', 'default_warehouse'], - ['Cost Center', 'buying_cost_center'], - ['Cost Center', 'selling_cost_center'], - ['Account', 'expense_account'], - ['Account', 'income_account'] + ["Warehouse", "default_warehouse"], + ["Cost Center", "buying_cost_center"], + ["Cost Center", "selling_cost_center"], + ["Account", "expense_account"], + ["Account", "income_account"], ]: if item_default.get(field): - company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True) + company = frappe.db.get_value(doctype, item_default.get(field), "company", cache=True) if company and company != item_default.company: - frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.") - .format( + frappe.throw( + _("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.").format( item_default.idx, doctype, frappe.bold(item_default.get(field)), frappe.bold(item_default.company), - frappe.bold(frappe.unscrub(field)) - ), title=_("Invalid Item Defaults")) + frappe.bold(frappe.unscrub(field)), + ), + title=_("Invalid Item Defaults"), + ) @frappe.whitelist() @@ -1084,4 +1298,3 @@ def get_asset_naming_series(): from erpnext.assets.doctype.asset.asset import get_asset_naming_series return get_asset_naming_series() - diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py index b0f1bbc2d52..33acf4bfd8a 100644 --- a/erpnext/stock/doctype/item/item_dashboard.py +++ b/erpnext/stock/doctype/item/item_dashboard.py @@ -1,48 +1,36 @@ - from frappe import _ def get_data(): return { - 'heatmap': True, - 'heatmap_message': _('This is based on stock movement. See {0} for details')\ - .format('' + _('Stock Ledger') + ''), - 'fieldname': 'item_code', - 'non_standard_fieldnames': { - 'Work Order': 'production_item', - 'Product Bundle': 'new_item_code', - 'BOM': 'item', - 'Batch': 'item' + "heatmap": True, + "heatmap_message": _("This is based on stock movement. See {0} for details").format( + '' + _("Stock Ledger") + "" + ), + "fieldname": "item_code", + "non_standard_fieldnames": { + "Work Order": "production_item", + "Product Bundle": "new_item_code", + "BOM": "item", + "Batch": "item", }, - 'transactions': [ + "transactions": [ + {"label": _("Groups"), "items": ["BOM", "Product Bundle", "Item Alternative"]}, + {"label": _("Pricing"), "items": ["Item Price", "Pricing Rule"]}, + {"label": _("Sell"), "items": ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]}, { - 'label': _('Groups'), - 'items': ['BOM', 'Product Bundle', 'Item Alternative'] + "label": _("Buy"), + "items": [ + "Material Request", + "Supplier Quotation", + "Request for Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ], }, - { - 'label': _('Pricing'), - 'items': ['Item Price', 'Pricing Rule'] - }, - { - 'label': _('Sell'), - 'items': ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Buy'), - 'items': ['Material Request', 'Supplier Quotation', 'Request for Quotation', - 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] - }, - { - 'label': _('Manufacture'), - 'items': ['Production Plan', 'Work Order', 'Item Manufacturer'] - }, - { - 'label': _('Traceability'), - 'items': ['Serial No', 'Batch'] - }, - { - 'label': _('Move'), - 'items': ['Stock Entry'] - } - ] + {"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]}, + {"label": _("Traceability"), "items": ["Serial No", "Batch"]}, + {"label": _("Move"), "items": ["Stock Entry"]}, + ], } diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 9e8bf02a707..7ef24020ef6 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -29,6 +29,7 @@ from erpnext.stock.get_item_details import get_item_details test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] + def make_item(item_code=None, properties=None): if not item_code: item_code = frappe.generate_hash(length=16) @@ -36,13 +37,15 @@ def make_item(item_code=None, properties=None): if frappe.db.exists("Item", item_code): return frappe.get_doc("Item", item_code) - item = frappe.get_doc({ - "doctype": "Item", - "item_code": item_code, - "item_name": item_code, - "description": item_code, - "item_group": "Products" - }) + item = frappe.get_doc( + { + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "Products", + } + ) if properties: item.update(properties) @@ -55,6 +58,7 @@ def make_item(item_code=None, properties=None): return item + class TestItem(FrappeTestCase): def setUp(self): super().setUp() @@ -97,56 +101,91 @@ class TestItem(FrappeTestCase): make_test_objects("Item Price") company = "_Test Company" - currency = frappe.get_cached_value("Company", company, "default_currency") + currency = frappe.get_cached_value("Company", company, "default_currency") - details = get_item_details({ - "item_code": "_Test Item", - "company": company, - "price_list": "_Test Price List", - "currency": currency, - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": currency, - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "conversion_factor": 1, - "price_list_uom_dependant": 1, - "ignore_pricing_rule": 1 - }) + details = get_item_details( + { + "item_code": "_Test Item", + "company": company, + "price_list": "_Test Price List", + "currency": currency, + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": currency, + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "conversion_factor": 1, + "price_list_uom_dependant": 1, + "ignore_pricing_rule": 1, + } + ) for key, value in to_check.items(): self.assertEqual(value, details.get(key)) def test_item_tax_template(self): expected_item_tax_template = [ - {"item_code": "_Test Item With Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, - {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, - {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, - - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"}, - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Item Tax Template 1 - _TC"}, - {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2", - "item_tax_template": None}, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item With Item Tax Template", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 1", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + }, + { + "item_code": "_Test Item Inherit Group Item Tax Template 2", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "", + "item_tax_template": "_Test Account Excise Duty @ 20 - _TC", + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "_Test Tax Category 1", + "item_tax_template": "_Test Item Tax Template 1 - _TC", + }, + { + "item_code": "_Test Item Override Group Item Tax Template", + "tax_category": "_Test Tax Category 2", + "item_tax_template": None, + }, ] expected_item_tax_map = { @@ -155,43 +194,55 @@ class TestItem(FrappeTestCase): "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12}, "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15}, "_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20}, - "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10, - "_Test Account S&H Education Cess - _TC": 15} + "_Test Item Tax Template 1 - _TC": { + "_Test Account Excise Duty - _TC": 5, + "_Test Account Education Cess - _TC": 10, + "_Test Account S&H Education Cess - _TC": 15, + }, } for data in expected_item_tax_template: - details = get_item_details({ - "item_code": data['item_code'], - "tax_category": data['tax_category'], - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Order", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "order_type": "Sales", - "customer": "_Test Customer", - "conversion_factor": 1, - "price_list_uom_dependant": 1, - "ignore_pricing_rule": 1 - }) + details = get_item_details( + { + "item_code": data["item_code"], + "tax_category": data["tax_category"], + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Order", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "order_type": "Sales", + "customer": "_Test Customer", + "conversion_factor": 1, + "price_list_uom_dependant": 1, + "ignore_pricing_rule": 1, + } + ) - self.assertEqual(details.item_tax_template, data['item_tax_template']) - self.assertEqual(json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template]) + self.assertEqual(details.item_tax_template, data["item_tax_template"]) + self.assertEqual( + json.loads(details.item_tax_rate), expected_item_tax_map[details.item_tax_template] + ) def test_item_defaults(self): frappe.delete_doc_if_exists("Item", "Test Item With Defaults", force=1) - make_item("Test Item With Defaults", { - "item_group": "_Test Item Group", - "brand": "_Test Brand With Item Defaults", - "item_defaults": [{ - "company": "_Test Company", - "default_warehouse": "_Test Warehouse 2 - _TC", # no override - "expense_account": "_Test Account Stock Expenses - _TC", # override brand default - "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default - }] - }) + make_item( + "Test Item With Defaults", + { + "item_group": "_Test Item Group", + "brand": "_Test Brand With Item Defaults", + "item_defaults": [ + { + "company": "_Test Company", + "default_warehouse": "_Test Warehouse 2 - _TC", # no override + "expense_account": "_Test Account Stock Expenses - _TC", # override brand default + "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default + } + ], + }, + ) sales_item_check = { "item_code": "Test Item With Defaults", @@ -200,17 +251,19 @@ class TestItem(FrappeTestCase): "expense_account": "_Test Account Stock Expenses - _TC", # from item "cost_center": "_Test Cost Center 2 - _TC", # from item group } - sales_item_details = get_item_details({ - "item_code": "Test Item With Defaults", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Sales Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "customer": "_Test Customer", - }) + sales_item_details = get_item_details( + { + "item_code": "Test Item With Defaults", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Sales Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "customer": "_Test Customer", + } + ) for key, value in sales_item_check.items(): self.assertEqual(value, sales_item_details.get(key)) @@ -219,38 +272,47 @@ class TestItem(FrappeTestCase): "warehouse": "_Test Warehouse 2 - _TC", # from item "expense_account": "_Test Account Stock Expenses - _TC", # from item "income_account": "_Test Account Sales - _TC", # from brand - "cost_center": "_Test Write Off Cost Center - _TC" # from item + "cost_center": "_Test Write Off Cost Center - _TC", # from item } - purchase_item_details = get_item_details({ - "item_code": "Test Item With Defaults", - "company": "_Test Company", - "price_list": "_Test Price List", - "currency": "_Test Currency", - "doctype": "Purchase Invoice", - "conversion_rate": 1, - "price_list_currency": "_Test Currency", - "plc_conversion_rate": 1, - "supplier": "_Test Supplier", - }) + purchase_item_details = get_item_details( + { + "item_code": "Test Item With Defaults", + "company": "_Test Company", + "price_list": "_Test Price List", + "currency": "_Test Currency", + "doctype": "Purchase Invoice", + "conversion_rate": 1, + "price_list_currency": "_Test Currency", + "plc_conversion_rate": 1, + "supplier": "_Test Supplier", + } + ) for key, value in purchase_item_check.items(): self.assertEqual(value, purchase_item_details.get(key)) def test_item_default_validations(self): with self.assertRaises(frappe.ValidationError) as ve: - make_item("Bad Item defaults", { - "item_group": "_Test Item Group", - "item_defaults": [{ - "company": "_Test Company 1", - "default_warehouse": "_Test Warehouse - _TC", - "expense_account": "Stock In Hand - _TC", - "buying_cost_center": "_Test Cost Center - _TC", - "selling_cost_center": "_Test Cost Center - _TC", - }] - }) + make_item( + "Bad Item defaults", + { + "item_group": "_Test Item Group", + "item_defaults": [ + { + "company": "_Test Company 1", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "Stock In Hand - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + } + ], + }, + ) - self.assertTrue("belong to company" in str(ve.exception).lower(), - msg="Mismatching company entities in item defaults should not be allowed.") + self.assertTrue( + "belong to company" in str(ve.exception).lower(), + msg="Mismatching company entities in item defaults should not be allowed.", + ) def test_item_attribute_change_after_variant(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) @@ -258,7 +320,7 @@ class TestItem(FrappeTestCase): variant = create_variant("_Test Variant Item", {"Test Size": "Large"}) variant.save() - attribute = frappe.get_doc('Item Attribute', 'Test Size') + attribute = frappe.get_doc("Item Attribute", "Test Size") attribute.item_attribute_values = [] # reset flags @@ -281,20 +343,18 @@ class TestItem(FrappeTestCase): def test_copy_fields_from_template_to_variants(self): frappe.delete_doc_if_exists("Item", "_Test Variant Item-XL", force=1) - fields = [{'field_name': 'item_group'}, {'field_name': 'is_stock_item'}] - allow_fields = [d.get('field_name') for d in fields] + fields = [{"field_name": "item_group"}, {"field_name": "is_stock_item"}] + allow_fields = [d.get("field_name") for d in fields] set_item_variant_settings(fields) - if not frappe.db.get_value('Item Attribute Value', - {'parent': 'Test Size', 'attribute_value': 'Extra Large'}, 'name'): - item_attribute = frappe.get_doc('Item Attribute', 'Test Size') - item_attribute.append('item_attribute_values', { - 'attribute_value' : 'Extra Large', - 'abbr': 'XL' - }) + if not frappe.db.get_value( + "Item Attribute Value", {"parent": "Test Size", "attribute_value": "Extra Large"}, "name" + ): + item_attribute = frappe.get_doc("Item Attribute", "Test Size") + item_attribute.append("item_attribute_values", {"attribute_value": "Extra Large", "abbr": "XL"}) item_attribute.save() - template = frappe.get_doc('Item', '_Test Variant Item') + template = frappe.get_doc("Item", "_Test Variant Item") template.item_group = "_Test Item Group D" template.save() @@ -303,70 +363,71 @@ class TestItem(FrappeTestCase): variant.item_name = "_Test Variant Item-XL" variant.save() - variant = frappe.get_doc('Item', '_Test Variant Item-XL') + variant = frappe.get_doc("Item", "_Test Variant Item-XL") for fieldname in allow_fields: self.assertEqual(template.get(fieldname), variant.get(fieldname)) - template = frappe.get_doc('Item', '_Test Variant Item') + template = frappe.get_doc("Item", "_Test Variant Item") template.item_group = "_Test Item Group Desktops" template.save() def test_make_item_variant_with_numeric_values(self): # cleanup - for d in frappe.db.get_all('Item', filters={'variant_of': - '_Test Numeric Template Item'}): + for d in frappe.db.get_all("Item", filters={"variant_of": "_Test Numeric Template Item"}): frappe.delete_doc_if_exists("Item", d.name) frappe.delete_doc_if_exists("Item", "_Test Numeric Template Item") frappe.delete_doc_if_exists("Item Attribute", "Test Item Length") - frappe.db.sql('''delete from `tabItem Variant Attribute` - where attribute="Test Item Length"''') + frappe.db.sql( + '''delete from `tabItem Variant Attribute` + where attribute="Test Item Length"''' + ) frappe.flags.attribute_values = None # make item attribute - frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": "Test Item Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0.5 - }).insert() + frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": "Test Item Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0.5, + } + ).insert() # make template item - make_item("_Test Numeric Template Item", { - "attributes": [ - { - "attribute": "Test Size" - }, - { - "attribute": "Test Item Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0.5 - } - ], - "item_defaults": [ - { - "default_warehouse": "_Test Warehouse - _TC", - "company": "_Test Company" - } - ], - "has_variants": 1 - }) + make_item( + "_Test Numeric Template Item", + { + "attributes": [ + {"attribute": "Test Size"}, + { + "attribute": "Test Item Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0.5, + }, + ], + "item_defaults": [{"default_warehouse": "_Test Warehouse - _TC", "company": "_Test Company"}], + "has_variants": 1, + }, + ) - variant = create_variant("_Test Numeric Template Item", - {"Test Size": "Large", "Test Item Length": 1.1}) + variant = create_variant( + "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.1} + ) self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.1") variant.item_code = "_Test Numeric Variant-L-1.1" variant.item_name = "_Test Numeric Variant Large 1.1m" self.assertRaises(InvalidItemAttributeValueError, variant.save) - variant = create_variant("_Test Numeric Template Item", - {"Test Size": "Large", "Test Item Length": 1.5}) + variant = create_variant( + "_Test Numeric Template Item", {"Test Size": "Large", "Test Item Length": 1.5} + ) self.assertEqual(variant.item_code, "_Test Numeric Template Item-L-1.5") variant.item_code = "_Test Numeric Variant-L-1.5" variant.item_name = "_Test Numeric Variant Large 1.5m" @@ -376,21 +437,20 @@ class TestItem(FrappeTestCase): old = create_item(frappe.generate_hash(length=20)).name new = create_item(frappe.generate_hash(length=20)).name - make_stock_entry(item_code=old, target="_Test Warehouse - _TC", - qty=1, rate=100) - make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", - qty=1, rate=100) - make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", - qty=1, rate=100) + make_stock_entry(item_code=old, target="_Test Warehouse - _TC", qty=1, rate=100) + make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC", qty=1, rate=100) + make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC", qty=1, rate=100) frappe.rename_doc("Item", old, new, merge=True) self.assertFalse(frappe.db.exists("Item", old)) - self.assertTrue(frappe.db.get_value("Bin", - {"item_code": new, "warehouse": "_Test Warehouse - _TC"})) - self.assertTrue(frappe.db.get_value("Bin", - {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"})) + self.assertTrue( + frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse - _TC"}) + ) + self.assertTrue( + frappe.db.get_value("Bin", {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"}) + ) def test_item_merging_with_product_bundle(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle @@ -413,13 +473,12 @@ class TestItem(FrappeTestCase): self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1")) def test_uom_conversion_factor(self): - if frappe.db.exists('Item', 'Test Item UOM'): - frappe.delete_doc('Item', 'Test Item UOM') + if frappe.db.exists("Item", "Test Item UOM"): + frappe.delete_doc("Item", "Test Item UOM") - item_doc = make_item("Test Item UOM", { - "stock_uom": "Gram", - "uoms": [dict(uom='Carat'), dict(uom='Kg')] - }) + item_doc = make_item( + "Test Item UOM", {"stock_uom": "Gram", "uoms": [dict(uom="Carat"), dict(uom="Kg")]} + ) for d in item_doc.uoms: value = get_uom_conv_factor(d.uom, item_doc.stock_uom) @@ -439,48 +498,46 @@ class TestItem(FrappeTestCase): self.assertEqual(factor, 1.0) def test_item_variant_by_manufacturer(self): - fields = [{'field_name': 'description'}, {'field_name': 'variant_based_on'}] + fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}] set_item_variant_settings(fields) - if frappe.db.exists('Item', '_Test Variant Mfg'): - frappe.delete_doc('Item', '_Test Variant Mfg') - if frappe.db.exists('Item', '_Test Variant Mfg-1'): - frappe.delete_doc('Item', '_Test Variant Mfg-1') - if frappe.db.exists('Manufacturer', 'MSG1'): - frappe.delete_doc('Manufacturer', 'MSG1') + if frappe.db.exists("Item", "_Test Variant Mfg"): + frappe.delete_doc("Item", "_Test Variant Mfg") + if frappe.db.exists("Item", "_Test Variant Mfg-1"): + frappe.delete_doc("Item", "_Test Variant Mfg-1") + if frappe.db.exists("Manufacturer", "MSG1"): + frappe.delete_doc("Manufacturer", "MSG1") - template = frappe.get_doc(dict( - doctype='Item', - item_code='_Test Variant Mfg', - has_variant=1, - item_group='Products', - variant_based_on='Manufacturer' - )).insert() + template = frappe.get_doc( + dict( + doctype="Item", + item_code="_Test Variant Mfg", + has_variant=1, + item_group="Products", + variant_based_on="Manufacturer", + ) + ).insert() - manufacturer = frappe.get_doc(dict( - doctype='Manufacturer', - short_name='MSG1' - )).insert() + manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert() variant = get_variant(template.name, manufacturer=manufacturer.name) - self.assertEqual(variant.item_code, '_Test Variant Mfg-1') - self.assertEqual(variant.description, '_Test Variant Mfg') - self.assertEqual(variant.manufacturer, 'MSG1') + self.assertEqual(variant.item_code, "_Test Variant Mfg-1") + self.assertEqual(variant.description, "_Test Variant Mfg") + self.assertEqual(variant.manufacturer, "MSG1") variant.insert() - variant = get_variant(template.name, manufacturer=manufacturer.name, - manufacturer_part_no='007') - self.assertEqual(variant.item_code, '_Test Variant Mfg-2') - self.assertEqual(variant.description, '_Test Variant Mfg') - self.assertEqual(variant.manufacturer, 'MSG1') - self.assertEqual(variant.manufacturer_part_no, '007') + variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007") + self.assertEqual(variant.item_code, "_Test Variant Mfg-2") + self.assertEqual(variant.description, "_Test Variant Mfg") + self.assertEqual(variant.manufacturer, "MSG1") + self.assertEqual(variant.manufacturer_part_no, "007") def test_stock_exists_against_template_item(self): - stock_item = frappe.get_all('Stock Ledger Entry', fields = ["item_code"], limit=1) + stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1) if stock_item: item_code = stock_item[0].item_code - item_doc = frappe.get_doc('Item', item_code) + item_doc = frappe.get_doc("Item", item_code) item_doc.has_variants = 1 self.assertRaises(StockExistsForTemplate, item_doc.save) @@ -493,37 +550,27 @@ class TestItem(FrappeTestCase): # Create new item and add barcodes barcode_properties_list = [ - { - "barcode": "0012345678905", - "barcode_type": "EAN" - }, - { - "barcode": "012345678905", - "barcode_type": "UAN" - }, + {"barcode": "0012345678905", "barcode_type": "EAN"}, + {"barcode": "012345678905", "barcode_type": "UAN"}, { "barcode": "ARBITRARY_TEXT", - } + }, ] create_item(item_code) for barcode_properties in barcode_properties_list: - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") new_barcode.update(barcode_properties) item_doc.save() # Check values saved correctly barcodes = frappe.get_all( - 'Item Barcode', - fields=['barcode', 'barcode_type'], - filters={'parent': item_code}) + "Item Barcode", fields=["barcode", "barcode_type"], filters={"parent": item_code} + ) for barcode_properties in barcode_properties_list: - barcode_to_find = barcode_properties['barcode'] - matching_barcodes = [ - x for x in barcodes - if x['barcode'] == barcode_to_find - ] + barcode_to_find = barcode_properties["barcode"] + matching_barcodes = [x for x in barcodes if x["barcode"] == barcode_to_find] self.assertEqual(len(matching_barcodes), 1) details = matching_barcodes[0] @@ -531,20 +578,21 @@ class TestItem(FrappeTestCase): self.assertEqual(value, details.get(key)) # Add barcode again - should cause DuplicateEntryError - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") new_barcode.update(barcode_properties_list[0]) self.assertRaises(frappe.UniqueValidationError, item_doc.save) # Add invalid barcode - should cause InvalidBarcode - item_doc = frappe.get_doc('Item', item_code) - new_barcode = item_doc.append('barcodes') - new_barcode.barcode = '9999999999999' - new_barcode.barcode_type = 'EAN' + item_doc = frappe.get_doc("Item", item_code) + new_barcode = item_doc.append("barcodes") + new_barcode.barcode = "9999999999999" + new_barcode.barcode_type = "EAN" self.assertRaises(InvalidBarcode, item_doc.save) def test_heatmap_data(self): import time + data = get_timeline_data("Item", "_Test Item") self.assertTrue(isinstance(data, dict)) @@ -576,20 +624,17 @@ class TestItem(FrappeTestCase): def test_check_stock_uom_with_bin_no_sle(self): from erpnext.stock.stock_balance import update_bin_qty + item = create_item("_Item with bin qty") item.stock_uom = "Gram" item.save() - update_bin_qty(item.item_code, "_Test Warehouse - _TC", { - "reserved_qty": 10 - }) + update_bin_qty(item.item_code, "_Test Warehouse - _TC", {"reserved_qty": 10}) item.stock_uom = "Kilometer" self.assertRaises(frappe.ValidationError, item.save) - update_bin_qty(item.item_code, "_Test Warehouse - _TC", { - "reserved_qty": 0 - }) + update_bin_qty(item.item_code, "_Test Warehouse - _TC", {"reserved_qty": 0}) item.load_from_db() item.stock_uom = "Kilometer" @@ -624,7 +669,9 @@ class TestItem(FrappeTestCase): @change_settings("Stock Settings", {"sample_retention_warehouse": "_Test Warehouse - _TC"}) def test_retain_sample(self): - item = make_item("_TestRetainSample", {'has_batch_no': 1, 'retain_sample': 1, 'sample_quantity': 1}) + item = make_item( + "_TestRetainSample", {"has_batch_no": 1, "retain_sample": 1, "sample_quantity": 1} + ) self.assertEqual(item.has_batch_no, 1) self.assertEqual(item.retain_sample, 1) @@ -638,10 +685,11 @@ class TestItem(FrappeTestCase): def set_item_variant_settings(fields): - doc = frappe.get_doc('Item Variant Settings') - doc.set('fields', fields) + doc = frappe.get_doc("Item Variant Settings") + doc.set("fields", fields) doc.save() + def make_item_variant(): if not frappe.db.exists("Item", "_Test Variant Item-S"): variant = create_variant("_Test Variant Item", """{"Test Size": "Small"}""") @@ -649,11 +697,23 @@ def make_item_variant(): variant.item_name = "_Test Variant Item-S" variant.save() -test_records = frappe.get_test_records('Item') -def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test Warehouse - _TC", - is_customer_provided_item=None, customer=None, is_purchase_item=None, opening_stock=0, is_fixed_asset=0, - asset_category=None, company="_Test Company"): +test_records = frappe.get_test_records("Item") + + +def create_item( + item_code, + is_stock_item=1, + valuation_rate=0, + warehouse="_Test Warehouse - _TC", + is_customer_provided_item=None, + customer=None, + is_purchase_item=None, + opening_stock=0, + is_fixed_asset=0, + asset_category=None, + company="_Test Company", +): if not frappe.db.exists("Item", item_code): item = frappe.new_doc("Item") item.item_code = item_code @@ -667,11 +727,8 @@ def create_item(item_code, is_stock_item=1, valuation_rate=0, warehouse="_Test W item.valuation_rate = valuation_rate item.is_purchase_item = is_purchase_item item.is_customer_provided_item = is_customer_provided_item - item.customer = customer or '' - item.append("item_defaults", { - "default_warehouse": warehouse, - "company": company - }) + item.customer = customer or "" + item.append("item_defaults", {"default_warehouse": warehouse, "company": company}) item.save() else: item = frappe.get_doc("Item", item_code) diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 766647b32e5..0f93bb9e95b 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -14,8 +14,7 @@ class ItemAlternative(Document): self.validate_duplicate() def has_alternative_item(self): - if (self.item_code and - not frappe.db.get_value('Item', self.item_code, 'allow_alternative_item')): + if self.item_code and not frappe.db.get_value("Item", self.item_code, "allow_alternative_item"): frappe.throw(_("Not allow to set alternative item for the item {0}").format(self.item_code)) def validate_alternative_item(self): @@ -23,19 +22,32 @@ class ItemAlternative(Document): frappe.throw(_("Alternative item must not be same as item code")) item_meta = frappe.get_meta("Item") - fields = ["is_stock_item", "include_item_in_manufacturing","has_serial_no", "has_batch_no", "allow_alternative_item"] + fields = [ + "is_stock_item", + "include_item_in_manufacturing", + "has_serial_no", + "has_batch_no", + "allow_alternative_item", + ] item_data = frappe.db.get_value("Item", self.item_code, fields, as_dict=1) - alternative_item_data = frappe.db.get_value("Item", self.alternative_item_code, fields, as_dict=1) + alternative_item_data = frappe.db.get_value( + "Item", self.alternative_item_code, fields, as_dict=1 + ) for field in fields: - if item_data.get(field) != alternative_item_data.get(field): + if item_data.get(field) != alternative_item_data.get(field): raise_exception, alert = [1, False] if field == "is_stock_item" else [0, True] - frappe.msgprint(_("The value of {0} differs between Items {1} and {2}") \ - .format(frappe.bold(item_meta.get_label(field)), - frappe.bold(self.alternative_item_code), - frappe.bold(self.item_code)), - alert=alert, raise_exception=raise_exception, indicator="Orange") + frappe.msgprint( + _("The value of {0} differs between Items {1} and {2}").format( + frappe.bold(item_meta.get_label(field)), + frappe.bold(self.alternative_item_code), + frappe.bold(self.item_code), + ), + alert=alert, + raise_exception=raise_exception, + indicator="Orange", + ) alternate_item_check_msg = _("Allow Alternative Item must be checked on Item {}") @@ -44,24 +56,30 @@ class ItemAlternative(Document): if self.two_way and not alternative_item_data.allow_alternative_item: frappe.throw(alternate_item_check_msg.format(self.item_code)) - - - def validate_duplicate(self): - if frappe.db.get_value("Item Alternative", {'item_code': self.item_code, - 'alternative_item_code': self.alternative_item_code, 'name': ('!=', self.name)}): + if frappe.db.get_value( + "Item Alternative", + { + "item_code": self.item_code, + "alternative_item_code": self.alternative_item_code, + "name": ("!=", self.name), + }, + ): frappe.throw(_("Already record exists for the item {0}").format(self.item_code)) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql(""" (select alternative_item_code from `tabItem Alternative` + return frappe.db.sql( + """ (select alternative_item_code from `tabItem Alternative` where item_code = %(item_code)s and alternative_item_code like %(txt)s) union (select item_code from `tabItem Alternative` where alternative_item_code = %(item_code)s and item_code like %(txt)s and two_way = 1) limit {0}, {1} - """.format(start, page_len), { - "item_code": filters.get('item_code'), - "txt": '%' + txt + '%' - }) + """.format( + start, page_len + ), + {"item_code": filters.get("item_code"), "txt": "%" + txt + "%"}, + ) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 501c1c1ad3c..d829b2cbf39 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -27,121 +27,181 @@ class TestItemAlternative(FrappeTestCase): make_items() def test_alternative_item_for_subcontract_rm(self): - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', 'BOM') + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) - create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) - create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) + create_stock_reconciliation( + item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + create_stock_reconciliation( + item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) supplier_warehouse = "Test Supplier Warehouse - _TC" - po = create_purchase_order(item = "Test Finished Goods - A", - is_subcontracted='Yes', qty=5, rate=3000, supplier_warehouse=supplier_warehouse) + po = create_purchase_order( + item="Test Finished Goods - A", + is_subcontracted="Yes", + qty=5, + rate=3000, + supplier_warehouse=supplier_warehouse, + ) - rm_item = [{"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 1", "item_name":"Test FG A RW 1", - "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"}, - {"item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 2", "item_name":"Test FG A RW 2", - "qty":5, "warehouse":"_Test Warehouse - _TC", "rate":2000, "amount":10000, "stock_uom":"Nos"}] + rm_item = [ + { + "item_code": "Test Finished Goods - A", + "rm_item_code": "Test FG A RW 1", + "item_name": "Test FG A RW 1", + "qty": 5, + "warehouse": "_Test Warehouse - _TC", + "rate": 2000, + "amount": 10000, + "stock_uom": "Nos", + }, + { + "item_code": "Test Finished Goods - A", + "rm_item_code": "Test FG A RW 2", + "item_name": "Test FG A RW 2", + "qty": 5, + "warehouse": "_Test Warehouse - _TC", + "rate": 2000, + "amount": 10000, + "stock_uom": "Nos", + }, + ] rm_item_string = json.dumps(rm_item) - reserved_qty_for_sub_contract = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract') + reserved_qty_for_sub_contract = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_sub_contract", + ) se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) se.to_warehouse = supplier_warehouse se.insert() - doc = frappe.get_doc('Stock Entry', se.name) + doc = frappe.get_doc("Stock Entry", se.name) for item in doc.items: - if item.item_code == 'Test FG A RW 1': - item.item_code = 'Alternate Item For A RW 1' - item.item_name = 'Alternate Item For A RW 1' - item.description = 'Alternate Item For A RW 1' - item.original_item = 'Test FG A RW 1' + if item.item_code == "Test FG A RW 1": + item.item_code = "Alternate Item For A RW 1" + item.item_name = "Alternate Item For A RW 1" + item.description = "Alternate Item For A RW 1" + item.original_item = "Test FG A RW 1" doc.save() doc.submit() - after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_sub_contract') + after_transfer_reserved_qty_for_sub_contract = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_sub_contract", + ) - self.assertEqual(after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)) + self.assertEqual( + after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5) + ) pr = make_purchase_receipt(po.name) pr.save() - pr = frappe.get_doc('Purchase Receipt', pr.name) + pr = frappe.get_doc("Purchase Receipt", pr.name) status = False for d in pr.supplied_items: - if d.rm_item_code == 'Alternate Item For A RW 1': + if d.rm_item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract') + frappe.db.set_value( + "Buying Settings", + None, + "backflush_raw_materials_of_subcontract_based_on", + "Material Transferred for Subcontract", + ) def test_alternative_item_for_production_rm(self): - create_stock_reconciliation(item_code='Alternate Item For A RW 1', - warehouse='_Test Warehouse - _TC',qty=5, rate=2000) - create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', - qty=5, rate=2000) - pro_order = make_wo_order_test_record(production_item='Test Finished Goods - A', - qty=5, source_warehouse='_Test Warehouse - _TC', wip_warehouse='Test Supplier Warehouse - _TC') + create_stock_reconciliation( + item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + create_stock_reconciliation( + item_code="Test FG A RW 2", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 + ) + pro_order = make_wo_order_test_record( + production_item="Test Finished Goods - A", + qty=5, + source_warehouse="_Test Warehouse - _TC", + wip_warehouse="Test Supplier Warehouse - _TC", + ) - reserved_qty_for_production = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production') + reserved_qty_for_production = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_production", + ) ste = frappe.get_doc(make_stock_entry(pro_order.name, "Material Transfer for Manufacture", 5)) ste.insert() for item in ste.items: - if item.item_code == 'Test FG A RW 1': - item.item_code = 'Alternate Item For A RW 1' - item.item_name = 'Alternate Item For A RW 1' - item.description = 'Alternate Item For A RW 1' - item.original_item = 'Test FG A RW 1' + if item.item_code == "Test FG A RW 1": + item.item_code = "Alternate Item For A RW 1" + item.item_name = "Alternate Item For A RW 1" + item.description = "Alternate Item For A RW 1" + item.original_item = "Test FG A RW 1" ste.submit() - reserved_qty_for_production_after_transfer = frappe.db.get_value('Bin', - {'item_code': 'Test FG A RW 1', 'warehouse': '_Test Warehouse - _TC'}, 'reserved_qty_for_production') + reserved_qty_for_production_after_transfer = frappe.db.get_value( + "Bin", + {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, + "reserved_qty_for_production", + ) - self.assertEqual(reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5)) + self.assertEqual( + reserved_qty_for_production_after_transfer, flt(reserved_qty_for_production - 5) + ) ste1 = frappe.get_doc(make_stock_entry(pro_order.name, "Manufacture", 5)) status = False for d in ste1.items: - if d.item_code == 'Alternate Item For A RW 1': + if d.item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) ste1.submit() + def make_items(): - items = ['Test Finished Goods - A', 'Test FG A RW 1', 'Test FG A RW 2', 'Alternate Item For A RW 1'] + items = [ + "Test Finished Goods - A", + "Test FG A RW 1", + "Test FG A RW 2", + "Alternate Item For A RW 1", + ] for item_code in items: - if not frappe.db.exists('Item', item_code): + if not frappe.db.exists("Item", item_code): create_item(item_code) - create_stock_reconciliation(item_code="Test FG A RW 1", - warehouse='_Test Warehouse - _TC', qty=10, rate=2000) + create_stock_reconciliation( + item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 + ) - if frappe.db.exists('Item', 'Test FG A RW 1'): - doc = frappe.get_doc('Item', 'Test FG A RW 1') + if frappe.db.exists("Item", "Test FG A RW 1"): + doc = frappe.get_doc("Item", "Test FG A RW 1") doc.allow_alternative_item = 1 doc.save() - if frappe.db.exists('Item', 'Test Finished Goods - A'): - doc = frappe.get_doc('Item', 'Test Finished Goods - A') + if frappe.db.exists("Item", "Test Finished Goods - A"): + doc = frappe.get_doc("Item", "Test Finished Goods - A") doc.is_sub_contracted_item = 1 doc.save() - if not frappe.db.get_value('BOM', - {'item': 'Test Finished Goods - A', 'docstatus': 1}): - make_bom(item = 'Test Finished Goods - A', raw_materials = ['Test FG A RW 1', 'Test FG A RW 2']) + if not frappe.db.get_value("BOM", {"item": "Test Finished Goods - A", "docstatus": 1}): + make_bom(item="Test Finished Goods - A", raw_materials=["Test FG A RW 1", "Test FG A RW 2"]) - if not frappe.db.get_value('Warehouse', {'warehouse_name': 'Test Supplier Warehouse'}): - frappe.get_doc({ - 'doctype': 'Warehouse', - 'warehouse_name': 'Test Supplier Warehouse', - 'company': '_Test Company' - }).insert(ignore_permissions=True) + if not frappe.db.get_value("Warehouse", {"warehouse_name": "Test Supplier Warehouse"}): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": "Test Supplier Warehouse", + "company": "_Test Company", + } + ).insert(ignore_permissions=True) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 5a28a9e231c..391ff06918a 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -14,7 +14,9 @@ from erpnext.controllers.item_variant import ( ) -class ItemAttributeIncrementError(frappe.ValidationError): pass +class ItemAttributeIncrementError(frappe.ValidationError): + pass + class ItemAttribute(Document): def __setup__(self): @@ -29,11 +31,12 @@ class ItemAttribute(Document): self.validate_exising_items() def validate_exising_items(self): - '''Validate that if there are existing items with attributes, they are valid''' + """Validate that if there are existing items with attributes, they are valid""" attributes_list = [d.attribute_value for d in self.item_attribute_values] # Get Item Variant Attribute details of variant items - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select i.name, iva.attribute_value as value from @@ -41,13 +44,18 @@ class ItemAttribute(Document): where iva.attribute = %(attribute)s and iva.parent = i.name and - i.variant_of is not null and i.variant_of != ''""", {"attribute" : self.name}, as_dict=1) + i.variant_of is not null and i.variant_of != ''""", + {"attribute": self.name}, + as_dict=1, + ) for item in items: if self.numeric_values: validate_is_incremental(self, self.name, item.value, item.name) else: - validate_item_attribute_value(attributes_list, self.name, item.value, item.name, from_variant=False) + validate_item_attribute_value( + attributes_list, self.name, item.value, item.name, from_variant=False + ) def validate_numeric(self): if self.numeric_values: diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py index 055c22e0c5d..a30f0e999f7 100644 --- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py @@ -4,7 +4,7 @@ import frappe -test_records = frappe.get_test_records('Item Attribute') +test_records = frappe.get_test_records("Item Attribute") from frappe.tests.utils import FrappeTestCase @@ -18,14 +18,16 @@ class TestItemAttribute(FrappeTestCase): frappe.delete_doc("Item Attribute", "_Test_Length") def test_numeric_item_attribute(self): - item_attribute = frappe.get_doc({ - "doctype": "Item Attribute", - "attribute_name": "_Test_Length", - "numeric_values": 1, - "from_range": 0.0, - "to_range": 100.0, - "increment": 0 - }) + item_attribute = frappe.get_doc( + { + "doctype": "Item Attribute", + "attribute_name": "_Test_Length", + "numeric_values": 1, + "from_range": 0.0, + "to_range": 100.0, + "increment": 0, + } + ) self.assertRaises(ItemAttributeIncrementError, item_attribute.save) diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.py b/erpnext/stock/doctype/item_barcode/item_barcode.py index 64c39dabde1..c2c042143ea 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.py +++ b/erpnext/stock/doctype/item_barcode/item_barcode.py @@ -6,4 +6,4 @@ from frappe.model.document import Document class ItemBarcode(Document): - pass + pass diff --git a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py index 469ccd8f2df..b65ba98a8bf 100644 --- a/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py +++ b/erpnext/stock/doctype/item_manufacturer/item_manufacturer.py @@ -18,14 +18,17 @@ class ItemManufacturer(Document): def validate_duplicate_entry(self): if self.is_new(): filters = { - 'item_code': self.item_code, - 'manufacturer': self.manufacturer, - 'manufacturer_part_no': self.manufacturer_part_no + "item_code": self.item_code, + "manufacturer": self.manufacturer, + "manufacturer_part_no": self.manufacturer_part_no, } if frappe.db.exists("Item Manufacturer", filters): - frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}") - .format(self.item_code, self.manufacturer)) + frappe.throw( + _("Duplicate entry against the item code {0} and manufacturer {1}").format( + self.item_code, self.manufacturer + ) + ) def manage_default_item_manufacturer(self, delete=False): from frappe.model.utils import set_default @@ -37,11 +40,9 @@ class ItemManufacturer(Document): if not self.is_default: # if unchecked and default in Item master, clear it. if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no: - frappe.db.set_value("Item", item.name, - { - "default_item_manufacturer": None, - "default_manufacturer_part_no": None - }) + frappe.db.set_value( + "Item", item.name, {"default_item_manufacturer": None, "default_manufacturer_part_no": None} + ) elif self.is_default: set_default(self, "item_code") @@ -50,18 +51,26 @@ class ItemManufacturer(Document): if delete: manufacturer, manufacturer_part_no = None, None - elif (default_manufacturer != self.manufacturer) or \ - (default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no): + elif (default_manufacturer != self.manufacturer) or ( + default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no + ): manufacturer = self.manufacturer manufacturer_part_no = self.manufacturer_part_no - frappe.db.set_value("Item", item.name, - { - "default_item_manufacturer": manufacturer, - "default_manufacturer_part_no": manufacturer_part_no - }) + frappe.db.set_value( + "Item", + item.name, + { + "default_item_manufacturer": manufacturer, + "default_manufacturer_part_no": manufacturer_part_no, + }, + ) + @frappe.whitelist() def get_item_manufacturer_part_no(item_code, manufacturer): - return frappe.db.get_value("Item Manufacturer", - {'item_code': item_code, 'manufacturer': manufacturer}, 'manufacturer_part_no') + return frappe.db.get_value( + "Item Manufacturer", + {"item_code": item_code, "manufacturer": manufacturer}, + "manufacturer_part_no", + ) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 010e01a78ba..562f7b9e12f 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -13,7 +13,6 @@ class ItemPriceDuplicateItem(frappe.ValidationError): class ItemPrice(Document): - def validate(self): self.validate_item() self.validate_dates() @@ -32,22 +31,26 @@ class ItemPrice(Document): def update_price_list_details(self): if self.price_list: - price_list_details = frappe.db.get_value("Price List", - {"name": self.price_list, "enabled": 1}, - ["buying", "selling", "currency"]) + price_list_details = frappe.db.get_value( + "Price List", {"name": self.price_list, "enabled": 1}, ["buying", "selling", "currency"] + ) if not price_list_details: - link = frappe.utils.get_link_to_form('Price List', self.price_list) + link = frappe.utils.get_link_to_form("Price List", self.price_list) frappe.throw("The price list {0} does not exist or is disabled".format(link)) self.buying, self.selling, self.currency = price_list_details def update_item_details(self): if self.item_code: - self.item_name, self.item_description = frappe.db.get_value("Item", self.item_code,["item_name", "description"]) + self.item_name, self.item_description = frappe.db.get_value( + "Item", self.item_code, ["item_name", "description"] + ) def check_duplicates(self): - conditions = """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" + conditions = ( + """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s""" + ) for field in [ "uom", @@ -56,21 +59,31 @@ class ItemPrice(Document): "packing_unit", "customer", "supplier", - "batch_no"]: + "batch_no", + ]: if self.get(field): conditions += " and {0} = %({0})s ".format(field) else: conditions += "and (isnull({0}) or {0} = '')".format(field) - price_list_rate = frappe.db.sql(""" + price_list_rate = frappe.db.sql( + """ select price_list_rate from `tabItem Price` {conditions} - """.format(conditions=conditions), - self.as_dict(),) + """.format( + conditions=conditions + ), + self.as_dict(), + ) if price_list_rate: - frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,) + frappe.throw( + _( + "Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates." + ), + ItemPriceDuplicateItem, + ) def before_save(self): if self.selling: diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index 6ceba3f8d3f..30d933e247d 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -23,8 +23,14 @@ class TestItemPrice(FrappeTestCase): def test_addition_of_new_fields(self): # Based on https://github.com/frappe/erpnext/issues/8456 test_fields_existance = [ - 'supplier', 'customer', 'uom', 'lead_time_days', - 'packing_unit', 'valid_from', 'valid_upto', 'note' + "supplier", + "customer", + "uom", + "lead_time_days", + "packing_unit", + "valid_from", + "valid_upto", + "note", ] doc_fields = frappe.copy_doc(test_records[1]).__dict__.keys() @@ -45,10 +51,10 @@ class TestItemPrice(FrappeTestCase): args = { "price_list": doc.price_list, - "customer": doc.customer, - "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 10 + "customer": doc.customer, + "uom": "_Test UOM", + "transaction_date": "2017-04-18", + "qty": 10, } price = get_price_list_rate_for(process_args(args), doc.item_code) @@ -61,13 +67,12 @@ class TestItemPrice(FrappeTestCase): "price_list": doc.price_list, "customer": doc.customer, "uom": "_Test UOM", - "transaction_date": '2017-04-18', + "transaction_date": "2017-04-18", } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, None) - def test_prices_at_date(self): # Check correct price at first date doc = frappe.copy_doc(test_records[2]) @@ -76,35 +81,35 @@ class TestItemPrice(FrappeTestCase): "price_list": doc.price_list, "customer": "_Test Customer", "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 7 + "transaction_date": "2017-04-18", + "qty": 7, } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, 20) def test_prices_at_invalid_date(self): - #Check correct price at invalid date + # Check correct price at invalid date doc = frappe.copy_doc(test_records[3]) args = { "price_list": doc.price_list, "qty": 7, "uom": "_Test UOM", - "transaction_date": "01-15-2019" + "transaction_date": "01-15-2019", } price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, None) def test_prices_outside_of_date(self): - #Check correct price when outside of the date + # Check correct price when outside of the date doc = frappe.copy_doc(test_records[4]) args = { "price_list": doc.price_list, - "customer": "_Test Customer", - "uom": "_Test UOM", + "customer": "_Test Customer", + "uom": "_Test UOM", "transaction_date": "2017-04-25", "qty": 7, } @@ -113,7 +118,7 @@ class TestItemPrice(FrappeTestCase): self.assertEqual(price, None) def test_lowest_price_when_no_date_provided(self): - #Check lowest price when no date provided + # Check lowest price when no date provided doc = frappe.copy_doc(test_records[1]) args = { @@ -125,7 +130,6 @@ class TestItemPrice(FrappeTestCase): price = get_price_list_rate_for(args, doc.item_code) self.assertEqual(price, 10) - def test_invalid_item(self): doc = frappe.copy_doc(test_records[1]) # Enter invalid item code @@ -150,8 +154,8 @@ class TestItemPrice(FrappeTestCase): args = { "price_list": doc.price_list, "uom": "_Test UOM", - "transaction_date": '2017-04-18', - "qty": 7 + "transaction_date": "2017-04-18", + "qty": 7, } price = get_price_list_rate_for(args, doc.item_code) @@ -159,4 +163,5 @@ class TestItemPrice(FrappeTestCase): self.assertEqual(price, 21) -test_records = frappe.get_test_records('Item Price') + +test_records = frappe.get_test_records("Item Price") diff --git a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py index 62bf842be83..49f311684c2 100644 --- a/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py +++ b/erpnext/stock/doctype/item_variant_settings/item_variant_settings.py @@ -8,29 +8,46 @@ from frappe.model.document import Document class ItemVariantSettings(Document): - invalid_fields_for_copy_fields_in_variants = ['barcodes'] + invalid_fields_for_copy_fields_in_variants = ["barcodes"] def set_default_fields(self): self.fields = [] - fields = frappe.get_meta('Item').fields - exclude_fields = {"naming_series", "item_code", "item_name", "published_in_website", - "standard_rate", "opening_stock", "image", "description", - "variant_of", "valuation_rate", "description", "barcodes", - "has_variants", "attributes"} + fields = frappe.get_meta("Item").fields + exclude_fields = { + "naming_series", + "item_code", + "item_name", + "published_in_website", + "standard_rate", + "opening_stock", + "image", + "description", + "variant_of", + "valuation_rate", + "description", + "barcodes", + "has_variants", + "attributes", + } for d in fields: - if not d.no_copy and d.fieldname not in exclude_fields and \ - d.fieldtype not in ['HTML', 'Section Break', 'Column Break', 'Button', 'Read Only']: - self.append('fields', { - 'field_name': d.fieldname - }) + if ( + not d.no_copy + and d.fieldname not in exclude_fields + and d.fieldtype not in ["HTML", "Section Break", "Column Break", "Button", "Read Only"] + ): + self.append("fields", {"field_name": d.fieldname}) def remove_invalid_fields_for_copy_fields_in_variants(self): - fields = [row for row in self.fields if row.field_name not in self.invalid_fields_for_copy_fields_in_variants] + fields = [ + row + for row in self.fields + if row.field_name not in self.invalid_fields_for_copy_fields_in_variants + ] self.fields = fields self.save() def validate(self): for d in self.fields: if d.field_name in self.invalid_fields_for_copy_fields_in_variants: - frappe.throw(_('Cannot set the field {0} for copying in variants').format(d.field_name)) + frappe.throw(_("Cannot set the field {0} for copying in variants").format(d.field_name)) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 7aff95d1e81..b3af309359a 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -19,13 +19,19 @@ class LandedCostVoucher(Document): self.set("items", []) for pr in self.get("purchase_receipts"): if pr.receipt_document_type and pr.receipt_document: - pr_items = frappe.db.sql("""select pr_item.item_code, pr_item.description, + pr_items = frappe.db.sql( + """select pr_item.item_code, pr_item.description, pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name, pr_item.cost_center, pr_item.is_fixed_asset from `tab{doctype} Item` pr_item where parent = %s and exists(select name from tabItem where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1)) - """.format(doctype=pr.receipt_document_type), pr.receipt_document, as_dict=True) + """.format( + doctype=pr.receipt_document_type + ), + pr.receipt_document, + as_dict=True, + ) for d in pr_items: item = self.append("items") @@ -33,8 +39,7 @@ class LandedCostVoucher(Document): item.description = d.description item.qty = d.qty item.rate = d.base_rate - item.cost_center = d.cost_center or \ - erpnext.get_default_cost_center(self.company) + item.cost_center = d.cost_center or erpnext.get_default_cost_center(self.company) item.amount = d.base_amount item.receipt_document_type = pr.receipt_document_type item.receipt_document = pr.receipt_document @@ -52,26 +57,30 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() self.validate_applicable_charges_for_item() - def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) - def validate_receipt_documents(self): receipt_documents = [] for d in self.get("purchase_receipts"): docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") if docstatus != 1: - msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" + msg = ( + f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted" + ) frappe.throw(_(msg), title=_("Invalid Document")) if d.receipt_document_type == "Purchase Invoice": update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock") if not update_stock: - msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document)) - msg += "
    " + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.") + msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format( + d.idx, frappe.bold(d.receipt_document) + ) + msg += "
    " + _( + "Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled." + ) frappe.throw(msg, title=_("Incorrect Invoice")) receipt_documents.append(d.receipt_document) @@ -81,52 +90,64 @@ class LandedCostVoucher(Document): frappe.throw(_("Item must be added using 'Get Items from Purchase Receipts' button")) elif item.receipt_document not in receipt_documents: - frappe.throw(_("Item Row {0}: {1} {2} does not exist in above '{1}' table") - .format(item.idx, item.receipt_document_type, item.receipt_document)) + frappe.throw( + _("Item Row {0}: {1} {2} does not exist in above '{1}' table").format( + item.idx, item.receipt_document_type, item.receipt_document + ) + ) if not item.cost_center: - frappe.throw(_("Row {0}: Cost center is required for an item {1}") - .format(item.idx, item.item_code)) + frappe.throw( + _("Row {0}: Cost center is required for an item {1}").format(item.idx, item.item_code) + ) def set_total_taxes_and_charges(self): self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes")) def set_applicable_charges_on_item(self): - if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually': + if self.get("taxes") and self.distribute_charges_based_on != "Distribute Manually": total_item_cost = 0.0 total_charges = 0.0 item_count = 0 based_on_field = frappe.scrub(self.distribute_charges_based_on) - for item in self.get('items'): + for item in self.get("items"): total_item_cost += item.get(based_on_field) - for item in self.get('items'): - item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), - item.precision('applicable_charges')) + for item in self.get("items"): + item.applicable_charges = flt( + flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)), + item.precision("applicable_charges"), + ) total_charges += item.applicable_charges item_count += 1 if total_charges != self.total_taxes_and_charges: diff = self.total_taxes_and_charges - total_charges - self.get('items')[item_count - 1].applicable_charges += diff + self.get("items")[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): based_on = self.distribute_charges_based_on.lower() - if based_on != 'distribute manually': + if based_on != "distribute manually": total = sum(flt(d.get(based_on)) for d in self.get("items")) else: # consider for proportion while distributing manually - total = sum(flt(d.get('applicable_charges')) for d in self.get("items")) + total = sum(flt(d.get("applicable_charges")) for d in self.get("items")) if not total: - frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on)) + frappe.throw( + _( + "Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'" + ).format(based_on) + ) total_applicable_charges = sum(flt(d.applicable_charges) for d in self.get("items")) - precision = get_field_precision(frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), - currency=frappe.get_cached_value('Company', self.company, "default_currency")) + precision = get_field_precision( + frappe.get_meta("Landed Cost Item").get_field("applicable_charges"), + currency=frappe.get_cached_value("Company", self.company, "default_currency"), + ) diff = flt(self.total_taxes_and_charges) - flt(total_applicable_charges) diff = flt(diff, precision) @@ -134,7 +155,11 @@ class LandedCostVoucher(Document): if abs(diff) < (2.0 / (10**precision)): self.items[-1].applicable_charges += diff else: - frappe.throw(_("Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges")) + frappe.throw( + _( + "Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges" + ) + ) def on_submit(self): self.update_landed_cost() @@ -177,25 +202,41 @@ class LandedCostVoucher(Document): doc.repost_future_sle_and_gle() def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): - for item in self.get('items'): + for item in self.get("items"): if item.is_fixed_asset: - receipt_document_type = 'purchase_invoice' if item.receipt_document_type == 'Purchase Invoice' \ - else 'purchase_receipt' - docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, - 'item_code': item.item_code }, fields=['name', 'docstatus']) + receipt_document_type = ( + "purchase_invoice" if item.receipt_document_type == "Purchase Invoice" else "purchase_receipt" + ) + docs = frappe.db.get_all( + "Asset", + filters={receipt_document_type: item.receipt_document, "item_code": item.item_code}, + fields=["name", "docstatus"], + ) if not docs or len(docs) != item.qty: - frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format( - item.receipt_document, item.qty)) + frappe.throw( + _( + "There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document." + ).format(item.receipt_document, item.qty) + ) if docs: for d in docs: if d.docstatus == 1: - frappe.throw(_('{2} {0} has submitted Assets. Remove Item {1} from table to continue.').format( - item.receipt_document, item.item_code, item.receipt_document_type)) + frappe.throw( + _( + "{2} {0} has submitted Assets. Remove Item {1} from table to continue." + ).format( + item.receipt_document, item.item_code, item.receipt_document_type + ) + ) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): if not item.is_fixed_asset and item.serial_no: serial_nos = get_serial_nos(item.serial_no) if serial_nos: - frappe.db.sql("update `tabSerial No` set purchase_rate=%s where name in ({0})" - .format(", ".join(["%s"]*len(serial_nos))), tuple([item.valuation_rate] + serial_nos)) + frappe.db.sql( + "update `tabSerial No` set purchase_rate=%s where name in ({0})".format( + ", ".join(["%s"] * len(serial_nos)) + ), + tuple([item.valuation_rate] + serial_nos), + ) diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 6dc4fee5697..1af99534516 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, flt, now @@ -22,34 +21,50 @@ class TestLandedCostVoucher(FrappeTestCase): def test_landed_cost_voucher(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", - get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + ) - last_sle = frappe.db.get_value("Stock Ledger Entry", { + last_sle = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", "warehouse": "Stores - TCP1", "is_cancelled": 0, }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount") + pr_lc_value = frappe.db.get_value( + "Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount" + ) self.assertEqual(pr_lc_value, 25.0) - last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { + last_sle_after_landed_cost = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pr.doctype, "voucher_no": pr.name, "item_code": "_Test Item", "warehouse": "Stores - TCP1", "is_cancelled": 0, }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) - self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) + self.assertEqual( + last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction + ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0) # assert after submit @@ -57,24 +72,20 @@ class TestLandedCostVoucher(FrappeTestCase): # Mess up cancelled SLE modified timestamp to check # if they aren't effective in any business logic. - frappe.db.set_value("Stock Ledger Entry", - { - "is_cancelled": 1, - "voucher_type": pr.doctype, - "voucher_no": pr.name - }, - "is_cancelled", 1, - modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True) + frappe.db.set_value( + "Stock Ledger Entry", + {"is_cancelled": 1, "voucher_type": pr.doctype, "voucher_no": pr.name}, + "is_cancelled", + 1, + modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True), ) items, warehouses = pr.get_items_and_warehouses() - update_gl_entries_after(pr.posting_date, pr.posting_time, - warehouses, items, company=pr.company) + update_gl_entries_after(pr.posting_date, pr.posting_time, warehouses, items, company=pr.company) # reassert after reposting self.assertPurchaseReceiptLCVGLEntries(pr) - def assertPurchaseReceiptLCVGLEntries(self, pr): gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -90,54 +101,74 @@ class TestLandedCostVoucher(FrappeTestCase): "Stock Received But Not Billed - TCP1": [0.0, 500.0], "Expenses Included In Valuation - TCP1": [0.0, 50.0], "_Test Account Customs Duty - TCP1": [0.0, 150], - "_Test Account Shipping Charges - TCP1": [0.0, 100.00] + "_Test Account Shipping Charges - TCP1": [0.0, 100.00], } else: expected_values = { stock_in_hand_account: [400.0, 0.0], fixed_asset_account: [400.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "Expenses Included In Valuation - TCP1": [0.0, 300.0] + "Expenses Included In Valuation - TCP1": [0.0, 300.0], } for gle in gl_entries: - if not gle.get('is_cancelled'): - self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}") - self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}") - + if not gle.get("is_cancelled"): + self.assertEqual( + expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}" + ) + self.assertEqual( + expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}" + ) def test_landed_cost_voucher_against_purchase_invoice(self): - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", - company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", - warehouse= "Stores - TCP1", cost_center = "Main - TCP1", - expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + cash_bank_account="Cash - TCP1", + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) - last_sle = frappe.db.get_value("Stock Ledger Entry", { + last_sle = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pi.doctype, "voucher_no": pi.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) - pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, - "landed_cost_voucher_amount") + pi_lc_value = frappe.db.get_value( + "Purchase Invoice Item", {"parent": pi.name}, "landed_cost_voucher_amount" + ) self.assertEqual(pi_lc_value, 50.0) - last_sle_after_landed_cost = frappe.db.get_value("Stock Ledger Entry", { + last_sle_after_landed_cost = frappe.db.get_value( + "Stock Ledger Entry", + { "voucher_type": pi.doctype, "voucher_no": pi.name, "item_code": "_Test Item", - "warehouse": "Stores - TCP1" + "warehouse": "Stores - TCP1", }, - fieldname=["qty_after_transaction", "stock_value"], as_dict=1) + fieldname=["qty_after_transaction", "stock_value"], + as_dict=1, + ) - self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction) + self.assertEqual( + last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction + ) self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0) @@ -149,20 +180,26 @@ class TestLandedCostVoucher(FrappeTestCase): expected_values = { stock_in_hand_account: [300.0, 0.0], "Creditors - TCP1": [0.0, 250.0], - "Expenses Included In Valuation - TCP1": [0.0, 50.0] + "Expenses Included In Valuation - TCP1": [0.0, 50.0], } for gle in gl_entries: - if not gle.get('is_cancelled'): + if not gle.get("is_cancelled"): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) - def test_landed_cost_voucher_for_serialized_item(self): - frappe.db.sql("delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')") - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, - get_taxes_and_charges = True, do_not_submit = True) + frappe.db.sql( + "delete from `tabSerial No` where name in ('SN001', 'SN002', 'SN003', 'SN004', 'SN005')" + ) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + do_not_submit=True, + ) pr.items[0].item_code = "_Test Serialized Item" pr.items[0].serial_no = "SN001\nSN002\nSN003\nSN004\nSN005" @@ -172,8 +209,7 @@ class TestLandedCostVoucher(FrappeTestCase): create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - serial_no = frappe.db.get_value("Serial No", "SN001", - ["warehouse", "purchase_rate"], as_dict=1) + serial_no = frappe.db.get_value("Serial No", "SN001", ["warehouse", "purchase_rate"], as_dict=1) self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) self.assertEqual(serial_no.warehouse, "Stores - TCP1") @@ -183,60 +219,82 @@ class TestLandedCostVoucher(FrappeTestCase): landed costs, this should be allowed for serial nos too. Case: - - receipt a serial no @ X rate - - delivery the serial no @ X rate - - add LCV to receipt X + Y - - LCV should be successful - - delivery should reflect X+Y valuation. + - receipt a serial no @ X rate + - delivery the serial no @ X rate + - add LCV to receipt X + Y + - LCV should be successful + - delivery should reflect X+Y valuation. """ serial_no = "LCV_TEST_SR_NO" item_code = "_Test Serialized Item" warehouse = "Stores - TCP1" - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse=warehouse, qty=1, rate=200, - item_code=item_code, serial_no=serial_no) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse=warehouse, + qty=1, + rate=200, + item_code=item_code, + serial_no=serial_no, + ) serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") # deliver it before creating LCV - dn = create_delivery_note(item_code=item_code, - company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', - serial_no=serial_no, qty=1, rate=500, - cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + dn = create_delivery_note( + item_code=item_code, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + serial_no=serial_no, + qty=1, + rate=500, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + ) charges = 10 create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) new_purchase_rate = serial_no_rate + charges - serial_no = frappe.db.get_value("Serial No", serial_no, - ["warehouse", "purchase_rate"], as_dict=1) + serial_no = frappe.db.get_value( + "Serial No", serial_no, ["warehouse", "purchase_rate"], as_dict=1 + ) self.assertEqual(serial_no.purchase_rate, new_purchase_rate) - stock_value_difference = frappe.db.get_value("Stock Ledger Entry", - filters={ - "voucher_no": dn.name, - "voucher_type": dn.doctype, - "is_cancelled": 0 # LCV cancels with same name. - }, - fieldname="stock_value_difference") + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + filters={ + "voucher_no": dn.name, + "voucher_type": dn.doctype, + "is_cancelled": 0, # LCV cancels with same name. + }, + fieldname="stock_value_difference", + ) # reposting should update the purchase rate in future delivery self.assertEqual(stock_value_difference, -new_purchase_rate) - def test_landed_cost_voucher_for_odd_numbers (self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) + def test_landed_cost_voucher_for_odd_numbers(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + do_not_save=True, + ) pr.items[0].cost_center = "Main - TCP1" for x in range(2): - pr.append("items", { - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "cost_center": "Main - TCP1", - "qty": 5, - "rate": 50 - }) + pr.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "cost_center": "Main - TCP1", + "qty": 5, + "rate": 50, + }, + ) pr.submit() lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) @@ -245,37 +303,50 @@ class TestLandedCostVoucher(FrappeTestCase): self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08) def test_multiple_landed_cost_voucher_against_pr(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Stores - TCP1", do_not_save=True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Stores - TCP1", + do_not_save=True, + ) - pr.append("items", { - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "cost_center": "Main - TCP1", - "qty": 5, - "rate": 100 - }) + pr.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "cost_center": "Main - TCP1", + "qty": 5, + "rate": 100, + }, + ) pr.submit() - lcv1 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', - receipt_document=pr.name, charges=100, do_not_save=True) + lcv1 = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) lcv1.insert() - lcv1.set('items', [ - lcv1.get('items')[0] - ]) + lcv1.set("items", [lcv1.get("items")[0]]) distribute_landed_cost_on_items(lcv1) lcv1.submit() - lcv2 = make_landed_cost_voucher(company = pr.company, receipt_document_type = 'Purchase Receipt', - receipt_document=pr.name, charges=100, do_not_save=True) + lcv2 = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) lcv2.insert() - lcv2.set('items', [ - lcv2.get('items')[1] - ]) + lcv2.set("items", [lcv2.get("items")[1]]) distribute_landed_cost_on_items(lcv2) lcv2.submit() @@ -294,22 +365,31 @@ class TestLandedCostVoucher(FrappeTestCase): save_new_records(test_records) ## Create USD Shipping charges_account - usd_shipping = create_account(account_name="Shipping Charges USD", - parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", - account_currency="USD") + usd_shipping = create_account( + account_name="Shipping Charges USD", + parent_account="Duties and Taxes - TCP1", + company="_Test Company with perpetual inventory", + account_currency="USD", + ) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", - supplier_warehouse = "Stores - TCP1") + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Stores - TCP1", + ) pr.submit() - lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt", - receipt_document=pr.name, charges=100, do_not_save=True) + lcv = make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=100, + do_not_save=True, + ) - lcv.append("taxes", { - "description": "Shipping Charges", - "expense_account": usd_shipping, - "amount": 10 - }) + lcv.append( + "taxes", {"description": "Shipping Charges", "expense_account": usd_shipping, "amount": 10} + ) lcv.save() lcv.submit() @@ -319,12 +399,18 @@ class TestLandedCostVoucher(FrappeTestCase): self.assertEqual(lcv.total_taxes_and_charges, 729) self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729) - gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"], - filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])}) + gl_entries = frappe.get_all( + "GL Entry", + fields=["account", "credit", "credit_in_account_currency"], + filters={ + "voucher_no": pr.name, + "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"]), + }, + ) expected_gl_entries = { "Shipping Charges USD - TCP1": [629, 10], - "Expenses Included In Valuation - TCP1": [100, 100] + "Expenses Included In Valuation - TCP1": [100, 100], } for entry in gl_entries: @@ -334,7 +420,9 @@ class TestLandedCostVoucher(FrappeTestCase): def test_asset_lcv(self): "Check if LCV for an Asset updates the Assets Gross Purchase Amount correctly." - frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC") + frappe.db.set_value( + "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" + ) if not frappe.db.exists("Asset Category", "Computers"): create_asset_category() @@ -345,15 +433,16 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=50000) # check if draft asset was created - assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name}) self.assertEqual(len(assets), 1) lcv = make_landed_cost_voucher( - company = pr.company, - receipt_document_type = "Purchase Receipt", + company=pr.company, + receipt_document_type="Purchase Receipt", receipt_document=pr.name, charges=80, - expense_account="Expenses Included In Valuation - _TC") + expense_account="Expenses Included In Valuation - _TC", + ) lcv.save() lcv.submit() @@ -365,27 +454,38 @@ class TestLandedCostVoucher(FrappeTestCase): lcv.cancel() pr.cancel() -def make_landed_cost_voucher(** args): + +def make_landed_cost_voucher(**args): args = frappe._dict(args) ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document) - lcv = frappe.new_doc('Landed Cost Voucher') - lcv.company = args.company or '_Test Company' - lcv.distribute_charges_based_on = 'Amount' + lcv = frappe.new_doc("Landed Cost Voucher") + lcv.company = args.company or "_Test Company" + lcv.distribute_charges_based_on = "Amount" - lcv.set('purchase_receipts', [{ - "receipt_document_type": args.receipt_document_type, - "receipt_document": args.receipt_document, - "supplier": ref_doc.supplier, - "posting_date": ref_doc.posting_date, - "grand_total": ref_doc.grand_total - }]) + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": args.receipt_document_type, + "receipt_document": args.receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.grand_total, + } + ], + ) - lcv.set("taxes", [{ - "description": "Shipping Charges", - "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", - "amount": args.charges - }]) + lcv.set( + "taxes", + [ + { + "description": "Shipping Charges", + "expense_account": args.expense_account or "Expenses Included In Valuation - TCP1", + "amount": args.charges, + } + ], + ) if not args.do_not_save: lcv.insert() @@ -400,21 +500,31 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, lcv = frappe.new_doc("Landed Cost Voucher") lcv.company = company - lcv.distribute_charges_based_on = 'Amount' + lcv.distribute_charges_based_on = "Amount" - lcv.set("purchase_receipts", [{ - "receipt_document_type": receipt_document_type, - "receipt_document": receipt_document, - "supplier": ref_doc.supplier, - "posting_date": ref_doc.posting_date, - "grand_total": ref_doc.base_grand_total - }]) + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": receipt_document_type, + "receipt_document": receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.base_grand_total, + } + ], + ) - lcv.set("taxes", [{ - "description": "Insurance Charges", - "expense_account": "Expenses Included In Valuation - TCP1", - "amount": charges - }]) + lcv.set( + "taxes", + [ + { + "description": "Insurance Charges", + "expense_account": "Expenses Included In Valuation - TCP1", + "amount": charges, + } + ], + ) lcv.insert() @@ -424,6 +534,7 @@ def create_landed_cost_voucher(receipt_document_type, receipt_document, company, return lcv + def distribute_landed_cost_on_items(lcv): based_on = lcv.distribute_charges_based_on.lower() total = sum(flt(d.get(based_on)) for d in lcv.get("items")) @@ -432,4 +543,5 @@ def distribute_landed_cost_on_items(lcv): item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total) item.applicable_charges = flt(item.applicable_charges, lcv.precision("applicable_charges", item)) -test_records = frappe.get_test_records('Landed Cost Voucher') + +test_records = frappe.get_test_records("Landed Cost Voucher") diff --git a/erpnext/stock/doctype/manufacturer/test_manufacturer.py b/erpnext/stock/doctype/manufacturer/test_manufacturer.py index 66323478c83..e176b28b85a 100644 --- a/erpnext/stock/doctype/manufacturer/test_manufacturer.py +++ b/erpnext/stock/doctype/manufacturer/test_manufacturer.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Manufacturer') + class TestManufacturer(unittest.TestCase): pass diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 341c83023a3..415c45cf1fe 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -19,9 +19,8 @@ from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty -form_grid_templates = { - "items": "templates/form_grid/material_request_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"} + class MaterialRequest(BuyingController): def get_feed(self): @@ -31,8 +30,8 @@ class MaterialRequest(BuyingController): pass def validate_qty_against_so(self): - so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} - for d in self.get('items'): + so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} + for d in self.get("items"): if d.sales_order: if not d.sales_order in so_items: so_items[d.sales_order] = {d.item_code: flt(d.qty)} @@ -44,24 +43,34 @@ class MaterialRequest(BuyingController): for so_no in so_items.keys(): for item in so_items[so_no].keys(): - already_indented = frappe.db.sql("""select sum(qty) + already_indented = frappe.db.sql( + """select sum(qty) from `tabMaterial Request Item` where item_code = %s and sales_order = %s and - docstatus = 1 and parent != %s""", (item, so_no, self.name)) + docstatus = 1 and parent != %s""", + (item, so_no, self.name), + ) already_indented = already_indented and flt(already_indented[0][0]) or 0 - actual_so_qty = frappe.db.sql("""select sum(stock_qty) from `tabSales Order Item` - where parent = %s and item_code = %s and docstatus = 1""", (so_no, item)) + actual_so_qty = frappe.db.sql( + """select sum(stock_qty) from `tabSales Order Item` + where parent = %s and item_code = %s and docstatus = 1""", + (so_no, item), + ) actual_so_qty = actual_so_qty and flt(actual_so_qty[0][0]) or 0 if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): - frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no)) + frappe.throw( + _("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format( + actual_so_qty - already_indented, item, so_no + ) + ) def validate(self): super(MaterialRequest, self).validate() self.validate_schedule_date() - self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order') + self.check_for_on_hold_or_closed_status("Sales Order", "sales_order") self.validate_uom_is_integer("uom", "qty") self.validate_material_request_type() @@ -69,9 +78,22 @@ class MaterialRequest(BuyingController): self.status = "Draft" from erpnext.controllers.status_updater import validate_status - validate_status(self.status, - ["Draft", "Submitted", "Stopped", "Cancelled", "Pending", - "Partially Ordered", "Ordered", "Issued", "Transferred", "Received"]) + + validate_status( + self.status, + [ + "Draft", + "Submitted", + "Stopped", + "Cancelled", + "Pending", + "Partially Ordered", + "Ordered", + "Issued", + "Transferred", + "Received", + ], + ) validate_for_items(self) @@ -87,22 +109,22 @@ class MaterialRequest(BuyingController): self.validate_schedule_date() def validate_material_request_type(self): - """ Validate fields in accordance with selected type """ + """Validate fields in accordance with selected type""" if self.material_request_type != "Customer Provided": self.customer = None def set_title(self): - '''Set title as comma separated list of items''' + """Set title as comma separated list of items""" if not self.title: - items = ', '.join([d.item_name for d in self.items][:3]) - self.title = _('{0} Request for {1}').format(self.material_request_type, items)[:100] + items = ", ".join([d.item_name for d in self.items][:3]) + self.title = _("{0} Request for {1}").format(self.material_request_type, items)[:100] def on_submit(self): # frappe.db.set(self, 'status', 'Submitted') self.update_requested_qty() self.update_requested_qty_in_production_plan() - if self.material_request_type == 'Purchase': + if self.material_request_type == "Purchase": self.validate_budget() def before_save(self): @@ -115,13 +137,15 @@ class MaterialRequest(BuyingController): # if MRQ is already closed, no point saving the document check_on_hold_or_closed_status(self.doctype, self.name) - self.set_status(update=True, status='Cancelled') + self.set_status(update=True, status="Cancelled") def check_modified_date(self): - mod_db = frappe.db.sql("""select modified from `tabMaterial Request` where name = %s""", - self.name) - date_diff = frappe.db.sql("""select TIMEDIFF('%s', '%s')""" - % (mod_db[0][0], cstr(self.modified))) + mod_db = frappe.db.sql( + """select modified from `tabMaterial Request` where name = %s""", self.name + ) + date_diff = frappe.db.sql( + """select TIMEDIFF('%s', '%s')""" % (mod_db[0][0], cstr(self.modified)) + ) if date_diff and date_diff[0][0]: frappe.throw(_("{0} {1} has been modified. Please refresh.").format(_(self.doctype), self.name)) @@ -137,22 +161,24 @@ class MaterialRequest(BuyingController): validates that `status` is acceptable for the present controller status and throws an Exception if otherwise. """ - if self.status and self.status == 'Cancelled': + if self.status and self.status == "Cancelled": # cancelled documents cannot change if status != self.status: frappe.throw( - _("{0} {1} is cancelled so the action cannot be completed"). - format(_(self.doctype), self.name), - frappe.InvalidStatusError + _("{0} {1} is cancelled so the action cannot be completed").format( + _(self.doctype), self.name + ), + frappe.InvalidStatusError, ) - elif self.status and self.status == 'Draft': + elif self.status and self.status == "Draft": # draft document to pending only - if status != 'Pending': + if status != "Pending": frappe.throw( - _("{0} {1} has not been submitted so the action cannot be completed"). - format(_(self.doctype), self.name), - frappe.InvalidStatusError + _("{0} {1} has not been submitted so the action cannot be completed").format( + _(self.doctype), self.name + ), + frappe.InvalidStatusError, ) def on_cancel(self): @@ -169,67 +195,90 @@ class MaterialRequest(BuyingController): for d in self.get("items"): if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): - d.ordered_qty = flt(frappe.db.sql("""select sum(transfer_qty) + d.ordered_qty = flt( + frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where material_request = %s and material_request_item = %s and docstatus = 1""", - (self.name, d.name))[0][0]) - mr_qty_allowance = frappe.db.get_single_value('Stock Settings', 'mr_qty_allowance') + (self.name, d.name), + )[0][0] + ) + mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") if mr_qty_allowance: - allowed_qty = d.qty + (d.qty * (mr_qty_allowance/100)) + allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) if d.ordered_qty and d.ordered_qty > allowed_qty: - frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ - cannot be greater than allowed requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, allowed_qty, d.item_code)) + frappe.throw( + _( + "The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than allowed requested quantity {2} for Item {3}" + ).format(d.ordered_qty, d.parent, allowed_qty, d.item_code) + ) elif d.ordered_qty and d.ordered_qty > d.stock_qty: - frappe.throw(_("The total Issue / Transfer quantity {0} in Material Request {1} \ - cannot be greater than requested quantity {2} for Item {3}").format(d.ordered_qty, d.parent, d.qty, d.item_code)) + frappe.throw( + _( + "The total Issue / Transfer quantity {0} in Material Request {1} \ + cannot be greater than requested quantity {2} for Item {3}" + ).format(d.ordered_qty, d.parent, d.qty, d.item_code) + ) elif self.material_request_type == "Manufacture": - d.ordered_qty = flt(frappe.db.sql("""select sum(qty) + d.ordered_qty = flt( + frappe.db.sql( + """select sum(qty) from `tabWork Order` where material_request = %s and material_request_item = %s and docstatus = 1""", - (self.name, d.name))[0][0]) + (self.name, d.name), + )[0][0] + ) frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) - self._update_percent_field({ - "target_dt": "Material Request Item", - "target_parent_dt": self.doctype, - "target_parent_field": "per_ordered", - "target_ref_field": "stock_qty", - "target_field": "ordered_qty", - "name": self.name, - }, update_modified) + self._update_percent_field( + { + "target_dt": "Material Request Item", + "target_parent_dt": self.doctype, + "target_parent_field": "per_ordered", + "target_ref_field": "stock_qty", + "target_field": "ordered_qty", + "name": self.name, + }, + update_modified, + ) def update_requested_qty(self, mr_item_rows=None): """update requested qty (before ordered_qty is updated)""" item_wh_list = [] for d in self.get("items"): - if (not mr_item_rows or d.name in mr_item_rows) and [d.item_code, d.warehouse] not in item_wh_list \ - and d.warehouse and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 : + if ( + (not mr_item_rows or d.name in mr_item_rows) + and [d.item_code, d.warehouse] not in item_wh_list + and d.warehouse + and frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1 + ): item_wh_list.append([d.item_code, d.warehouse]) for item_code, warehouse in item_wh_list: - update_bin_qty(item_code, warehouse, { - "indented_qty": get_indented_qty(item_code, warehouse) - }) + update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)}) def update_requested_qty_in_production_plan(self): production_plans = [] - for d in self.get('items'): + for d in self.get("items"): if d.production_plan and d.material_request_plan_item: qty = d.qty if self.docstatus == 1 else 0 - frappe.db.set_value('Material Request Plan Item', - d.material_request_plan_item, 'requested_qty', qty) + frappe.db.set_value( + "Material Request Plan Item", d.material_request_plan_item, "requested_qty", qty + ) if d.production_plan not in production_plans: production_plans.append(d.production_plan) for production_plan in production_plans: - doc = frappe.get_doc('Production Plan', production_plan) + doc = frappe.get_doc("Production Plan", production_plan) doc.set_status() - doc.db_set('status', doc.status) + doc.db_set("status", doc.status) + def update_completed_and_requested_qty(stock_entry, method): if stock_entry.doctype == "Stock Entry": @@ -244,43 +293,55 @@ def update_completed_and_requested_qty(stock_entry, method): mr_obj = frappe.get_doc("Material Request", mr) if mr_obj.status in ["Stopped", "Cancelled"]: - frappe.throw(_("{0} {1} is cancelled or stopped").format(_("Material Request"), mr), - frappe.InvalidStatusError) + frappe.throw( + _("{0} {1} is cancelled or stopped").format(_("Material Request"), mr), + frappe.InvalidStatusError, + ) mr_obj.update_completed_qty(mr_item_rows) mr_obj.update_requested_qty(mr_item_rows) + def set_missing_values(source, target_doc): - if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate(nowdate()): + if target_doc.doctype == "Purchase Order" and getdate(target_doc.schedule_date) < getdate( + nowdate() + ): target_doc.schedule_date = None target_doc.run_method("set_missing_values") target_doc.run_method("calculate_taxes_and_totals") + def update_item(obj, target, source_parent): target.conversion_factor = obj.conversion_factor - target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor - target.stock_qty = (target.qty * target.conversion_factor) + target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + target.stock_qty = target.qty * target.conversion_factor if getdate(target.schedule_date) < getdate(nowdate()): target.schedule_date = None + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Material Request'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Material Request"), + } + ) return list_context + @frappe.whitelist() def update_status(name, status): - material_request = frappe.get_doc('Material Request', name) - material_request.check_permission('write') + material_request = frappe.get_doc("Material Request", name) + material_request.check_permission("write") material_request.update_status(status) + @frappe.whitelist() def make_purchase_order(source_name, target_doc=None, args=None): if args is None: @@ -293,7 +354,7 @@ def make_purchase_order(source_name, target_doc=None, args=None): # items only for given default supplier supplier_items = [] for d in target_doc.items: - default_supplier = get_item_defaults(d.item_code, target_doc.company).get('default_supplier') + default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier") if frappe.flags.args.default_supplier == default_supplier: supplier_items.append(d) target_doc.items = supplier_items @@ -301,58 +362,65 @@ def make_purchase_order(source_name, target_doc=None, args=None): set_missing_values(source, target_doc) def select_item(d): - filtered_items = args.get('filtered_children', []) + filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True return d.ordered_qty < d.stock_qty and child_filter - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Purchase Order", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Purchase Order", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "stock_uom"], + ["uom", "uom"], + ["sales_order", "sales_order"], + ["sales_order_item", "sales_order_item"], + ], + "postprocess": update_item, + "condition": select_item, + }, }, - "Material Request Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"], - ["sales_order", "sales_order"], - ["sales_order_item", "sales_order_item"] - ], - "postprocess": update_item, - "condition": select_item - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Request for Quotation", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Request for Quotation", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Request for Quotation Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "uom"], + ], + }, }, - "Material Request Item": { - "doctype": "Request for Quotation Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "uom"] - ] - } - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=None): mr = source_name @@ -363,43 +431,59 @@ def make_purchase_order_based_on_supplier(source_name, target_doc=None, args=Non target_doc.supplier = args.get("supplier") if getdate(target_doc.schedule_date) < getdate(nowdate()): target_doc.schedule_date = None - target_doc.set("items", [d for d in target_doc.get("items") - if d.get("item_code") in supplier_items and d.get("qty") > 0]) + target_doc.set( + "items", + [ + d for d in target_doc.get("items") if d.get("item_code") in supplier_items and d.get("qty") > 0 + ], + ) set_missing_values(source, target_doc) - target_doc = get_mapped_doc("Material Request", mr, { - "Material Request": { - "doctype": "Purchase Order", + target_doc = get_mapped_doc( + "Material Request", + mr, + { + "Material Request": { + "doctype": "Purchase Order", + }, + "Material Request Item": { + "doctype": "Purchase Order Item", + "field_map": [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["uom", "stock_uom"], + ["uom", "uom"], + ], + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.qty, + }, }, - "Material Request Item": { - "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"] - ], - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.qty - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return target_doc + @frappe.whitelist() def get_items_based_on_default_supplier(supplier): - supplier_items = [d.parent for d in frappe.db.get_all("Item Default", - {"default_supplier": supplier, "parenttype": "Item"}, 'parent')] + supplier_items = [ + d.parent + for d in frappe.db.get_all( + "Item Default", {"default_supplier": supplier, "parenttype": "Item"}, "parent" + ) + ] return supplier_items + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters): conditions = "" if txt: - conditions += "and mr.name like '%%"+txt+"%%' " + conditions += "and mr.name like '%%" + txt + "%%' " if filters.get("transaction_date"): date = filters.get("transaction_date")[1] @@ -411,7 +495,8 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa if not supplier_items: frappe.throw(_("{0} is not the default supplier for any items.").format(supplier)) - material_requests = frappe.db.sql("""select distinct mr.name, transaction_date,company + material_requests = frappe.db.sql( + """select distinct mr.name, transaction_date,company from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr.name = mr_item.parent and mr_item.item_code in ({0}) @@ -422,12 +507,16 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa and mr.company = '{1}' {2} order by mr_item.item_code ASC - limit {3} offset {4} """ \ - .format(', '.join(['%s']*len(supplier_items)), filters.get("company"), conditions, page_len, start), - tuple(supplier_items), as_dict=1) + limit {3} offset {4} """.format( + ", ".join(["%s"] * len(supplier_items)), filters.get("company"), conditions, page_len, start + ), + tuple(supplier_items), + as_dict=1, + ) return material_requests + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters): @@ -436,47 +525,63 @@ def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filte for d in doc.items: item_list.append(d.item_code) - return frappe.db.sql("""select default_supplier + return frappe.db.sql( + """select default_supplier from `tabItem Default` where parent in ({0}) and default_supplier IS NOT NULL - """.format(', '.join(['%s']*len(item_list))),tuple(item_list)) + """.format( + ", ".join(["%s"] * len(item_list)) + ), + tuple(item_list), + ) + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): def postprocess(source, target_doc): set_missing_values(source, target_doc) - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Supplier Quotation", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["=", "Purchase"] - } + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Supplier Quotation", + "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + }, + "Material Request Item": { + "doctype": "Supplier Quotation Item", + "field_map": { + "name": "material_request_item", + "parent": "material_request", + "sales_order": "sales_order", + }, + }, }, - "Material Request Item": { - "doctype": "Supplier Quotation Item", - "field_map": { - "name": "material_request_item", - "parent": "material_request", - "sales_order": "sales_order" - } - } - }, target_doc, postprocess) + target_doc, + postprocess, + ) return doclist + @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): def update_item(obj, target, source_parent): - qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor \ - if flt(obj.stock_qty) > flt(obj.ordered_qty) else 0 + qty = ( + flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + if flt(obj.stock_qty) > flt(obj.ordered_qty) + else 0 + ) target.qty = qty target.transfer_qty = qty * obj.conversion_factor target.conversion_factor = obj.conversion_factor - if source_parent.material_request_type == "Material Transfer" or source_parent.material_request_type == "Customer Provided": + if ( + source_parent.material_request_type == "Material Transfer" + or source_parent.material_request_type == "Customer Provided" + ): target.t_warehouse = obj.warehouse else: target.s_warehouse = obj.warehouse @@ -490,7 +595,7 @@ def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.purpose = source.material_request_type if source.job_card: - target.purpose = 'Material Transfer for Manufacture' + target.purpose = "Material Transfer for Manufacture" if source.material_request_type == "Customer Provided": target.purpose = "Material Receipt" @@ -499,101 +604,119 @@ def make_stock_entry(source_name, target_doc=None): target.set_stock_entry_type() target.set_job_card_data() - doclist = get_mapped_doc("Material Request", source_name, { - "Material Request": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1], - "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]] - } - }, - "Material Request Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "name": "material_request_item", - "parent": "material_request", - "uom": "stock_uom", - "job_card_item": "job_card_item" + doclist = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Stock Entry", + "validation": { + "docstatus": ["=", 1], + "material_request_type": ["in", ["Material Transfer", "Material Issue", "Customer Provided"]], + }, }, - "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty - } - }, target_doc, set_missing_values) + "Material Request Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "name": "material_request_item", + "parent": "material_request", + "uom": "stock_uom", + "job_card_item": "job_card_item", + }, + "postprocess": update_item, + "condition": lambda doc: doc.ordered_qty < doc.stock_qty, + }, + }, + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def raise_work_orders(material_request): - mr= frappe.get_doc("Material Request", material_request) - errors =[] + mr = frappe.get_doc("Material Request", material_request) + errors = [] work_orders = [] - default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") + default_wip_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_wip_warehouse" + ) for d in mr.items: if (d.stock_qty - d.ordered_qty) > 0: if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}): wo_order = frappe.new_doc("Work Order") - wo_order.update({ - "production_item": d.item_code, - "qty": d.stock_qty - d.ordered_qty, - "fg_warehouse": d.warehouse, - "wip_warehouse": default_wip_warehouse, - "description": d.description, - "stock_uom": d.stock_uom, - "expected_delivery_date": d.schedule_date, - "sales_order": d.sales_order, - "sales_order_item": d.get("sales_order_item"), - "bom_no": get_item_details(d.item_code).bom_no, - "material_request": mr.name, - "material_request_item": d.name, - "planned_start_date": mr.transaction_date, - "company": mr.company - }) + wo_order.update( + { + "production_item": d.item_code, + "qty": d.stock_qty - d.ordered_qty, + "fg_warehouse": d.warehouse, + "wip_warehouse": default_wip_warehouse, + "description": d.description, + "stock_uom": d.stock_uom, + "expected_delivery_date": d.schedule_date, + "sales_order": d.sales_order, + "sales_order_item": d.get("sales_order_item"), + "bom_no": get_item_details(d.item_code).bom_no, + "material_request": mr.name, + "material_request_item": d.name, + "planned_start_date": mr.transaction_date, + "company": mr.company, + } + ) wo_order.set_work_order_operations() wo_order.save() work_orders.append(wo_order.name) else: - errors.append(_("Row {0}: Bill of Materials not found for the Item {1}") - .format(d.idx, get_link_to_form("Item", d.item_code))) + errors.append( + _("Row {0}: Bill of Materials not found for the Item {1}").format( + d.idx, get_link_to_form("Item", d.item_code) + ) + ) if work_orders: work_orders_list = [get_link_to_form("Work Order", d) for d in work_orders] if len(work_orders) > 1: - msgprint(_("The following {0} were created: {1}") - .format(frappe.bold(_("Work Orders")), '
    ' + ', '.join(work_orders_list))) + msgprint( + _("The following {0} were created: {1}").format( + frappe.bold(_("Work Orders")), "
    " + ", ".join(work_orders_list) + ) + ) else: - msgprint(_("The {0} {1} created sucessfully") - .format(frappe.bold(_("Work Order")), work_orders_list[0])) + msgprint( + _("The {0} {1} created sucessfully").format(frappe.bold(_("Work Order")), work_orders_list[0]) + ) if errors: - frappe.throw(_("Work Order cannot be created for following reason:
    {0}") - .format(new_line_sep(errors))) + frappe.throw( + _("Work Order cannot be created for following reason:
    {0}").format(new_line_sep(errors)) + ) return work_orders + @frappe.whitelist() def create_pick_list(source_name, target_doc=None): - doc = get_mapped_doc('Material Request', source_name, { - 'Material Request': { - 'doctype': 'Pick List', - 'field_map': { - 'material_request_type': 'purpose' + doc = get_mapped_doc( + "Material Request", + source_name, + { + "Material Request": { + "doctype": "Pick List", + "field_map": {"material_request_type": "purpose"}, + "validation": {"docstatus": ["=", 1]}, }, - 'validation': { - 'docstatus': ['=', 1] - } - }, - 'Material Request Item': { - 'doctype': 'Pick List Item', - 'field_map': { - 'name': 'material_request_item', - 'qty': 'stock_qty' + "Material Request Item": { + "doctype": "Pick List Item", + "field_map": {"name": "material_request_item", "qty": "stock_qty"}, }, }, - }, target_doc) + target_doc, + ) doc.set_item_locations() diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index 9133859b24f..b073e6a22ee 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -1,23 +1,15 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'material_request', - 'transactions': [ + "fieldname": "material_request", + "transactions": [ { - 'label': _('Reference'), - 'items': ['Request for Quotation', 'Supplier Quotation', 'Purchase Order'] + "label": _("Reference"), + "items": ["Request for Quotation", "Supplier Quotation", "Purchase Order"], }, - { - 'label': _('Stock'), - 'items': ['Stock Entry', 'Purchase Receipt', 'Pick List'] - - }, - { - 'label': _('Manufacturing'), - 'items': ['Work Order'] - } - ] + {"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]}, + {"label": _("Manufacturing"), "items": ["Work Order"]}, + ], } diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 866f3ab2d57..78af1532ea8 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -22,8 +22,7 @@ class TestMaterialRequest(FrappeTestCase): def test_make_purchase_order(self): mr = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_purchase_order, - mr.name) + self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.submit() @@ -44,7 +43,6 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(sq.doctype, "Supplier Quotation") self.assertEqual(len(sq.get("items")), len(mr.get("items"))) - def test_make_stock_entry(self): mr = frappe.copy_doc(test_records[0]).insert() @@ -58,42 +56,44 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) - def _insert_stock_entry(self, qty1, qty2, warehouse = None ): - se = frappe.get_doc({ - "company": "_Test Company", - "doctype": "Stock Entry", - "posting_date": "2013-03-01", - "posting_time": "00:00:00", - "purpose": "Material Receipt", - "items": [ - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 100", - "parentfield": "items", - "basic_rate": 100, - "qty": qty1, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty1, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - }, - { - "conversion_factor": 1.0, - "doctype": "Stock Entry Detail", - "item_code": "_Test Item Home Desktop 200", - "parentfield": "items", - "basic_rate": 100, - "qty": qty2, - "stock_uom": "_Test UOM 1", - "transfer_qty": qty2, - "uom": "_Test UOM 1", - "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", - "cost_center": "_Test Cost Center - _TC" - } - ] - }) + def _insert_stock_entry(self, qty1, qty2, warehouse=None): + se = frappe.get_doc( + { + "company": "_Test Company", + "doctype": "Stock Entry", + "posting_date": "2013-03-01", + "posting_time": "00:00:00", + "purpose": "Material Receipt", + "items": [ + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 100", + "parentfield": "items", + "basic_rate": 100, + "qty": qty1, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty1, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "conversion_factor": 1.0, + "doctype": "Stock Entry Detail", + "item_code": "_Test Item Home Desktop 200", + "parentfield": "items", + "basic_rate": 100, + "qty": qty2, + "stock_uom": "_Test UOM 1", + "transfer_qty": qty2, + "uom": "_Test UOM 1", + "t_warehouse": warehouse or "_Test Warehouse 1 - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ], + } + ) se.set_stock_entry_type() se.insert() @@ -106,19 +106,19 @@ class TestMaterialRequest(FrappeTestCase): mr.load_from_db() mr.cancel() - self.assertRaises(frappe.ValidationError, mr.update_status, 'Stopped') + self.assertRaises(frappe.ValidationError, mr.update_status, "Stopped") def test_mr_changes_from_stopped_to_pending_after_reopen(self): mr = frappe.copy_doc(test_records[0]) mr.insert() mr.submit() - self.assertEqual('Pending', mr.status) + self.assertEqual("Pending", mr.status) - mr.update_status('Stopped') - self.assertEqual('Stopped', mr.status) + mr.update_status("Stopped") + self.assertEqual("Stopped", mr.status) - mr.update_status('Submitted') - self.assertEqual('Pending', mr.status) + mr.update_status("Submitted") + self.assertEqual("Pending", mr.status) def test_cannot_submit_cancelled_mr(self): mr = frappe.copy_doc(test_records[0]) @@ -133,7 +133,7 @@ class TestMaterialRequest(FrappeTestCase): mr.insert() mr.submit() mr.cancel() - self.assertEqual('Cancelled', mr.status) + self.assertEqual("Cancelled", mr.status) def test_cannot_change_cancelled_mr(self): mr = frappe.copy_doc(test_records[0]) @@ -142,12 +142,12 @@ class TestMaterialRequest(FrappeTestCase): mr.load_from_db() mr.cancel() - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Draft') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Ordered') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Issued') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Transferred') - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Pending') + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Draft") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Ordered") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Issued") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Transferred") + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Pending") def test_cannot_submit_deleted_material_request(self): mr = frappe.copy_doc(test_records[0]) @@ -169,9 +169,9 @@ class TestMaterialRequest(FrappeTestCase): mr.submit() mr.load_from_db() - mr.update_status('Stopped') - mr.update_status('Submitted') - self.assertEqual(mr.status, 'Pending') + mr.update_status("Stopped") + mr.update_status("Submitted") + self.assertEqual(mr.status, "Pending") def test_pending_mr_changes_to_stopped_after_stop(self): mr = frappe.copy_doc(test_records[0]) @@ -179,17 +179,21 @@ class TestMaterialRequest(FrappeTestCase): mr.submit() mr.load_from_db() - mr.update_status('Stopped') - self.assertEqual(mr.status, 'Stopped') + mr.update_status("Stopped") + self.assertEqual(mr.status, "Stopped") def test_cannot_stop_unsubmitted_mr(self): mr = frappe.copy_doc(test_records[0]) mr.insert() - self.assertRaises(frappe.InvalidStatusError, mr.update_status, 'Stopped') + self.assertRaises(frappe.InvalidStatusError, mr.update_status, "Stopped") def test_completed_qty_for_purchase(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -206,19 +210,18 @@ class TestMaterialRequest(FrappeTestCase): po_doc.get("items")[0].schedule_date = "2013-07-09" po_doc.get("items")[1].schedule_date = "2013-07-09" - # check for stopped status of Material Request po = frappe.copy_doc(po_doc) po.insert() po.load_from_db() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, po.submit) frappe.db.set(po, "docstatus", 1) self.assertRaises(frappe.InvalidStatusError, po.cancel) # resubmit and check for per complete mr.load_from_db() - mr.update_status('Submitted') + mr.update_status("Submitted") po = frappe.copy_doc(po_doc) po.insert() po.submit() @@ -229,8 +232,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 27.0) self.assertEqual(mr.get("items")[1].ordered_qty, 1.5) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5) @@ -242,15 +249,23 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_completed_qty_for_transfer(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -264,31 +279,31 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "01:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 27.0, - "transfer_qty": 27.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "qty": 1.5, - "transfer_qty": 1.5, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "01:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + {"qty": 27.0, "transfer_qty": 27.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) + se_doc.get("items")[1].update( + {"qty": 1.5, "transfer_qty": 1.5, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) # make available the qty in _Test Warehouse 1 before transfer self._insert_stock_entry(27.0, 1.5) @@ -296,17 +311,17 @@ class TestMaterialRequest(FrappeTestCase): # check for stopped status of Material Request se = frappe.copy_doc(se_doc) se.insert() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.submit) - mr.update_status('Submitted') + mr.update_status("Submitted") se.flags.ignore_validate_update_after_submit = True se.submit() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.cancel) - mr.update_status('Submitted') + mr.update_status("Submitted") se = frappe.copy_doc(se_doc) se.insert() se.submit() @@ -317,8 +332,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 27.0) self.assertEqual(mr.get("items")[1].ordered_qty, 1.5) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5) @@ -330,56 +349,70 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) def test_over_transfer_qty_allowance(self): - mr = frappe.new_doc('Material Request') + mr = frappe.new_doc("Material Request") mr.company = "_Test Company" mr.scheduled_date = today() - mr.append('items',{ - "item_code": "_Test FG Item", - "item_name": "_Test FG Item", - "qty": 10, - "schedule_date": today(), - "uom": "_Test UOM 1", - "warehouse": "_Test Warehouse - _TC" - }) + mr.append( + "items", + { + "item_code": "_Test FG Item", + "item_name": "_Test FG Item", + "qty": 10, + "schedule_date": today(), + "uom": "_Test UOM 1", + "warehouse": "_Test Warehouse - _TC", + }, + ) mr.material_request_type = "Material Transfer" mr.insert() mr.submit() - frappe.db.set_value('Stock Settings', None, 'mr_qty_allowance', 20) + frappe.db.set_value("Stock Settings", None, "mr_qty_allowance", 20) # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": today(), - "posting_time": "00:00", - }) - se_doc.get("items")[0].update({ - "qty": 13, - "transfer_qty": 12.0, - "s_warehouse": "_Test Warehouse - _TC", - "t_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": today(), + "posting_time": "00:00", + } + ) + se_doc.get("items")[0].update( + { + "qty": 13, + "transfer_qty": 12.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0, + } + ) # make available the qty in _Test Warehouse 1 before transfer sr = frappe.new_doc("Stock Reconciliation") sr.company = "_Test Company" sr.purpose = "Opening Stock" - sr.append('items', { - "item_code": "_Test FG Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 20, - "valuation_rate": 0.01 - }) + sr.append( + "items", + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 20, + "valuation_rate": 0.01, + }, + ) sr.insert() sr.submit() se = frappe.copy_doc(se_doc) @@ -389,8 +422,12 @@ class TestMaterialRequest(FrappeTestCase): se.submit() def test_completed_qty_for_over_transfer(self): - existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - existing_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + existing_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + existing_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) # submit material request of type Purchase mr = frappe.copy_doc(test_records[0]) @@ -401,23 +438,19 @@ class TestMaterialRequest(FrappeTestCase): # map a stock entry se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "00:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 54.0, - "transfer_qty": 54.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "qty": 3.0, - "transfer_qty": 3.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "00:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + {"qty": 54.0, "transfer_qty": 54.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) + se_doc.get("items")[1].update( + {"qty": 3.0, "transfer_qty": 3.0, "s_warehouse": "_Test Warehouse 1 - _TC", "basic_rate": 1.0} + ) # make available the qty in _Test Warehouse 1 before transfer self._insert_stock_entry(60.0, 3.0) @@ -426,11 +459,11 @@ class TestMaterialRequest(FrappeTestCase): se = frappe.copy_doc(se_doc) se.set_stock_entry_type() se.insert() - mr.update_status('Stopped') + mr.update_status("Stopped") self.assertRaises(frappe.InvalidStatusError, se.submit) self.assertRaises(frappe.InvalidStatusError, se.cancel) - mr.update_status('Submitted') + mr.update_status("Submitted") se = frappe.copy_doc(se_doc) se.set_stock_entry_type() se.insert() @@ -443,8 +476,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 54.0) self.assertEqual(mr.get("items")[1].ordered_qty, 3.0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2) @@ -456,8 +493,12 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 0) self.assertEqual(mr.get("items")[1].ordered_qty, 0) - current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") - current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") + current_requested_qty_item1 = self._get_requested_qty( + "_Test Item Home Desktop 100", "_Test Warehouse - _TC" + ) + current_requested_qty_item2 = self._get_requested_qty( + "_Test Item Home Desktop 200", "_Test Warehouse - _TC" + ) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0) @@ -470,25 +511,31 @@ class TestMaterialRequest(FrappeTestCase): mr.submit() se_doc = make_stock_entry(mr.name) - se_doc.update({ - "posting_date": "2013-03-01", - "posting_time": "00:00", - "fiscal_year": "_Test Fiscal Year 2013", - }) - se_doc.get("items")[0].update({ - "qty": 60.0, - "transfer_qty": 60.0, - "s_warehouse": "_Test Warehouse - _TC", - "t_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) - se_doc.get("items")[1].update({ - "item_code": "_Test Item Home Desktop 100", - "qty": 3.0, - "transfer_qty": 3.0, - "s_warehouse": "_Test Warehouse 1 - _TC", - "basic_rate": 1.0 - }) + se_doc.update( + { + "posting_date": "2013-03-01", + "posting_time": "00:00", + "fiscal_year": "_Test Fiscal Year 2013", + } + ) + se_doc.get("items")[0].update( + { + "qty": 60.0, + "transfer_qty": 60.0, + "s_warehouse": "_Test Warehouse - _TC", + "t_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0, + } + ) + se_doc.get("items")[1].update( + { + "item_code": "_Test Item Home Desktop 100", + "qty": 3.0, + "transfer_qty": 3.0, + "s_warehouse": "_Test Warehouse 1 - _TC", + "basic_rate": 1.0, + } + ) # check for stopped status of Material Request se = frappe.copy_doc(se_doc) @@ -505,18 +552,20 @@ class TestMaterialRequest(FrappeTestCase): def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany + mr = frappe.copy_doc(test_records[0]) mr.company = "_Test Company 1" self.assertRaises(InvalidWarehouseCompany, mr.insert) def _get_requested_qty(self, item_code, warehouse): - return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")) + return flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty") + ) def test_make_stock_entry_for_material_issue(self): mr = frappe.copy_doc(test_records[0]).insert() - self.assertRaises(frappe.ValidationError, make_stock_entry, - mr.name) + self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.material_request_type = "Material Issue" @@ -528,8 +577,13 @@ class TestMaterialRequest(FrappeTestCase): def test_completed_qty_for_issue(self): def _get_requested_qty(): - return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100", - "warehouse": "_Test Warehouse - _TC"}, "indented_qty")) + return flt( + frappe.db.get_value( + "Bin", + {"item_code": "_Test Item Home Desktop 100", "warehouse": "_Test Warehouse - _TC"}, + "indented_qty", + ) + ) existing_requested_qty = _get_requested_qty() @@ -537,7 +591,7 @@ class TestMaterialRequest(FrappeTestCase): mr.material_request_type = "Material Issue" mr.submit() - #testing bin value after material request is submitted + # testing bin value after material request is submitted self.assertEqual(_get_requested_qty(), existing_requested_qty - 54.0) # receive items to allow issue @@ -556,7 +610,7 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.get("items")[0].ordered_qty, 54.0) self.assertEqual(mr.get("items")[1].ordered_qty, 3.0) - #testing bin requested qty after issuing stock against material request + # testing bin requested qty after issuing stock against material request self.assertEqual(_get_requested_qty(), existing_requested_qty) def test_material_request_type_manufacture(self): @@ -564,8 +618,11 @@ class TestMaterialRequest(FrappeTestCase): mr = frappe.get_doc("Material Request", mr.name) mr.submit() completed_qty = mr.items[0].ordered_qty - requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] prod_order = raise_work_orders(mr.name) po = frappe.get_doc("Work Order", prod_order[0]) @@ -575,8 +632,11 @@ class TestMaterialRequest(FrappeTestCase): mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(completed_qty + po.qty, mr.items[0].ordered_qty) - new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + new_requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] self.assertEqual(requested_qty - po.qty, new_requested_qty) @@ -585,17 +645,24 @@ class TestMaterialRequest(FrappeTestCase): mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(completed_qty, mr.items[0].ordered_qty) - new_requested_qty = frappe.db.sql("""select indented_qty from `tabBin` where \ - item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] + new_requested_qty = frappe.db.sql( + """select indented_qty from `tabBin` where \ + item_code= %s and warehouse= %s """, + (mr.items[0].item_code, mr.items[0].warehouse), + )[0][0] self.assertEqual(requested_qty, new_requested_qty) def test_requested_qty_multi_uom(self): - existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + existing_requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") - mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture', - uom="_Test UOM 1", conversion_factor=12) + mr = make_material_request( + item_code="_Test FG Item", + material_request_type="Manufacture", + uom="_Test UOM 1", + conversion_factor=12, + ) - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 120) @@ -605,42 +672,36 @@ class TestMaterialRequest(FrappeTestCase): wo.wip_warehouse = "_Test Warehouse 1 - _TC" wo.submit() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 70) wo.cancel() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty + 120) mr.reload() mr.cancel() - requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC') + requested_qty = self._get_requested_qty("_Test FG Item", "_Test Warehouse - _TC") self.assertEqual(requested_qty, existing_requested_qty) - def test_multi_uom_for_purchase(self): mr = frappe.copy_doc(test_records[0]) - mr.material_request_type = 'Purchase' + mr.material_request_type = "Purchase" item = mr.items[0] mr.schedule_date = today() - if not frappe.db.get_value('UOM Conversion Detail', - {'parent': item.item_code, 'uom': 'Kg'}): - item_doc = frappe.get_doc('Item', item.item_code) - item_doc.append('uoms', { - 'uom': 'Kg', - 'conversion_factor': 5 - }) + if not frappe.db.get_value("UOM Conversion Detail", {"parent": item.item_code, "uom": "Kg"}): + item_doc = frappe.get_doc("Item", item.item_code) + item_doc.append("uoms", {"uom": "Kg", "conversion_factor": 5}) item_doc.save(ignore_permissions=True) - item.uom = 'Kg' + item.uom = "Kg" for item in mr.items: item.schedule_date = mr.schedule_date mr.insert() - self.assertRaises(frappe.ValidationError, make_purchase_order, - mr.name) + self.assertRaises(frappe.ValidationError, make_purchase_order, mr.name) mr = frappe.get_doc("Material Request", mr.name) mr.submit() @@ -654,17 +715,19 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(po.doctype, "Purchase Order") self.assertEqual(len(po.get("items")), len(mr.get("items"))) - po.supplier = '_Test Supplier' + po.supplier = "_Test Supplier" po.insert() po.submit() mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(mr.per_ordered, 100) def test_customer_provided_parts_mr(self): - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) + create_item( + "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC") - mr = make_material_request(item_code='CUST-0987', material_request_type='Customer Provided') + mr = make_material_request(item_code="CUST-0987", material_request_type="Customer Provided") se = make_stock_entry(mr.name) se.insert() se.submit() @@ -677,25 +740,30 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.per_ordered, 100) self.assertEqual(existing_requested_qty, current_requested_qty) + def make_material_request(**args): args = frappe._dict(args) mr = frappe.new_doc("Material Request") mr.material_request_type = args.material_request_type or "Purchase" mr.company = args.company or "_Test Company" - mr.customer = args.customer or '_Test Customer' - mr.append("items", { - "item_code": args.item_code or "_Test Item", - "qty": args.qty or 10, - "uom": args.uom or "_Test UOM", - "conversion_factor": args.conversion_factor or 1, - "schedule_date": args.schedule_date or today(), - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC" - }) + mr.customer = args.customer or "_Test Customer" + mr.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "qty": args.qty or 10, + "uom": args.uom or "_Test UOM", + "conversion_factor": args.conversion_factor or 1, + "schedule_date": args.schedule_date or today(), + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + }, + ) mr.insert() if not args.do_not_submit: mr.submit() return mr + test_dependencies = ["Currency Exchange", "BOM"] -test_records = frappe.get_test_records('Material Request') +test_records = frappe.get_test_records("Material Request") diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py index 32407d0fb09..08c9ed27427 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.py +++ b/erpnext/stock/doctype/material_request_item/material_request_item.py @@ -11,5 +11,6 @@ from frappe.model.document import Document class MaterialRequestItem(Document): pass + def on_doctype_update(): frappe.db.add_index("Material Request Item", ["item_code", "warehouse"]) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index f9c00c59bac..026dd4e122a 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -23,7 +23,9 @@ def make_packing_list(doc): return parent_items_price, reset = {}, False - set_price_from_children = frappe.db.get_single_value("Selling Settings", "editable_bundle_item_rates") + set_price_from_children = frappe.db.get_single_value( + "Selling Settings", "editable_bundle_item_rates" + ) stale_packed_items_table = get_indexed_packed_items_table(doc) @@ -33,9 +35,11 @@ def make_packing_list(doc): if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): for bundle_item in get_product_bundle_items(item_row.item_code): pi_row = add_packed_item_row( - doc=doc, packing_item=bundle_item, - main_item_row=item_row, packed_items_table=stale_packed_items_table, - reset=reset + doc=doc, + packing_item=bundle_item, + main_item_row=item_row, + packed_items_table=stale_packed_items_table, + reset=reset, ) item_data = get_packed_item_details(bundle_item.item_code, doc.company) update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data) @@ -43,18 +47,19 @@ def make_packing_list(doc): update_packed_item_price_data(pi_row, item_data, doc) update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) - if set_price_from_children: # create/update bundle item wise price dict + if set_price_from_children: # create/update bundle item wise price dict update_product_bundle_rate(parent_items_price, pi_row) if parent_items_price: - set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item + set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item + def get_indexed_packed_items_table(doc): """ - Create dict from stale packed items table like: - {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} + Create dict from stale packed items table like: + {(Parent Item 1, Bundle Item 1, ae4b5678): {...}, (key): {value}} - Use: to quickly retrieve/check if row existed in table instead of looping n times + Use: to quickly retrieve/check if row existed in table instead of looping n times """ indexed_table = {} for packed_item in doc.get("packed_items"): @@ -63,6 +68,7 @@ def get_indexed_packed_items_table(doc): return indexed_table + def reset_packing_list(doc): "Conditionally reset the table and return if it was reset or not." reset_table = False @@ -86,33 +92,34 @@ def reset_packing_list(doc): doc.set("packed_items", []) return reset_table + def get_product_bundle_items(item_code): product_bundle = frappe.qb.DocType("Product Bundle") product_bundle_item = frappe.qb.DocType("Product Bundle Item") query = ( frappe.qb.from_(product_bundle_item) - .join(product_bundle).on(product_bundle_item.parent == product_bundle.name) + .join(product_bundle) + .on(product_bundle_item.parent == product_bundle.name) .select( product_bundle_item.item_code, product_bundle_item.qty, product_bundle_item.uom, - product_bundle_item.description - ).where( - product_bundle.new_item_code == item_code - ).orderby( - product_bundle_item.idx + product_bundle_item.description, ) + .where(product_bundle.new_item_code == item_code) + .orderby(product_bundle_item.idx) ) return query.run(as_dict=True) + def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset): """Add and return packed item row. - doc: Transaction document - packing_item (dict): Packed Item details - main_item_row (dict): Items table row corresponding to packed item - packed_items_table (dict): Packed Items table before save (indexed) - reset (bool): State if table is reset or preserved as is + doc: Transaction document + packing_item (dict): Packed Item details + main_item_row (dict): Items table row corresponding to packed item + packed_items_table (dict): Packed Items table before save (indexed) + reset (bool): State if table is reset or preserved as is """ exists, pi_row = False, {} @@ -122,33 +129,34 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re pi_row, exists = packed_items_table.get(key), True if not exists: - pi_row = doc.append('packed_items', {}) - elif reset: # add row if row exists but table is reset + pi_row = doc.append("packed_items", {}) + elif reset: # add row if row exists but table is reset pi_row.idx, pi_row.name = None, None - pi_row = doc.append('packed_items', pi_row) + pi_row = doc.append("packed_items", pi_row) return pi_row + def get_packed_item_details(item_code, company): item = frappe.qb.DocType("Item") item_default = frappe.qb.DocType("Item Default") query = ( frappe.qb.from_(item) .left_join(item_default) - .on( - (item_default.parent == item.name) - & (item_default.company == company) - ).select( - item.item_name, item.is_stock_item, - item.description, item.stock_uom, + .on((item_default.parent == item.name) & (item_default.company == company)) + .select( + item.item_name, + item.is_stock_item, + item.description, + item.stock_uom, item.valuation_rate, - item_default.default_warehouse - ).where( - item.name == item_code + item_default.default_warehouse, ) + .where(item.name == item_code) ) return query.run(as_dict=True)[0] + def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data): pi_row.parent_item = main_item_row.item_code pi_row.parent_detail_docname = main_item_row.name @@ -161,12 +169,16 @@ def update_packed_item_basic_data(main_item_row, pi_row, packing_item, item_data if not pi_row.description: pi_row.description = packing_item.get("description") + def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data, doc): # TODO batch_no, actual_batch_qty, incoming_rate if not pi_row.warehouse and not doc.amended_from: - fetch_warehouse = (doc.get('is_pos') or item_data.is_stock_item or not item_data.default_warehouse) - pi_row.warehouse = (main_item_row.warehouse if (fetch_warehouse and main_item_row.warehouse) - else item_data.default_warehouse) + fetch_warehouse = doc.get("is_pos") or item_data.is_stock_item or not item_data.default_warehouse + pi_row.warehouse = ( + main_item_row.warehouse + if (fetch_warehouse and main_item_row.warehouse) + else item_data.default_warehouse + ) if not pi_row.target_warehouse: pi_row.target_warehouse = main_item_row.get("target_warehouse") @@ -175,6 +187,7 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data pi_row.actual_qty = flt(bin.get("actual_qty")) pi_row.projected_qty = flt(bin.get("projected_qty")) + def update_packed_item_price_data(pi_row, item_data, doc): "Set price as per price list or from the Item master." if pi_row.rate: @@ -182,50 +195,60 @@ def update_packed_item_price_data(pi_row, item_data, doc): item_doc = frappe.get_cached_doc("Item", pi_row.item_code) row_data = pi_row.as_dict().copy() - row_data.update({ - "company": doc.get("company"), - "price_list": doc.get("selling_price_list"), - "currency": doc.get("currency"), - "conversion_rate": doc.get("conversion_rate"), - }) + row_data.update( + { + "company": doc.get("company"), + "price_list": doc.get("selling_price_list"), + "currency": doc.get("currency"), + "conversion_rate": doc.get("conversion_rate"), + } + ) rate = get_price_list_rate(row_data, item_doc).get("price_list_rate") pi_row.rate = rate or item_data.get("valuation_rate") or 0.0 + def update_packed_item_from_cancelled_doc(main_item_row, packing_item, pi_row, doc): "Update packed item row details from cancelled doc into amended doc." prev_doc_packed_items_map = None if doc.amended_from: prev_doc_packed_items_map = get_cancelled_doc_packed_item_details(doc.packed_items) - if prev_doc_packed_items_map and prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)): + if prev_doc_packed_items_map and prev_doc_packed_items_map.get( + (packing_item.item_code, main_item_row.item_code) + ): prev_doc_row = prev_doc_packed_items_map.get((packing_item.item_code, main_item_row.item_code)) pi_row.batch_no = prev_doc_row[0].batch_no pi_row.serial_no = prev_doc_row[0].serial_no pi_row.warehouse = prev_doc_row[0].warehouse + def get_packed_item_bin_qty(item, warehouse): bin_data = frappe.db.get_values( "Bin", fieldname=["actual_qty", "projected_qty"], filters={"item_code": item, "warehouse": warehouse}, - as_dict=True + as_dict=True, ) return bin_data[0] if bin_data else {} + def get_cancelled_doc_packed_item_details(old_packed_items): prev_doc_packed_items_map = {} for items in old_packed_items: - prev_doc_packed_items_map.setdefault((items.item_code ,items.parent_item), []).append(items.as_dict()) + prev_doc_packed_items_map.setdefault((items.item_code, items.parent_item), []).append( + items.as_dict() + ) return prev_doc_packed_items_map + def update_product_bundle_rate(parent_items_price, pi_row): """ - Update the price dict of Product Bundles based on the rates of the Items in the bundle. + Update the price dict of Product Bundles based on the rates of the Items in the bundle. - Stucture: - {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0} + Stucture: + {(Bundle Item 1, ae56fgji): 150.0, (Bundle Item 2, bc78fkjo): 200.0} """ key = (pi_row.parent_item, pi_row.parent_detail_docname) rate = parent_items_price.get(key) @@ -234,6 +257,7 @@ def update_product_bundle_rate(parent_items_price, pi_row): parent_items_price[key] += flt(pi_row.rate) + def set_product_bundle_rate_amount(doc, parent_items_price): "Set cumulative rate and amount in bundle item." for item in doc.get("items"): @@ -242,6 +266,7 @@ def set_product_bundle_rate_amount(doc, parent_items_price): item.rate = bundle_rate item.amount = flt(bundle_rate * item.qty) + def on_doctype_update(): frappe.db.add_index("Packed Item", ["item_code", "warehouse"]) @@ -252,10 +277,7 @@ def get_items_from_product_bundle(row): bundled_items = get_product_bundle_items(row["item_code"]) for item in bundled_items: - row.update({ - "item_code": item.item_code, - "qty": flt(row["quantity"]) * flt(item.qty) - }) + row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)}) items.append(get_item_details(row)) return items diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 5f1b9542d6a..fe1b0d9f792 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -14,6 +14,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPackedItem(FrappeTestCase): "Test impact on Packed Items table in various scenarios." + @classmethod def setUpClass(cls) -> None: super().setUpClass() @@ -39,8 +40,7 @@ class TestPackedItem(FrappeTestCase): def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." - so = make_sales_order(item_code = self.bundle, qty=1, - do_not_submit=True) + so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) self.assertEqual(so.items[0].qty, 1) self.assertEqual(len(so.packed_items), 2) @@ -51,7 +51,7 @@ class TestPackedItem(FrappeTestCase): "Test impact on packed items if bundle item row is updated." so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True) - so.items[0].qty = 2 # change qty + so.items[0].qty = 2 # change qty so.save() self.assertEqual(so.packed_items[0].qty, 4) @@ -67,12 +67,9 @@ class TestPackedItem(FrappeTestCase): "Test impact on packed items if same bundle item is added and removed." so_items = [] for qty in [2, 4, 6, 8]: - so_items.append({ - "item_code": self.bundle, - "qty": qty, - "rate": 400, - "warehouse": "_Test Warehouse - _TC" - }) + so_items.append( + {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"} + ) # create SO with recurring bundle item so = make_sales_order(item_list=so_items, do_not_submit=True) @@ -120,18 +117,15 @@ class TestPackedItem(FrappeTestCase): "Test impact on packed items in newly mapped DN from SO." so_items = [] for qty in [2, 4]: - so_items.append({ - "item_code": self.bundle, - "qty": qty, - "rate": 400, - "warehouse": "_Test Warehouse - _TC" - }) + so_items.append( + {"item_code": self.bundle, "qty": qty, "rate": 400, "warehouse": "_Test Warehouse - _TC"} + ) # create SO with recurring bundle item so = make_sales_order(item_list=so_items) dn = make_delivery_note(so.name) - dn.items[1].qty = 3 # change second row qty for inserting doc + dn.items[1].qty = 3 # change second row qty for inserting doc dn.save() self.assertEqual(len(dn.packed_items), 4) @@ -148,7 +142,7 @@ class TestPackedItem(FrappeTestCase): for item in self.bundle_items: make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today) - so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse) + so = make_sales_order(item_code=self.bundle, qty=1, company=company, warehouse=warehouse) dn = make_delivery_note(so.name) dn.save() @@ -159,7 +153,9 @@ class TestPackedItem(FrappeTestCase): # backdated stock entry for item in self.bundle_items: - make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday) + make_stock_entry( + item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday + ) # assert correct reposting gles = get_gl_entries(dn.doctype, dn.name) @@ -173,8 +169,7 @@ class TestPackedItem(FrappeTestCase): sort_function = lambda p: (p.parent_item, p.item_code, p.qty) for sent, returned in zip( - sorted(original, key=sort_function), - sorted(returned, key=sort_function) + sorted(original, key=sort_function), sorted(returned, key=sort_function) ): self.assertEqual(sent.item_code, returned.item_code) self.assertEqual(sent.parent_item, returned.parent_item) @@ -195,7 +190,7 @@ class TestPackedItem(FrappeTestCase): "warehouse": self.warehouse, "qty": 1, "rate": 100, - } + }, ] so = make_sales_order(item_list=item_list, warehouse=self.warehouse) @@ -224,7 +219,7 @@ class TestPackedItem(FrappeTestCase): "warehouse": self.warehouse, "qty": 1, "rate": 100, - } + }, ] so = make_sales_order(item_list=item_list, warehouse=self.warehouse) @@ -246,11 +241,10 @@ class TestPackedItem(FrappeTestCase): expected_returns = [d for d in dn.packed_items if d.parent_item == self.bundle] self.assertReturns(expected_returns, dn_ret.packed_items) - def test_returning_partial_bundle_qty(self): from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return - so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty = 2) + so = make_sales_order(item_code=self.bundle, warehouse=self.warehouse, qty=2) dn = make_delivery_note(so.name) dn.save() diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index b092862415a..e9ccf5fc779 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -10,14 +10,13 @@ from frappe.utils import cint, flt class PackingSlip(Document): - def validate(self): """ - * Validate existence of submitted Delivery Note - * Case nos do not overlap - * Check if packed qty doesn't exceed actual qty of delivery note + * Validate existence of submitted Delivery Note + * Case nos do not overlap + * Check if packed qty doesn't exceed actual qty of delivery note - It is necessary to validate case nos before checking quantity + It is necessary to validate case nos before checking quantity """ self.validate_delivery_note() self.validate_items_mandatory() @@ -25,12 +24,13 @@ class PackingSlip(Document): self.validate_qty() from erpnext.utilities.transaction_base import validate_uom_is_integer + validate_uom_is_integer(self, "stock_uom", "qty") validate_uom_is_integer(self, "weight_uom", "net_weight") def validate_delivery_note(self): """ - Validates if delivery note has status as draft + Validates if delivery note has status as draft """ if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0: frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note)) @@ -42,27 +42,33 @@ class PackingSlip(Document): def validate_case_nos(self): """ - Validate if case nos overlap. If they do, recommend next case no. + Validate if case nos overlap. If they do, recommend next case no. """ if not cint(self.from_case_no): frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1) elif not self.to_case_no: self.to_case_no = self.from_case_no elif cint(self.from_case_no) > cint(self.to_case_no): - frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), - raise_exception=1) + frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1) - res = frappe.db.sql("""SELECT name FROM `tabPacking Slip` + res = frappe.db.sql( + """SELECT name FROM `tabPacking Slip` WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND ((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s) OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no)) - """, {"delivery_note":self.delivery_note, - "from_case_no":self.from_case_no, - "to_case_no":self.to_case_no}) + """, + { + "delivery_note": self.delivery_note, + "from_case_no": self.from_case_no, + "to_case_no": self.to_case_no, + }, + ) if res: - frappe.throw(_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())) + frappe.throw( + _("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no()) + ) def validate_qty(self): """Check packed qty across packing slips and delivery note""" @@ -70,36 +76,37 @@ class PackingSlip(Document): dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing() for item in dn_details: - new_packed_qty = (flt(ps_item_qty[item['item_code']]) * no_of_cases) + \ - flt(item['packed_qty']) - if new_packed_qty > flt(item['qty']) and no_of_cases: + new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"]) + if new_packed_qty > flt(item["qty"]) and no_of_cases: self.recommend_new_qty(item, ps_item_qty, no_of_cases) - def get_details_for_packing(self): """ - Returns - * 'Delivery Note Items' query result as a list of dict - * Item Quantity dict of current packing slip doc - * No. of Cases of this packing slip + Returns + * 'Delivery Note Items' query result as a list of dict + * Item Quantity dict of current packing slip doc + * No. of Cases of this packing slip """ rows = [d.item_code for d in self.get("items")] # also pick custom fields from delivery note - custom_fields = ', '.join('dni.`{0}`'.format(d.fieldname) + custom_fields = ", ".join( + "dni.`{0}`".format(d.fieldname) for d in frappe.get_meta("Delivery Note Item").get_custom_fields() - if d.fieldtype not in no_value_fields) + if d.fieldtype not in no_value_fields + ) if custom_fields: - custom_fields = ', ' + custom_fields + custom_fields = ", " + custom_fields condition = "" if rows: - condition = " and item_code in (%s)" % (", ".join(["%s"]*len(rows))) + condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows))) # gets item code, qty per item code, latest packed qty per item code and stock uom - res = frappe.db.sql("""select item_code, sum(qty) as qty, + res = frappe.db.sql( + """select item_code, sum(qty) as qty, (select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1)) from `tabPacking Slip` ps, `tabPacking Slip Item` psi where ps.name = psi.parent and ps.docstatus = 1 @@ -107,47 +114,57 @@ class PackingSlip(Document): stock_uom, item_name, description, dni.batch_no {custom_fields} from `tabDelivery Note Item` dni where parent=%s {condition} - group by item_code""".format(condition=condition, custom_fields=custom_fields), - tuple([self.delivery_note] + rows), as_dict=1) + group by item_code""".format( + condition=condition, custom_fields=custom_fields + ), + tuple([self.delivery_note] + rows), + as_dict=1, + ) ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")]) no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1 return res, ps_item_qty, no_of_cases - def recommend_new_qty(self, item, ps_item_qty, no_of_cases): """ - Recommend a new quantity and raise a validation exception + Recommend a new quantity and raise a validation exception """ - item['recommended_qty'] = (flt(item['qty']) - flt(item['packed_qty'])) / no_of_cases - item['specified_qty'] = flt(ps_item_qty[item['item_code']]) - if not item['packed_qty']: item['packed_qty'] = 0 + item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases + item["specified_qty"] = flt(ps_item_qty[item["item_code"]]) + if not item["packed_qty"]: + item["packed_qty"] = 0 - frappe.throw(_("Quantity for Item {0} must be less than {1}").format(item.get("item_code"), item.get("recommended_qty"))) + frappe.throw( + _("Quantity for Item {0} must be less than {1}").format( + item.get("item_code"), item.get("recommended_qty") + ) + ) def update_item_details(self): """ - Fill empty columns in Packing Slip Item + Fill empty columns in Packing Slip Item """ if not self.from_case_no: self.from_case_no = self.get_recommended_case_no() for d in self.get("items"): - res = frappe.db.get_value("Item", d.item_code, - ["weight_per_unit", "weight_uom"], as_dict=True) + res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True) - if res and len(res)>0: + if res and len(res) > 0: d.net_weight = res["weight_per_unit"] d.weight_uom = res["weight_uom"] def get_recommended_case_no(self): """ - Returns the next case no. for a new packing slip for a delivery - note + Returns the next case no. for a new packing slip for a delivery + note """ - recommended_case_no = frappe.db.sql("""SELECT MAX(to_case_no) FROM `tabPacking Slip` - WHERE delivery_note = %s AND docstatus=1""", self.delivery_note) + recommended_case_no = frappe.db.sql( + """SELECT MAX(to_case_no) FROM `tabPacking Slip` + WHERE delivery_note = %s AND docstatus=1""", + self.delivery_note, + ) return cint(recommended_case_no[0][0]) + 1 @@ -160,7 +177,7 @@ class PackingSlip(Document): dn_details = self.get_details_for_packing()[0] for item in dn_details: if flt(item.qty) > flt(item.packed_qty): - ch = self.append('items', {}) + ch = self.append("items", {}) ch.item_code = item.item_code ch.item_name = item.item_name ch.stock_uom = item.stock_uom @@ -175,14 +192,18 @@ class PackingSlip(Document): self.update_item_details() + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_details(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond - return frappe.db.sql("""select name, item_name, description from `tabItem` + + return frappe.db.sql( + """select name, item_name, description from `tabItem` where name in ( select item_code FROM `tabDelivery Note Item` where parent= %s) and %s like "%s" %s - limit %s, %s """ % ("%s", searchfield, "%s", - get_match_cond(doctype), "%s", "%s"), - ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len)) + limit %s, %s """ + % ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"), + ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len), + ) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 35cbc2fd858..7061ee1eea4 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -19,6 +19,7 @@ from erpnext.stock.get_item_details import get_conversion_factor # TODO: Prioritize SO or WO group warehouse + class PickList(Document): def validate(self): self.validate_for_qty() @@ -27,8 +28,11 @@ class PickList(Document): self.set_item_locations() # set percentage picked in SO - for location in self.get('locations'): - if location.sales_order and frappe.db.get_value("Sales Order",location.sales_order,"per_picked") == 100: + for location in self.get("locations"): + if ( + location.sales_order + and frappe.db.get_value("Sales Order", location.sales_order, "per_picked") == 100 + ): frappe.throw("Row " + str(location.idx) + " has been picked already!") def before_submit(self): @@ -39,44 +43,62 @@ class PickList(Document): if item.sales_order_item: # update the picked_qty in SO Item - self.update_so(item.sales_order_item,item.picked_qty,item.item_code) + self.update_so(item.sales_order_item, item.picked_qty, item.item_code) - if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'): + if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue if not item.serial_no: - frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}").format( - frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)), - title=_("Serial Nos Required")) - if len(item.serial_no.split('\n')) == item.picked_qty: + frappe.throw( + _("Row #{0}: {1} does not have any available serial numbers in {2}").format( + frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse) + ), + title=_("Serial Nos Required"), + ) + if len(item.serial_no.split("\n")) == item.picked_qty: continue - frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity') - .format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch")) + frappe.throw( + _( + "For item {0} at row {1}, count of serial numbers does not match with the picked quantity" + ).format(frappe.bold(item.item_code), frappe.bold(item.idx)), + title=_("Quantity Mismatch"), + ) def before_cancel(self): - #update picked_qty in SO Item on cancel of PL - for item in self.get('locations'): + # update picked_qty in SO Item on cancel of PL + for item in self.get("locations"): if item.sales_order_item: self.update_so(item.sales_order_item, -1 * item.picked_qty, item.item_code) - def update_so(self,so_item,picked_qty,item_code): - so_doc = frappe.get_doc("Sales Order",frappe.db.get_value("Sales Order Item",so_item,"parent")) - already_picked,actual_qty = frappe.db.get_value("Sales Order Item",so_item,["picked_qty","qty"]) + def update_so(self, so_item, picked_qty, item_code): + so_doc = frappe.get_doc( + "Sales Order", frappe.db.get_value("Sales Order Item", so_item, "parent") + ) + already_picked, actual_qty = frappe.db.get_value( + "Sales Order Item", so_item, ["picked_qty", "qty"] + ) if self.docstatus == 1: - if (((already_picked + picked_qty)/ actual_qty)*100) > (100 + flt(frappe.db.get_single_value('Stock Settings', 'over_delivery_receipt_allowance'))): - frappe.throw('You are picking more than required quantity for ' + item_code + '. Check if there is any other pick list created for '+so_doc.name) + if (((already_picked + picked_qty) / actual_qty) * 100) > ( + 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")) + ): + frappe.throw( + "You are picking more than required quantity for " + + item_code + + ". Check if there is any other pick list created for " + + so_doc.name + ) - frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty) + frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty) total_picked_qty = 0 total_so_qty = 0 - for item in so_doc.get('items'): + for item in so_doc.get("items"): total_picked_qty += flt(item.picked_qty) total_so_qty += flt(item.stock_qty) - total_picked_qty=total_picked_qty + picked_qty - per_picked = total_picked_qty/total_so_qty * 100 + total_picked_qty = total_picked_qty + picked_qty + per_picked = total_picked_qty / total_so_qty * 100 - so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False) + so_doc.db_set("per_picked", flt(per_picked), update_modified=False) @frappe.whitelist() def set_item_locations(self, save=False): @@ -86,20 +108,26 @@ class PickList(Document): from_warehouses = None if self.parent_warehouse: - from_warehouses = frappe.db.get_descendants('Warehouse', self.parent_warehouse) + from_warehouses = frappe.db.get_descendants("Warehouse", self.parent_warehouse) # Create replica before resetting, to handle empty table on update after submit. - locations_replica = self.get('locations') + locations_replica = self.get("locations") # reset - self.delete_key('locations') + self.delete_key("locations") for item_doc in items: item_code = item_doc.item_code - self.item_location_map.setdefault(item_code, - get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company)) + self.item_location_map.setdefault( + item_code, + get_available_item_locations( + item_code, from_warehouses, self.item_count_map.get(item_code), self.company + ), + ) - locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus) + locations = get_items_with_location_and_quantity( + item_doc, self.item_location_map, self.docstatus + ) item_doc.idx = None item_doc.name = None @@ -107,23 +135,28 @@ class PickList(Document): for row in locations: location = item_doc.as_dict() location.update(row) - self.append('locations', location) + self.append("locations", location) # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red # and give feedback to the user. This is to avoid empty Pick Lists. - if not self.get('locations') and self.docstatus == 1: + if not self.get("locations") and self.docstatus == 1: for location in locations_replica: location.stock_qty = 0 location.picked_qty = 0 - self.append('locations', location) - frappe.msgprint(_("Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."), - title=_("Out of Stock"), indicator="red") + self.append("locations", location) + frappe.msgprint( + _( + "Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List." + ), + title=_("Out of Stock"), + indicator="red", + ) if save: self.save() def aggregate_item_qty(self): - locations = self.get('locations') + locations = self.get("locations") self.item_count_map = {} # aggregate qty for same item item_map = OrderedDict() @@ -150,8 +183,9 @@ class PickList(Document): return item_map.values() def validate_for_qty(self): - if self.purpose == "Material Transfer for Manufacture" \ - and (self.for_qty is None or self.for_qty == 0): + if self.purpose == "Material Transfer for Manufacture" and ( + self.for_qty is None or self.for_qty == 0 + ): frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) def before_print(self, settings=None): @@ -163,7 +197,7 @@ class PickList(Document): group_picked_qty = defaultdict(float) for item in self.locations: - group_item_qty[(item.item_code, item.warehouse)] += item.qty + group_item_qty[(item.item_code, item.warehouse)] += item.qty group_picked_qty[(item.item_code, item.warehouse)] += item.picked_qty duplicate_list = [] @@ -187,37 +221,47 @@ def validate_item_locations(pick_list): if not pick_list.locations: frappe.throw(_("Add items in the Item Locations table")) + def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus): available_locations = item_location_map.get(item_doc.item_code) locations = [] # if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock. - remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty + remaining_stock_qty = ( + item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty + ) while remaining_stock_qty > 0 and available_locations: item_location = available_locations.pop(0) item_location = frappe._dict(item_location) - stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty + stock_qty = ( + remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty + ) qty = stock_qty / (item_doc.conversion_factor or 1) - uom_must_be_whole_number = frappe.db.get_value('UOM', item_doc.uom, 'must_be_whole_number') + uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number") if uom_must_be_whole_number: qty = floor(qty) stock_qty = qty * item_doc.conversion_factor - if not stock_qty: break + if not stock_qty: + break serial_nos = None if item_location.serial_no: - serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)]) + serial_nos = "\n".join(item_location.serial_no[0 : cint(stock_qty)]) - locations.append(frappe._dict({ - 'qty': qty, - 'stock_qty': stock_qty, - 'warehouse': item_location.warehouse, - 'serial_no': serial_nos, - 'batch_no': item_location.batch_no - })) + locations.append( + frappe._dict( + { + "qty": qty, + "stock_qty": stock_qty, + "warehouse": item_location.warehouse, + "serial_no": serial_nos, + "batch_no": item_location.batch_no, + } + ) + ) remaining_stock_qty -= stock_qty @@ -227,55 +271,69 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) item_location.qty = qty_diff if item_location.serial_no: # set remaining serial numbers - item_location.serial_no = item_location.serial_no[-int(qty_diff):] + item_location.serial_no = item_location.serial_no[-int(qty_diff) :] available_locations = [item_location] + available_locations # update available locations for the item item_location_map[item_doc.item_code] = available_locations return locations -def get_available_item_locations(item_code, from_warehouses, required_qty, company, ignore_validation=False): + +def get_available_item_locations( + item_code, from_warehouses, required_qty, company, ignore_validation=False +): locations = [] - has_serial_no = frappe.get_cached_value('Item', item_code, 'has_serial_no') - has_batch_no = frappe.get_cached_value('Item', item_code, 'has_batch_no') + has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") + has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") if has_batch_no and has_serial_no: - locations = get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_serial_and_batched_item( + item_code, from_warehouses, required_qty, company + ) elif has_serial_no: - locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_serialized_item( + item_code, from_warehouses, required_qty, company + ) elif has_batch_no: - locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company + ) else: - locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company) + locations = get_available_item_locations_for_other_item( + item_code, from_warehouses, required_qty, company + ) - total_qty_available = sum(location.get('qty') for location in locations) + total_qty_available = sum(location.get("qty") for location in locations) remaining_qty = required_qty - total_qty_available if remaining_qty > 0 and not ignore_validation: - frappe.msgprint(_('{0} units of Item {1} is not available.') - .format(remaining_qty, frappe.get_desk_link('Item', item_code)), - title=_("Insufficient Stock")) + frappe.msgprint( + _("{0} units of Item {1} is not available.").format( + remaining_qty, frappe.get_desk_link("Item", item_code) + ), + title=_("Insufficient Stock"), + ) return locations -def get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company): - filters = frappe._dict({ - 'item_code': item_code, - 'company': company, - 'warehouse': ['!=', ''] - }) +def get_available_item_locations_for_serialized_item( + item_code, from_warehouses, required_qty, company +): + filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]}) if from_warehouses: - filters.warehouse = ['in', from_warehouses] + filters.warehouse = ["in", from_warehouses] - serial_nos = frappe.get_all('Serial No', - fields=['name', 'warehouse'], + serial_nos = frappe.get_all( + "Serial No", + fields=["name", "warehouse"], filters=filters, limit=required_qty, - order_by='purchase_date', - as_list=1) + order_by="purchase_date", + as_list=1, + ) warehouse_serial_nos_map = frappe._dict() for serial_no, warehouse in serial_nos: @@ -283,17 +341,17 @@ def get_available_item_locations_for_serialized_item(item_code, from_warehouses, locations = [] for warehouse, serial_nos in warehouse_serial_nos_map.items(): - locations.append({ - 'qty': len(serial_nos), - 'warehouse': warehouse, - 'serial_no': serial_nos - }) + locations.append({"qty": len(serial_nos), "warehouse": warehouse, "serial_no": serial_nos}) return locations -def get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company): - warehouse_condition = 'and warehouse in %(warehouses)s' if from_warehouses else '' - batch_locations = frappe.db.sql(""" + +def get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company +): + warehouse_condition = "and warehouse in %(warehouses)s" if from_warehouses else "" + batch_locations = frappe.db.sql( + """ SELECT sle.`warehouse`, sle.`batch_no`, @@ -314,84 +372,94 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re sle.`item_code` HAVING `qty` > 0 ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` - """.format(warehouse_condition=warehouse_condition), { #nosec - 'item_code': item_code, - 'company': company, - 'today': today(), - 'warehouses': from_warehouses - }, as_dict=1) + """.format( + warehouse_condition=warehouse_condition + ), + { # nosec + "item_code": item_code, + "company": company, + "today": today(), + "warehouses": from_warehouses, + }, + as_dict=1, + ) return batch_locations -def get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company): - # Get batch nos by FIFO - locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company) - filters = frappe._dict({ - 'item_code': item_code, - 'company': company, - 'warehouse': ['!=', ''], - 'batch_no': '' - }) +def get_available_item_locations_for_serial_and_batched_item( + item_code, from_warehouses, required_qty, company +): + # Get batch nos by FIFO + locations = get_available_item_locations_for_batched_item( + item_code, from_warehouses, required_qty, company + ) + + filters = frappe._dict( + {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} + ) # Get Serial Nos by FIFO for Batch No for location in locations: filters.batch_no = location.batch_no filters.warehouse = location.warehouse - location.qty = required_qty if location.qty > required_qty else location.qty # if extra qty in batch + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch - serial_nos = frappe.get_list('Serial No', - fields=['name'], - filters=filters, - limit=location.qty, - order_by='purchase_date') + serial_nos = frappe.get_list( + "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" + ) serial_nos = [sn.name for sn in serial_nos] location.serial_no = serial_nos return locations + def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): # gets all items available in different warehouses - warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")] + warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] - filters = frappe._dict({ - 'item_code': item_code, - 'warehouse': ['in', warehouses], - 'actual_qty': ['>', 0] - }) + filters = frappe._dict( + {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} + ) if from_warehouses: - filters.warehouse = ['in', from_warehouses] + filters.warehouse = ["in", from_warehouses] - item_locations = frappe.get_all('Bin', - fields=['warehouse', 'actual_qty as qty'], + item_locations = frappe.get_all( + "Bin", + fields=["warehouse", "actual_qty as qty"], filters=filters, limit=required_qty, - order_by='creation') + order_by="creation", + ) return item_locations @frappe.whitelist() def create_delivery_note(source_name, target_doc=None): - pick_list = frappe.get_doc('Pick List', source_name) + pick_list = frappe.get_doc("Pick List", source_name) validate_item_locations(pick_list) sales_dict = dict() sales_orders = [] delivery_note = None for location in pick_list.locations: if location.sales_order: - sales_orders.append([frappe.db.get_value("Sales Order",location.sales_order,'customer'),location.sales_order]) + sales_orders.append( + [frappe.db.get_value("Sales Order", location.sales_order, "customer"), location.sales_order] + ) # Group sales orders by customer - for key,keydata in groupby(sales_orders,key=itemgetter(0)): + for key, keydata in groupby(sales_orders, key=itemgetter(0)): sales_dict[key] = set([d[1] for d in keydata]) if sales_dict: - delivery_note = create_dn_with_so(sales_dict,pick_list) + delivery_note = create_dn_with_so(sales_dict, pick_list) is_item_wo_so = 0 - for location in pick_list.locations : + for location in pick_list.locations: if not location.sales_order: is_item_wo_so = 1 break @@ -399,64 +467,69 @@ def create_delivery_note(source_name, target_doc=None): # Create a DN for items without sales orders as well delivery_note = create_dn_wo_so(pick_list) - frappe.msgprint(_('Delivery Note(s) created for the Pick List')) + frappe.msgprint(_("Delivery Note(s) created for the Pick List")) return delivery_note + def create_dn_wo_so(pick_list): - delivery_note = frappe.new_doc("Delivery Note") + delivery_note = frappe.new_doc("Delivery Note") - item_table_mapper_without_so = { - 'doctype': 'Delivery Note Item', - 'field_map': { - 'rate': 'rate', - 'name': 'name', - 'parent': '', - } - } - map_pl_locations(pick_list,item_table_mapper_without_so,delivery_note) - delivery_note.insert(ignore_mandatory = True) + item_table_mapper_without_so = { + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "name", + "parent": "", + }, + } + map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note) + delivery_note.insert(ignore_mandatory=True) - return delivery_note + return delivery_note -def create_dn_with_so(sales_dict,pick_list): +def create_dn_with_so(sales_dict, pick_list): delivery_note = None for customer in sales_dict: for so in sales_dict[customer]: delivery_note = None - delivery_note = create_delivery_note_from_sales_order(so, - delivery_note, skip_item_mapping=True) + delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True) item_table_mapper = { - 'doctype': 'Delivery Note Item', - 'field_map': { - 'rate': 'rate', - 'name': 'so_detail', - 'parent': 'against_sales_order', + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "so_detail", + "parent": "against_sales_order", }, - 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1 + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, } break if delivery_note: # map all items of all sales orders of that customer for so in sales_dict[customer]: - map_pl_locations(pick_list,item_table_mapper,delivery_note,so) - delivery_note.insert(ignore_mandatory = True) + map_pl_locations(pick_list, item_table_mapper, delivery_note, so) + delivery_note.insert(ignore_mandatory=True) return delivery_note -def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None): + +def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): for location in pick_list.locations: if location.sales_order == sales_order: if location.sales_order_item: - sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item}) + sales_order_item = frappe.get_cached_doc( + "Sales Order Item", {"name": location.sales_order_item} + ) else: sales_order_item = None - source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \ - else [location, item_mapper] + source_doc, table_mapper = ( + [sales_order_item, item_mapper] if sales_order_item else [location, item_mapper] + ) dn_item = map_child_doc(source_doc, delivery_note, table_mapper) @@ -471,7 +544,7 @@ def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None): delivery_note.pick_list = pick_list.name delivery_note.company = pick_list.company - delivery_note.customer = frappe.get_value("Sales Order",sales_order,"customer") + delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") @frappe.whitelist() @@ -479,17 +552,17 @@ def create_stock_entry(pick_list): pick_list = frappe.get_doc(json.loads(pick_list)) validate_item_locations(pick_list) - if stock_entry_exists(pick_list.get('name')): - return frappe.msgprint(_('Stock Entry has been already created against this Pick List')) + if stock_entry_exists(pick_list.get("name")): + return frappe.msgprint(_("Stock Entry has been already created against this Pick List")) - stock_entry = frappe.new_doc('Stock Entry') - stock_entry.pick_list = pick_list.get('name') - stock_entry.purpose = pick_list.get('purpose') + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.pick_list = pick_list.get("name") + stock_entry.purpose = pick_list.get("purpose") stock_entry.set_stock_entry_type() - if pick_list.get('work_order'): + if pick_list.get("work_order"): stock_entry = update_stock_entry_based_on_work_order(pick_list, stock_entry) - elif pick_list.get('material_request'): + elif pick_list.get("material_request"): stock_entry = update_stock_entry_based_on_material_request(pick_list, stock_entry) else: stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry) @@ -499,9 +572,11 @@ def create_stock_entry(pick_list): return stock_entry.as_dict() + @frappe.whitelist() def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filters, as_dict): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT `name`, `company`, `planned_start_date` FROM @@ -517,25 +592,27 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte LIMIT %(start)s, %(page_length)s""", { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace('%', ''), - 'start': start, - 'page_length': frappe.utils.cint(page_length), - 'company': filters.get('company') - }, as_dict=as_dict) + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_length": frappe.utils.cint(page_length), + "company": filters.get("company"), + }, + as_dict=as_dict, + ) + @frappe.whitelist() def target_document_exists(pick_list_name, purpose): - if purpose == 'Delivery': - return frappe.db.exists('Delivery Note', { - 'pick_list': pick_list_name - }) + if purpose == "Delivery": + return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name}) return stock_entry_exists(pick_list_name) + @frappe.whitelist() def get_item_details(item_code, uom=None): - details = frappe.db.get_value('Item', item_code, ['stock_uom', 'name'], as_dict=1) + details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1) details.uom = uom or details.stock_uom if uom: details.update(get_conversion_factor(item_code, uom)) @@ -544,37 +621,37 @@ def get_item_details(item_code, uom=None): def update_delivery_note_item(source, target, delivery_note): - cost_center = frappe.db.get_value('Project', delivery_note.project, 'cost_center') + cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center") if not cost_center: - cost_center = get_cost_center(source.item_code, 'Item', delivery_note.company) + cost_center = get_cost_center(source.item_code, "Item", delivery_note.company) if not cost_center: - cost_center = get_cost_center(source.item_group, 'Item Group', delivery_note.company) + cost_center = get_cost_center(source.item_group, "Item Group", delivery_note.company) target.cost_center = cost_center + def get_cost_center(for_item, from_doctype, company): - '''Returns Cost Center for Item or Item Group''' - return frappe.db.get_value('Item Default', - fieldname=['buying_cost_center'], - filters={ - 'parent': for_item, - 'parenttype': from_doctype, - 'company': company - }) + """Returns Cost Center for Item or Item Group""" + return frappe.db.get_value( + "Item Default", + fieldname=["buying_cost_center"], + filters={"parent": for_item, "parenttype": from_doctype, "company": company}, + ) + def set_delivery_note_missing_values(target): - target.run_method('set_missing_values') - target.run_method('set_po_nos') - target.run_method('calculate_taxes_and_totals') + target.run_method("set_missing_values") + target.run_method("set_po_nos") + target.run_method("calculate_taxes_and_totals") + def stock_entry_exists(pick_list_name): - return frappe.db.exists('Stock Entry', { - 'pick_list': pick_list_name - }) + return frappe.db.exists("Stock Entry", {"pick_list": pick_list_name}) + def update_stock_entry_based_on_work_order(pick_list, stock_entry): - work_order = frappe.get_doc("Work Order", pick_list.get('work_order')) + work_order = frappe.get_doc("Work Order", pick_list.get("work_order")) stock_entry.work_order = work_order.name stock_entry.company = work_order.company @@ -583,10 +660,11 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): stock_entry.use_multi_level_bom = work_order.use_multi_level_bom stock_entry.fg_completed_qty = pick_list.for_qty if work_order.bom_no: - stock_entry.inspection_required = frappe.db.get_value('BOM', - work_order.bom_no, 'inspection_required') + stock_entry.inspection_required = frappe.db.get_value( + "BOM", work_order.bom_no, "inspection_required" + ) - is_wip_warehouse_group = frappe.db.get_value('Warehouse', work_order.wip_warehouse, 'is_group') + is_wip_warehouse_group = frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group") if not (is_wip_warehouse_group and work_order.skip_transfer): wip_warehouse = work_order.wip_warehouse else: @@ -600,32 +678,36 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry): update_common_item_properties(item, location) item.t_warehouse = wip_warehouse - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_stock_entry_based_on_material_request(pick_list, stock_entry): for location in pick_list.locations: target_warehouse = None if location.material_request_item: - target_warehouse = frappe.get_value('Material Request Item', - location.material_request_item, 'warehouse') + target_warehouse = frappe.get_value( + "Material Request Item", location.material_request_item, "warehouse" + ) item = frappe._dict() update_common_item_properties(item, location) item.t_warehouse = target_warehouse - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_stock_entry_items_with_no_reference(pick_list, stock_entry): for location in pick_list.locations: item = frappe._dict() update_common_item_properties(item, location) - stock_entry.append('items', item) + stock_entry.append("items", item) return stock_entry + def update_common_item_properties(item, location): item.item_code = location.item_code item.s_warehouse = location.warehouse diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index d19bedeeafe..92e57bed220 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -1,11 +1,7 @@ - - def get_data(): return { - 'fieldname': 'pick_list', - 'transactions': [ - { - 'items': ['Stock Entry', 'Delivery Note'] - }, - ] + "fieldname": "pick_list", + "transactions": [ + {"items": ["Stock Entry", "Delivery Note"]}, + ], } diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index f60104c09ac..7496b6b1798 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -4,7 +4,7 @@ import frappe from frappe import _dict -test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] +test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"] from frappe.tests.utils import FrappeTestCase @@ -19,146 +19,174 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( class TestPickList(FrappeTestCase): def test_pick_list_picks_warehouse_for_each_item(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 5 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 5, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) def test_pick_list_splits_row_according_to_warehouse_availability(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'warehouse': '_Test Warehouse Group-C1 - _TC', - 'valuation_rate': 100, - 'qty': 5 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "warehouse": "_Test Warehouse Group-C1 - _TC", + "valuation_rate": 100, + "qty": 5, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'warehouse': '_Test Warehouse 2 - _TC', - 'valuation_rate': 400, - 'qty': 10 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "warehouse": "_Test Warehouse 2 - _TC", + "valuation_rate": 400, + "qty": 10, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item Warehouse Group Wise Reorder', - 'qty': 1000, - 'stock_qty': 1000, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item Warehouse Group Wise Reorder", + "qty": 1000, + "stock_qty": 1000, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item Warehouse Group Wise Reorder') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse Group-C1 - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item Warehouse Group Wise Reorder") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse Group-C1 - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[1].item_code, '_Test Item Warehouse Group Wise Reorder') - self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse 2 - _TC') + self.assertEqual(pick_list.locations[1].item_code, "_Test Item Warehouse Group Wise Reorder") + self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse 2 - _TC") self.assertEqual(pick_list.locations[1].qty, 10) def test_pick_list_shows_serial_no_for_serialized_item(self): - stock_reconciliation = frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'purpose': 'Stock Reconciliation', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Serialized Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 5, - 'serial_no': '123450\n123451\n123452\n123453\n123454' - }] - }) + stock_reconciliation = frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "purpose": "Stock Reconciliation", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Serialized Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 5, + "serial_no": "123450\n123451\n123452\n123453\n123454", + } + ], + } + ) try: stock_reconciliation.submit() except EmptyStockReconciliationItemsError: pass - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Serialized Item', - 'qty': 1000, - 'stock_qty': 1000, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Serialized Item", + "qty": 1000, + "stock_qty": 1000, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + } + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Serialized Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Serialized Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[0].serial_no, '123450\n123451\n123452\n123453\n123454') + self.assertEqual(pick_list.locations[0].serial_no, "123450\n123451\n123452\n123453\n123454") def test_pick_list_shows_batch_no_for_batched_item(self): # check if oldest batch no is picked - item = frappe.db.exists("Item", {'item_name': 'Batched Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched Item"}) if not item: item = create_item("Batched Item") item.has_batch_no = 1 @@ -166,7 +194,7 @@ class TestPickList(FrappeTestCase): item.batch_number_series = "B-BATCH-.##" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched Item"}) pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0) @@ -175,27 +203,30 @@ class TestPickList(FrappeTestCase): pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0) - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Material Transfer', - 'locations': [{ - 'item_code': 'Batched Item', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "locations": [ + { + "item_code": "Batched Item", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + } + ], + } + ) pick_list.set_item_locations() self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) pr1.cancel() pr2.cancel() - def test_pick_list_for_batched_and_serialised_item(self): # check if oldest batch no and serial nos are picked - item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) if not item: item = create_item("Batched and Serialised Item") item.has_batch_no = 1 @@ -205,7 +236,7 @@ class TestPickList(FrappeTestCase): item.serial_no_series = "S-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) @@ -215,17 +246,21 @@ class TestPickList(FrappeTestCase): pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0) - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Material Transfer', - 'locations': [{ - 'item_code': 'Batched and Serialised Item', - 'qty': 2, - 'stock_qty': 2, - 'conversion_factor': 1, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Material Transfer", + "locations": [ + { + "item_code": "Batched and Serialised Item", + "qty": 2, + "stock_qty": 2, + "conversion_factor": 1, + } + ], + } + ) pick_list.set_item_locations() self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) @@ -236,64 +271,71 @@ class TestPickList(FrappeTestCase): def test_pick_list_for_items_from_multiple_sales_orders(self): try: - frappe.get_doc({ - 'doctype': 'Stock Reconciliation', - 'company': '_Test Company', - 'purpose': 'Opening Stock', - 'expense_account': 'Temporary Opening - _TC', - 'items': [{ - 'item_code': '_Test Item', - 'warehouse': '_Test Warehouse - _TC', - 'valuation_rate': 100, - 'qty': 10 - }] - }).submit() + frappe.get_doc( + { + "doctype": "Stock Reconciliation", + "company": "_Test Company", + "purpose": "Opening Stock", + "expense_account": "Temporary Opening - _TC", + "items": [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "valuation_rate": 100, + "qty": 10, + } + ], + } + ).submit() except EmptyStockReconciliationItemsError: pass - sales_order = frappe.get_doc({ - 'doctype': "Sales Order", - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 10, - 'delivery_date': frappe.utils.today() - }], - }) + sales_order = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [{"item_code": "_Test Item", "qty": 10, "delivery_date": frappe.utils.today()}], + } + ) sales_order.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': '_T-Sales Order-1', - 'sales_order_item': '_T-Sales Order-1_item', - }, { - 'item_code': '_Test Item', - 'qty': 5, - 'stock_qty': 5, - 'conversion_factor': 1, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[0].name, - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": "_T-Sales Order-1", + "sales_order_item": "_T-Sales Order-1_item", + }, + { + "item_code": "_Test Item", + "qty": 5, + "stock_qty": 5, + "conversion_factor": 1, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[0].name, + }, + ], + } + ) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].item_code, '_Test Item') - self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[0].item_code, "_Test Item") + self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[0].qty, 5) - self.assertEqual(pick_list.locations[0].sales_order_item, '_T-Sales Order-1_item') + self.assertEqual(pick_list.locations[0].sales_order_item, "_T-Sales Order-1_item") - self.assertEqual(pick_list.locations[1].item_code, '_Test Item') - self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse - _TC') + self.assertEqual(pick_list.locations[1].item_code, "_Test Item") + self.assertEqual(pick_list.locations[1].warehouse, "_Test Warehouse - _TC") self.assertEqual(pick_list.locations[1].qty, 5) self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name) @@ -301,47 +343,57 @@ class TestPickList(FrappeTestCase): purchase_receipt = make_purchase_receipt(item_code="_Test Item", qty=10) purchase_receipt.submit() - sales_order = frappe.get_doc({ - 'doctype': 'Sales Order', - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 5, - 'stock_qty':5, - 'delivery_date': frappe.utils.today() - }, { - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 1, - 'delivery_date': frappe.utils.today() - }], - }).insert() + sales_order = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 5, + "stock_qty": 5, + "delivery_date": frappe.utils.today(), + }, + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + }, + ], + } + ).insert() sales_order.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'customer': '_Test Customer', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'locations': [{ - 'item_code': '_Test Item', - 'qty': 2, - 'stock_qty': 1, - 'conversion_factor': 0.5, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[0].name , - }, { - 'item_code': '_Test Item', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - 'sales_order': sales_order.name, - 'sales_order_item': sales_order.items[1].name , - }] - }) + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "customer": "_Test Customer", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "locations": [ + { + "item_code": "_Test Item", + "qty": 2, + "stock_qty": 1, + "conversion_factor": 0.5, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[0].name, + }, + { + "item_code": "_Test Item", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order.name, + "sales_order_item": sales_order.items[1].name, + }, + ], + } + ) pick_list.set_item_locations() pick_list.submit() @@ -349,7 +401,9 @@ class TestPickList(FrappeTestCase): self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) - self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor) + self.assertEqual( + sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor + ) pick_list.cancel() sales_order.cancel() @@ -362,22 +416,30 @@ class TestPickList(FrappeTestCase): self.assertEqual(b.get(key), value, msg=f"{key} doesn't match") # nothing should be grouped - pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[ - _dict(item_code="A", warehouse="X", qty=1, picked_qty=2), - _dict(item_code="B", warehouse="X", qty=1, picked_qty=2), - _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2), - _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2), - ]) + pl = frappe.get_doc( + doctype="Pick List", + group_same_items=True, + locations=[ + _dict(item_code="A", warehouse="X", qty=1, picked_qty=2), + _dict(item_code="B", warehouse="X", qty=1, picked_qty=2), + _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2), + _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2), + ], + ) pl.before_print() self.assertEqual(len(pl.locations), 4) # grouping should halve the number of items - pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[ - _dict(item_code="A", warehouse="X", qty=5, picked_qty=1), - _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), - _dict(item_code="A", warehouse="X", qty=3, picked_qty=2), - _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2), - ]) + pl = frappe.get_doc( + doctype="Pick List", + group_same_items=True, + locations=[ + _dict(item_code="A", warehouse="X", qty=5, picked_qty=1), + _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2), + _dict(item_code="A", warehouse="X", qty=3, picked_qty=2), + _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2), + ], + ) pl.before_print() self.assertEqual(len(pl.locations), 2) @@ -389,93 +451,118 @@ class TestPickList(FrappeTestCase): _compare_dicts(expected_item, created_item) def test_multiple_dn_creation(self): - sales_order_1 = frappe.get_doc({ - 'doctype': 'Sales Order', - 'customer': '_Test Customer', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item', - 'qty': 1, - 'conversion_factor': 1, - 'delivery_date': frappe.utils.today() - }], - }).insert() - sales_order_1.submit() - sales_order_2 = frappe.get_doc({ - 'doctype': 'Sales Order', - 'customer': '_Test Customer 1', - 'company': '_Test Company', - 'items': [{ - 'item_code': '_Test Item 2', - 'qty': 1, - 'conversion_factor': 1, - 'delivery_date': frappe.utils.today() - }, + sales_order_1 = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + } ], - }).insert() - sales_order_2.submit() - pick_list = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'items_based_on': 'Sales Order', - 'purpose': 'Delivery', - 'picker':'P001', - 'locations': [{ - 'item_code': '_Test Item ', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - 'sales_order': sales_order_1.name, - 'sales_order_item': sales_order_1.items[0].name , - }, { - 'item_code': '_Test Item 2', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - 'sales_order': sales_order_2.name, - 'sales_order_item': sales_order_2.items[0].name , } - ] - }) + ).insert() + sales_order_1.submit() + sales_order_2 = frappe.get_doc( + { + "doctype": "Sales Order", + "customer": "_Test Customer 1", + "company": "_Test Company", + "items": [ + { + "item_code": "_Test Item 2", + "qty": 1, + "conversion_factor": 1, + "delivery_date": frappe.utils.today(), + }, + ], + } + ).insert() + sales_order_2.submit() + pick_list = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "items_based_on": "Sales Order", + "purpose": "Delivery", + "picker": "P001", + "locations": [ + { + "item_code": "_Test Item ", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order_1.name, + "sales_order_item": sales_order_1.items[0].name, + }, + { + "item_code": "_Test Item 2", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "sales_order": sales_order_2.name, + "sales_order_item": sales_order_2.items[0].name, + }, + ], + } + ) pick_list.set_item_locations() pick_list.submit() create_delivery_note(pick_list.name) - for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer"},fields={"name"}): - for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): - self.assertEqual(dn_item.item_code, '_Test Item') - self.assertEqual(dn_item.against_sales_order,sales_order_1.name) - for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer 1"},fields={"name"}): - for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): - self.assertEqual(dn_item.item_code, '_Test Item 2') - self.assertEqual(dn_item.against_sales_order,sales_order_2.name) - #test DN creation without so - pick_list_1 = frappe.get_doc({ - 'doctype': 'Pick List', - 'company': '_Test Company', - 'purpose': 'Delivery', - 'picker':'P001', - 'locations': [{ - 'item_code': '_Test Item ', - 'qty': 1, - 'stock_qty': 1, - 'conversion_factor': 1, - }, { - 'item_code': '_Test Item 2', - 'qty': 2, - 'stock_qty': 2, - 'conversion_factor': 1, + for dn in frappe.get_all( + "Delivery Note", + filters={"pick_list": pick_list.name, "customer": "_Test Customer"}, + fields={"name"}, + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + self.assertEqual(dn_item.item_code, "_Test Item") + self.assertEqual(dn_item.against_sales_order, sales_order_1.name) + for dn in frappe.get_all( + "Delivery Note", + filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"}, + fields={"name"}, + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + self.assertEqual(dn_item.item_code, "_Test Item 2") + self.assertEqual(dn_item.against_sales_order, sales_order_2.name) + # test DN creation without so + pick_list_1 = frappe.get_doc( + { + "doctype": "Pick List", + "company": "_Test Company", + "purpose": "Delivery", + "picker": "P001", + "locations": [ + { + "item_code": "_Test Item ", + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + }, + { + "item_code": "_Test Item 2", + "qty": 2, + "stock_qty": 2, + "conversion_factor": 1, + }, + ], } - ] - }) + ) pick_list_1.set_item_locations() pick_list_1.submit() create_delivery_note(pick_list_1.name) - for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list_1.name},fields={"name"}): - for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"): - if dn_item.item_code == '_Test Item': - self.assertEqual(dn_item.qty,1) - if dn_item.item_code == '_Test Item 2': - self.assertEqual(dn_item.qty,2) + for dn in frappe.get_all( + "Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"} + ): + for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): + if dn_item.item_code == "_Test Item": + self.assertEqual(dn_item.qty, 1) + if dn_item.item_code == "_Test Item 2": + self.assertEqual(dn_item.qty, 2) # def test_pick_list_skips_items_in_expired_batch(self): # pass diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 8a3172e9e22..554055fd839 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -31,9 +31,11 @@ class PriceList(Document): frappe.set_value("Buying Settings", "Buying Settings", "buying_price_list", self.name) def update_item_price(self): - frappe.db.sql("""update `tabItem Price` set currency=%s, + frappe.db.sql( + """update `tabItem Price` set currency=%s, buying=%s, selling=%s, modified=NOW() where price_list=%s""", - (self.currency, cint(self.buying), cint(self.selling), self.name)) + (self.currency, cint(self.buying), cint(self.selling), self.name), + ) def check_impact_on_shopping_cart(self): "Check if Price List currency change impacts E Commerce Cart." @@ -66,12 +68,14 @@ class PriceList(Document): def delete_price_list_details_key(self): frappe.cache().hdel("price_list_details", self.name) + def get_price_list_details(price_list): price_list_details = frappe.cache().hget("price_list_details", price_list) if not price_list_details: - price_list_details = frappe.get_cached_value("Price List", price_list, - ["currency", "price_not_uom_dependent", "enabled"], as_dict=1) + price_list_details = frappe.get_cached_value( + "Price List", price_list, ["currency", "price_not_uom_dependent", "enabled"], as_dict=1 + ) if not price_list_details or not price_list_details.get("enabled"): throw(_("Price List {0} is disabled or does not exist").format(price_list)) diff --git a/erpnext/stock/doctype/price_list/test_price_list.py b/erpnext/stock/doctype/price_list/test_price_list.py index b8218b942e7..93660930c79 100644 --- a/erpnext/stock/doctype/price_list/test_price_list.py +++ b/erpnext/stock/doctype/price_list/test_price_list.py @@ -6,4 +6,4 @@ import frappe # test_ignore = ["Item"] -test_records = frappe.get_test_records('Price List') +test_records = frappe.get_test_records("Price List") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 2a6b4ea34b4..37074a2f000 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -17,82 +17,85 @@ from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.buying_controller import BuyingController from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class PurchaseReceipt(BuyingController): def __init__(self, *args, **kwargs): super(PurchaseReceipt, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Order', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_dt': 'Purchase Receipt Item', - 'source_field': 'received_qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': 'received_qty', - 'second_join_field': 'po_detail', - 'percent_join_field': 'purchase_order', - 'overflow_type': 'receipt', - 'second_source_extra_cond': """ and exists(select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""" - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Material Request Item', - 'join_field': 'material_request_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Material Request', - 'target_parent_field': 'per_received', - 'target_ref_field': 'stock_qty', - 'source_field': 'stock_qty', - 'percent_join_field': 'material_request' - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Invoice Item', - 'join_field': 'purchase_invoice_item', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Invoice', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_field': 'received_qty', - 'percent_join_field': 'purchase_invoice', - 'overflow_type': 'receipt' - }] + self.status_updater = [ + { + "target_dt": "Purchase Order Item", + "join_field": "purchase_order_item", + "target_field": "received_qty", + "target_parent_dt": "Purchase Order", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_dt": "Purchase Receipt Item", + "source_field": "received_qty", + "second_source_dt": "Purchase Invoice Item", + "second_source_field": "received_qty", + "second_join_field": "po_detail", + "percent_join_field": "purchase_order", + "overflow_type": "receipt", + "second_source_extra_cond": """ and exists(select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Material Request Item", + "join_field": "material_request_item", + "target_field": "received_qty", + "target_parent_dt": "Material Request", + "target_parent_field": "per_received", + "target_ref_field": "stock_qty", + "source_field": "stock_qty", + "percent_join_field": "material_request", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Invoice Item", + "join_field": "purchase_invoice_item", + "target_field": "received_qty", + "target_parent_dt": "Purchase Invoice", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_field": "received_qty", + "percent_join_field": "purchase_invoice", + "overflow_type": "receipt", + }, + ] if cint(self.is_return): - self.status_updater.extend([ - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'purchase_order_item', - 'target_field': 'returned_qty', - 'source_field': '-1 * qty', - 'second_source_dt': 'Purchase Invoice Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'po_detail', - 'extra_cond': """ and exists (select name from `tabPurchase Receipt` + self.status_updater.extend( + [ + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Order Item", + "join_field": "purchase_order_item", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "second_source_dt": "Purchase Invoice Item", + "second_source_field": "-1 * qty", + "second_join_field": "po_detail", + "extra_cond": """ and exists (select name from `tabPurchase Receipt` where name=`tabPurchase Receipt Item`.parent and is_return=1)""", - 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""" - }, - { - 'source_dt': 'Purchase Receipt Item', - 'target_dt': 'Purchase Receipt Item', - 'join_field': 'purchase_receipt_item', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Purchase Receipt', - 'target_parent_field': 'per_returned', - 'target_ref_field': 'received_stock_qty', - 'source_field': '-1 * received_stock_qty', - 'percent_join_field_parent': 'return_against' - } - ]) + "second_source_extra_cond": """ and exists (select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)""", + }, + { + "source_dt": "Purchase Receipt Item", + "target_dt": "Purchase Receipt Item", + "join_field": "purchase_receipt_item", + "target_field": "returned_qty", + "target_parent_dt": "Purchase Receipt", + "target_parent_field": "per_returned", + "target_ref_field": "received_stock_qty", + "source_field": "-1 * received_stock_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule @@ -104,8 +107,8 @@ class PurchaseReceipt(BuyingController): self.validate_posting_time() super(PurchaseReceipt, self).validate() - if self._action=="submit": - self.make_batches('warehouse') + if self._action == "submit": + self.make_batches("warehouse") else: self.set_status() @@ -125,20 +128,23 @@ class PurchaseReceipt(BuyingController): 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'): + for item in self.get("items"): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): # check cwip accounts before making auto assets # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account arbnb_account = self.get_company_default("asset_received_but_not_billed") - cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ - company = self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) break def validate_provisional_expense_account(self): - provisional_accounting_for_non_stock_items = \ - cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) if provisional_accounting_for_non_stock_items: default_provisional_account = self.get_company_default("default_provisional_account") @@ -146,56 +152,68 @@ class PurchaseReceipt(BuyingController): self.provisional_expense_account = default_provisional_account def validate_with_previous_doc(self): - super(PurchaseReceipt, self).validate_with_previous_doc({ - "Purchase Order": { - "ref_dn_field": "purchase_order", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Purchase Order Item": { - "ref_dn_field": "purchase_order_item", - "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True + super(PurchaseReceipt, self).validate_with_previous_doc( + { + "Purchase Order": { + "ref_dn_field": "purchase_order", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Purchase Order Item": { + "ref_dn_field": "purchase_order_item", + "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, } - }) + ) - if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')) and not self.is_return: - self.validate_rate_with_reference_doc([["Purchase Order", "purchase_order", "purchase_order_item"]]) + if ( + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return + ): + self.validate_rate_with_reference_doc( + [["Purchase Order", "purchase_order", "purchase_order_item"]] + ) def po_required(self): - if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes': - for d in self.get('items'): + if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": + for d in self.get("items"): if not d.purchase_order: frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code)) def get_already_received_qty(self, po, po_detail): - qty = frappe.db.sql("""select sum(qty) from `tabPurchase Receipt Item` + qty = frappe.db.sql( + """select sum(qty) from `tabPurchase Receipt Item` where purchase_order_item = %s and docstatus = 1 and purchase_order=%s - and parent != %s""", (po_detail, po, self.name)) + and parent != %s""", + (po_detail, po, self.name), + ) return qty and flt(qty[0][0]) or 0.0 def get_po_qty_and_warehouse(self, po_detail): - po_qty, po_warehouse = frappe.db.get_value("Purchase Order Item", po_detail, - ["qty", "warehouse"]) + po_qty, po_warehouse = frappe.db.get_value( + "Purchase Order Item", po_detail, ["qty", "warehouse"] + ) return po_qty, po_warehouse # Check for Closed status def check_on_hold_or_closed_status(self): - check_list =[] - for d in self.get('items'): - if (d.meta.get_field('purchase_order') and d.purchase_order - and d.purchase_order not in check_list): + check_list = [] + for d in self.get("items"): + if ( + d.meta.get_field("purchase_order") and d.purchase_order and d.purchase_order not in check_list + ): check_list.append(d.purchase_order) - check_on_hold_or_closed_status('Purchase Order', d.purchase_order) + check_on_hold_or_closed_status("Purchase Order", d.purchase_order) # on submit def on_submit(self): super(PurchaseReceipt, self).on_submit() # Check for Approving Authority - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total + ) self.update_prevdoc_status() if flt(self.per_billed) < 100: @@ -203,13 +221,13 @@ class PurchaseReceipt(BuyingController): else: self.db_set("status", "Completed") - # Updating stock ledger should always be called after updating prevdoc status, # because updating ordered qty, reserved_qty_for_subcontract in bin # depends upon updated ordered qty in PO self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") self.make_gl_entries() @@ -217,10 +235,12 @@ class PurchaseReceipt(BuyingController): self.set_consumed_qty_in_po() def check_next_docstatus(self): - submit_rv = frappe.db.sql("""select t1.name + submit_rv = frappe.db.sql( + """select t1.name from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - (self.name)) + (self.name), + ) if submit_rv: frappe.throw(_("Purchase Invoice {0} is already submitted").format(self.submit_rv[0][0])) @@ -229,10 +249,12 @@ class PurchaseReceipt(BuyingController): self.check_on_hold_or_closed_status() # Check if Purchase Invoice has been submitted against current Purchase Order - submitted = frappe.db.sql("""select t1.name + submitted = frappe.db.sql( + """select t1.name from `tabPurchase Invoice` t1,`tabPurchase Invoice Item` t2 where t1.name = t2.parent and t2.purchase_receipt = %s and t1.docstatus = 1""", - self.name) + self.name, + ) if submitted: frappe.throw(_("Purchase Invoice {0} is already submitted").format(submitted[0][0])) @@ -244,19 +266,24 @@ class PurchaseReceipt(BuyingController): self.update_stock_ledger() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.delete_auto_created_batches() self.set_consumed_qty_in_po() @frappe.whitelist() def get_current_stock(self): - for d in self.get('supplied_items'): + for d in self.get("supplied_items"): if self.supplier_warehouse: - bin = frappe.db.sql("select actual_qty from `tabBin` where item_code = %s and warehouse = %s", (d.rm_item_code, self.supplier_warehouse), as_dict = 1) - d.current_stock = bin and flt(bin[0]['actual_qty']) or 0 + bin = frappe.db.sql( + "select actual_qty from `tabBin` where item_code = %s and warehouse = %s", + (d.rm_item_code, self.supplier_warehouse), + as_dict=1, + ) + d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0 def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map + gl_entries = [] self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) @@ -273,29 +300,44 @@ class PurchaseReceipt(BuyingController): warehouse_with_no_account = [] stock_items = self.get_stock_items() - provisional_accounting_for_non_stock_items = \ - cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items')) + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) for d in self.get("items"): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if warehouse_account.get(d.warehouse): - stock_value_diff = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": self.name, - "voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference") + stock_value_diff = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": self.name, + "voucher_detail_no": d.name, + "warehouse": d.warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) warehouse_account_name = warehouse_account[d.warehouse]["account"] warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") - supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get("account_currency") + supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( + "account_currency" + ) remarks = self.get("remarks") or _("Accounting Entry for Stock") # If PR is sub-contracted and fg item rate is zero # in that case if account for source and target warehouse are same, # then GL entries should not be posted - if flt(stock_value_diff) == flt(d.rm_supp_cost) \ - and warehouse_account.get(self.supplier_warehouse) \ - and warehouse_account_name == supplier_warehouse_account: - continue + if ( + flt(stock_value_diff) == flt(d.rm_supp_cost) + and warehouse_account.get(self.supplier_warehouse) + and warehouse_account_name == supplier_warehouse_account + ): + continue self.add_gl_entry( gl_entries=gl_entries, @@ -306,18 +348,24 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=stock_rbnb, account_currency=warehouse_account_currency, - item=d) + item=d, + ) # GL Entry for from warehouse or Stock Received but not billed # Intentionally passed negative debit amount to avoid incorrect GL Entry validation - credit_currency = get_account_currency(warehouse_account[d.from_warehouse]['account']) \ - if d.from_warehouse else get_account_currency(stock_rbnb) + credit_currency = ( + get_account_currency(warehouse_account[d.from_warehouse]["account"]) + if d.from_warehouse + else get_account_currency(stock_rbnb) + ) - credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ - if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) + credit_amount = ( + flt(d.base_net_amount, d.precision("base_net_amount")) + if credit_currency == self.company_currency + else flt(d.net_amount, d.precision("net_amount")) + ) if credit_amount: - account = warehouse_account[d.from_warehouse]['account'] \ - if d.from_warehouse else stock_rbnb + account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb self.add_gl_entry( gl_entries=gl_entries, @@ -329,14 +377,18 @@ class PurchaseReceipt(BuyingController): against_account=warehouse_account_name, debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, - item=d) + item=d, + ) # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): account_currency = get_account_currency(account) - credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or - account_currency!=self.company_currency) else flt(amount["amount"])) + credit_amount = ( + flt(amount["base_amount"]) + if (amount["base_amount"] or account_currency != self.company_currency) + else flt(amount["amount"]) + ) self.add_gl_entry( gl_entries=gl_entries, @@ -349,7 +401,8 @@ class PurchaseReceipt(BuyingController): credit_in_account_currency=flt(amount["amount"]), account_currency=account_currency, project=d.project, - item=d) + item=d, + ) # sub-contracting warehouse if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): @@ -362,22 +415,32 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=warehouse_account_name, account_currency=supplier_warehouse_account_currency, - item=d) + item=d, + ) # divisional loss adjustment - valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \ - flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) + valuation_amount_as_per_doc = ( + flt(d.base_net_amount, d.precision("base_net_amount")) + + flt(d.landed_cost_voucher_amount) + + flt(d.rm_supp_cost) + + flt(d.item_tax_amount) + ) - divisional_loss = flt(valuation_amount_as_per_doc - stock_value_diff, - d.precision("base_net_amount")) + divisional_loss = flt( + valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount") + ) if divisional_loss: if self.is_return or flt(d.item_tax_amount): loss_account = expenses_included_in_valuation else: - loss_account = self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb + loss_account = ( + self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb + ) - cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") + cost_center = d.cost_center or frappe.get_cached_value( + "Company", self.company, "cost_center" + ) self.add_gl_entry( gl_entries=gl_entries, @@ -389,20 +452,31 @@ class PurchaseReceipt(BuyingController): against_account=warehouse_account_name, account_currency=credit_currency, project=d.project, - item=d) + item=d, + ) - elif d.warehouse not in warehouse_with_no_account or \ - d.rejected_warehouse not in warehouse_with_no_account: - warehouse_with_no_account.append(d.warehouse) - elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and provisional_accounting_for_non_stock_items: + elif ( + d.warehouse not in warehouse_with_no_account + or d.rejected_warehouse not in warehouse_with_no_account + ): + warehouse_with_no_account.append(d.warehouse) + elif ( + d.item_code not in stock_items + and not d.is_fixed_asset + and flt(d.qty) + and provisional_accounting_for_non_stock_items + ): self.add_provisional_gl_entry(d, gl_entries, self.posting_date) if warehouse_with_no_account: - frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + - "\n".join(warehouse_with_no_account)) + frappe.msgprint( + _("No accounting entries for the following warehouses") + + ": \n" + + "\n".join(warehouse_with_no_account) + ) def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0): - provisional_expense_account = self.get('provisional_expense_account') + provisional_expense_account = self.get("provisional_expense_account") credit_currency = get_account_currency(provisional_expense_account) debit_currency = get_account_currency(item.expense_account) expense_account = item.expense_account @@ -411,7 +485,9 @@ class PurchaseReceipt(BuyingController): if reverse: multiplication_factor = -1 - expense_account = frappe.db.get_value('Purchase Receipt Item', {'name': item.get('pr_detail')}, ['expense_account']) + expense_account = frappe.db.get_value( + "Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"] + ) self.add_gl_entry( gl_entries=gl_entries, @@ -425,7 +501,8 @@ class PurchaseReceipt(BuyingController): project=item.project, voucher_detail_no=item.name, item=item, - posting_date=posting_date) + posting_date=posting_date, + ) self.add_gl_entry( gl_entries=gl_entries, @@ -435,27 +512,35 @@ class PurchaseReceipt(BuyingController): credit=0.0, remarks=remarks, against_account=provisional_expense_account, - account_currency = debit_currency, + account_currency=debit_currency, project=item.project, voucher_detail_no=item.name, item=item, - posting_date=posting_date) + posting_date=posting_date, + ) def make_tax_gl_entries(self, gl_entries): if erpnext.is_perpetual_inventory_enabled(self.company): expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')]) + negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): - if tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): + if tax.category in ("Valuation", "Valuation and Total") and flt( + tax.base_tax_amount_after_discount_amount + ): if not tax.cost_center: - frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) + frappe.throw( + _("Cost Center is required in row {0} in Taxes table for type {1}").format( + tax.idx, _(tax.category) + ) + ) valuation_tax.setdefault(tax.name, 0) - valuation_tax[tax.name] += \ - (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) + valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt( + tax.base_tax_amount_after_discount_amount + ) if negative_expense_to_be_booked and valuation_tax: # Backward compatibility: @@ -464,10 +549,13 @@ class PurchaseReceipt(BuyingController): # post valuation related charges on "Stock Received But Not Billed" # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account - negative_expense_booked_in_pi = frappe.db.sql("""select name from `tabPurchase Invoice Item` pi + negative_expense_booked_in_pi = frappe.db.sql( + """select name from `tabPurchase Invoice Item` pi where docstatus = 1 and purchase_receipt=%s and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' - and voucher_no=pi.parent and account=%s)""", (self.name, expenses_included_in_valuation)) + and voucher_no=pi.parent and account=%s)""", + (self.name, expenses_included_in_valuation), + ) against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) @@ -485,7 +573,9 @@ class PurchaseReceipt(BuyingController): if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss else: - applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) + applicable_amount = negative_expense_to_be_booked * ( + valuation_tax[tax.name] / total_valuation_amount + ) amount_including_divisional_loss -= applicable_amount self.add_gl_entry( @@ -496,13 +586,28 @@ class PurchaseReceipt(BuyingController): credit=applicable_amount, remarks=self.remarks or _("Accounting Entry for Stock"), against_account=against_account, - item=tax) + item=tax, + ) i += 1 - def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account, - debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None, - project=None, voucher_detail_no=None, item=None, posting_date=None): + def add_gl_entry( + self, + gl_entries, + account, + cost_center, + debit, + credit, + remarks, + against_account, + debit_in_account_currency=None, + credit_in_account_currency=None, + account_currency=None, + project=None, + voucher_detail_no=None, + item=None, + posting_date=None, + ): gl_entry = { "account": account, @@ -542,17 +647,19 @@ class PurchaseReceipt(BuyingController): def add_asset_gl_entries(self, item, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") # This returns category's cwip account if not then fallback to company's default cwip account - cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ - company = self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) + asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) remarks = self.get("remarks") or _("Accounting Entry for Asset") cwip_account_currency = get_account_currency(cwip_account) # debit cwip account - debit_in_account_currency = (base_asset_amount - if cwip_account_currency == self.company_currency else asset_amount) + debit_in_account_currency = ( + base_asset_amount if cwip_account_currency == self.company_currency else asset_amount + ) self.add_gl_entry( gl_entries=gl_entries, @@ -563,12 +670,14 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=arbnb_account, debit_in_account_currency=debit_in_account_currency, - item=item) + item=item, + ) asset_rbnb_currency = get_account_currency(arbnb_account) # credit arbnb account - credit_in_account_currency = (base_asset_amount - if asset_rbnb_currency == self.company_currency else asset_amount) + credit_in_account_currency = ( + base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount + ) self.add_gl_entry( gl_entries=gl_entries, @@ -579,13 +688,17 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=cwip_account, credit_in_account_currency=credit_in_account_currency, - item=item) + item=item, + ) def add_lcv_gl_entries(self, item, gl_entries): - expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") + expenses_included_in_asset_valuation = self.get_company_default( + "expenses_included_in_asset_valuation" + ) if not is_cwip_accounting_enabled(item.asset_category): - asset_account = get_asset_category_account(asset_category=item.asset_category, \ - fieldname='fixed_asset_account', company=self.company) + asset_account = get_asset_category_account( + asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company + ) else: # This returns company's default cwip account asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) @@ -601,7 +714,8 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=asset_account, project=item.project, - item=item) + item=item, + ) self.add_gl_entry( gl_entries=gl_entries, @@ -612,11 +726,12 @@ class PurchaseReceipt(BuyingController): remarks=remarks, against_account=expenses_included_in_asset_valuation, project=item.project, - item=item) + item=item, + ) def update_assets(self, item, valuation_rate): - assets = frappe.db.get_all('Asset', - filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } + assets = frappe.db.get_all( + "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} ) for asset in assets: @@ -632,7 +747,7 @@ class PurchaseReceipt(BuyingController): updated_pr = [self.name] for d in self.get("items"): if d.get("purchase_invoice") and d.get("purchase_invoice_item"): - d.db_set('billed_amt', d.amount, update_modified=update_modified) + d.db_set("billed_amt", d.amount, update_modified=update_modified) elif d.purchase_order_item: updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified) @@ -642,24 +757,35 @@ class PurchaseReceipt(BuyingController): self.load_from_db() + def update_billed_amount_based_on_po(po_detail, update_modified=True): # Billed against Sales Order directly - billed_against_po = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail) + billed_against_po = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", + po_detail, + ) billed_against_po = billed_against_po and billed_against_po[0][0] or 0 # Get all Purchase Receipt Item rows against the Purchase Order Item row - pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent + pr_details = frappe.db.sql( + """select pr_item.name, pr_item.amount, pr_item.parent from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name=pr_item.parent and pr_item.purchase_order_item=%s and pr.docstatus=1 and pr.is_return = 0 - order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", po_detail, as_dict=1) + order by pr.posting_date asc, pr.posting_time asc, pr.name asc""", + po_detail, + as_dict=1, + ) updated_pr = [] for pr_item in pr_details: # Get billed amount directly against Purchase Receipt - billed_amt_agianst_pr = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", pr_item.name) + billed_amt_agianst_pr = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where pr_detail=%s and docstatus=1""", + pr_item.name, + ) billed_amt_agianst_pr = billed_amt_agianst_pr and billed_amt_agianst_pr[0][0] or 0 # Distribute billed amount directly against PO between PRs based on FIFO @@ -672,12 +798,19 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True): billed_amt_agianst_pr += billed_against_po billed_against_po = 0 - frappe.db.set_value("Purchase Receipt Item", pr_item.name, "billed_amt", billed_amt_agianst_pr, update_modified=update_modified) + frappe.db.set_value( + "Purchase Receipt Item", + pr_item.name, + "billed_amt", + billed_amt_agianst_pr, + update_modified=update_modified, + ) updated_pr.append(pr_item.parent) return updated_pr + def update_billing_percentage(pr_doc, update_modified=True): # Reload as billed amount was set in db directly pr_doc.load_from_db() @@ -685,15 +818,15 @@ def update_billing_percentage(pr_doc, update_modified=True): # Update Billing % based on pending accepted qty total_amount, total_billed_amount = 0, 0 for item in pr_doc.items: - return_data = frappe.db.get_list("Purchase Receipt", - fields = [ - "sum(abs(`tabPurchase Receipt Item`.qty)) as qty" - ], - filters = [ + return_data = frappe.db.get_list( + "Purchase Receipt", + fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"], + filters=[ ["Purchase Receipt", "docstatus", "=", 1], ["Purchase Receipt", "is_return", "=", 1], - ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name] - ]) + ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name], + ], + ) returned_qty = return_data[0].qty if return_data else 0 returned_amount = flt(returned_qty) * flt(item.rate) @@ -711,11 +844,12 @@ def update_billing_percentage(pr_doc, update_modified=True): pr_doc.set_status(update=True) pr_doc.notify_update() + @frappe.whitelist() def make_purchase_invoice(source_name, target_doc=None): from erpnext.accounts.party import get_payment_terms_template - doc = frappe.get_doc('Purchase Receipt', source_name) + doc = frappe.get_doc("Purchase Receipt", source_name) returned_qty_map = get_returned_qty_map(source_name) invoiced_qty_map = get_invoiced_qty_map(source_name) @@ -724,7 +858,9 @@ def make_purchase_invoice(source_name, target_doc=None): frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) - doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company) + doc.payment_terms_template = get_payment_terms_template( + source.supplier, "Supplier", source.company + ) doc.run_method("onload") doc.run_method("set_missing_values") doc.run_method("calculate_taxes_and_totals") @@ -732,14 +868,20 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + if frappe.db.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ): target_doc.rejected_qty = 0 - target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor")) + target_doc.stock_qty = flt(target_doc.qty) * flt( + target_doc.conversion_factor, target_doc.precision("conversion_factor") + ) returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): qty = item_row.qty - if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + if frappe.db.get_single_value( + "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" + ): qty = item_row.received_qty pending_qty = qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = flt(returned_qty_map.get(item_row.name, 0)) @@ -752,69 +894,85 @@ def make_purchase_invoice(source_name, target_doc=None): returned_qty = 0 return pending_qty, returned_qty - - doclist = get_mapped_doc("Purchase Receipt", source_name, { - "Purchase Receipt": { - "doctype": "Purchase Invoice", - "field_map": { - "supplier_warehouse":"supplier_warehouse", - "is_return": "is_return", - "bill_date": "bill_date" + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Purchase Invoice", + "field_map": { + "supplier_warehouse": "supplier_warehouse", + "is_return": "is_return", + "bill_date": "bill_date", + }, + "validation": { + "docstatus": ["=", 1], + }, }, - "validation": { - "docstatus": ["=", 1], + "Purchase Receipt Item": { + "doctype": "Purchase Invoice Item", + "field_map": { + "name": "pr_detail", + "parent": "purchase_receipt", + "purchase_order_item": "po_detail", + "purchase_order": "purchase_order", + "is_fixed_asset": "is_fixed_asset", + "asset_location": "asset_location", + "asset_category": "asset_category", + }, + "postprocess": update_item, + "filter": lambda d: get_pending_qty(d)[0] <= 0 + if not doc.get("is_return") + else get_pending_qty(d)[0] > 0, }, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, }, - "Purchase Receipt Item": { - "doctype": "Purchase Invoice Item", - "field_map": { - "name": "pr_detail", - "parent": "purchase_receipt", - "purchase_order_item": "po_detail", - "purchase_order": "purchase_order", - "is_fixed_asset": "is_fixed_asset", - "asset_location": "asset_location", - "asset_category": 'asset_category' - }, - "postprocess": update_item, - "filter": lambda d: get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 - }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges", - "add_if_empty": True - } - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) - doclist.set_onload('ignore_price_list', True) + doclist.set_onload("ignore_price_list", True) return doclist + def get_invoiced_qty_map(purchase_receipt): """returns a map: {pr_detail: invoiced_qty}""" invoiced_qty_map = {} - for pr_detail, qty in frappe.db.sql("""select pr_detail, qty from `tabPurchase Invoice Item` - where purchase_receipt=%s and docstatus=1""", purchase_receipt): - if not invoiced_qty_map.get(pr_detail): - invoiced_qty_map[pr_detail] = 0 - invoiced_qty_map[pr_detail] += qty + for pr_detail, qty in frappe.db.sql( + """select pr_detail, qty from `tabPurchase Invoice Item` + where purchase_receipt=%s and docstatus=1""", + purchase_receipt, + ): + if not invoiced_qty_map.get(pr_detail): + invoiced_qty_map[pr_detail] = 0 + invoiced_qty_map[pr_detail] += qty return invoiced_qty_map + def get_returned_qty_map(purchase_receipt): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty + returned_qty_map = frappe._dict( + frappe.db.sql( + """select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name = pr_item.parent and pr.docstatus = 1 and pr.is_return = 1 and pr.return_against = %s - """, purchase_receipt)) + """, + purchase_receipt, + ) + ) return returned_qty_map + @frappe.whitelist() def make_purchase_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Purchase Receipt", source_name, target_doc) @@ -823,35 +981,47 @@ def update_purchase_receipt_status(docname, status): pr = frappe.get_doc("Purchase Receipt", docname) pr.update_status(status) + @frappe.whitelist() -def make_stock_entry(source_name,target_doc=None): +def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.stock_entry_type = "Material Transfer" - target.purpose = "Material Transfer" + target.purpose = "Material Transfer" - doclist = get_mapped_doc("Purchase Receipt", source_name,{ - "Purchase Receipt": { - "doctype": "Stock Entry", - }, - "Purchase Receipt Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "warehouse": "s_warehouse", - "parent": "reference_purchase_receipt", - "batch_no": "batch_no" + doclist = get_mapped_doc( + "Purchase Receipt", + source_name, + { + "Purchase Receipt": { + "doctype": "Stock Entry", + }, + "Purchase Receipt Item": { + "doctype": "Stock Entry Detail", + "field_map": { + "warehouse": "s_warehouse", + "parent": "reference_purchase_receipt", + "batch_no": "batch_no", + }, }, }, - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def make_inter_company_delivery_note(source_name, target_doc=None): return make_inter_company_transaction("Purchase Receipt", source_name, target_doc) + def get_item_account_wise_additional_cost(purchase_document): - landed_cost_vouchers = frappe.get_all("Landed Cost Purchase Receipt", fields=["parent"], - filters = {"receipt_document": purchase_document, "docstatus": 1}) + landed_cost_vouchers = frappe.get_all( + "Landed Cost Purchase Receipt", + fields=["parent"], + filters={"receipt_document": purchase_document, "docstatus": 1}, + ) if not landed_cost_vouchers: return @@ -861,9 +1031,9 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) - #Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually': - based_on_field = 'amount' + # Use amount field for total item cost for manually cost distributed LCVs + if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually": + based_on_field = "amount" else: based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) @@ -876,18 +1046,20 @@ def get_item_account_wise_additional_cost(purchase_document): if item.receipt_document == purchase_document: for account in landed_cost_voucher_doc.taxes: item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {}) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, { - "amount": 0.0, - "base_amount": 0.0 - }) + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault( + account.expense_account, {"amount": 0.0, "base_amount": 0.0} + ) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["amount"] += \ - account.amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ + "amount" + ] += (account.amount * item.get(based_on_field) / total_item_cost) - item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["base_amount"] += \ - account.base_amount * item.get(based_on_field) / total_item_cost + item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][ + "base_amount" + ] += (account.base_amount * item.get(based_on_field) / total_item_cost) return item_account_wise_cost + def on_doctype_update(): frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index 18da88c3753..06ba9365561 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -1,38 +1,25 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'purchase_receipt_no', - 'non_standard_fieldnames': { - 'Purchase Invoice': 'purchase_receipt', - 'Asset': 'purchase_receipt', - 'Landed Cost Voucher': 'receipt_document', - 'Auto Repeat': 'reference_document', - 'Purchase Receipt': 'return_against' + "fieldname": "purchase_receipt_no", + "non_standard_fieldnames": { + "Purchase Invoice": "purchase_receipt", + "Asset": "purchase_receipt", + "Landed Cost Voucher": "receipt_document", + "Auto Repeat": "reference_document", + "Purchase Receipt": "return_against", }, - 'internal_links': { - 'Purchase Order': ['items', 'purchase_order'], - 'Project': ['items', 'project'], - 'Quality Inspection': ['items', 'quality_inspection'], + "internal_links": { + "Purchase Order": ["items", "purchase_order"], + "Project": ["items", "project"], + "Quality Inspection": ["items", "quality_inspection"], }, - 'transactions': [ - { - 'label': _('Related'), - 'items': ['Purchase Invoice', 'Landed Cost Voucher', 'Asset'] - }, - { - 'label': _('Reference'), - 'items': ['Purchase Order', 'Quality Inspection', 'Project'] - }, - { - 'label': _('Returns'), - 'items': ['Purchase Receipt'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + "transactions": [ + {"label": _("Related"), "items": ["Purchase Invoice", "Landed Cost Voucher", "Asset"]}, + {"label": _("Reference"), "items": ["Purchase Order", "Quality Inspection", "Project"]}, + {"label": _("Returns"), "items": ["Purchase Receipt"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c8a8fce7d63..0da89937ed0 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -27,15 +27,11 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_receipt_received_qty(self): """ - 1. Test if received qty is validated against accepted + rejected - 2. Test if received qty is auto set on save + 1. Test if received qty is validated against accepted + rejected + 2. Test if received qty is auto set on save """ pr = make_purchase_receipt( - qty=1, - rejected_qty=1, - received_qty=3, - item_code="_Test Item Home Desktop 200", - do_not_save=True + qty=1, rejected_qty=1, received_qty=3, item_code="_Test Item Home Desktop 200", do_not_save=True ) self.assertRaises(QtyMismatchError, pr.save) @@ -52,11 +48,8 @@ class TestPurchaseReceipt(FrappeTestCase): sl_entry = frappe.db.get_all( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name - }, - ['actual_qty'] + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + ["actual_qty"], ) self.assertEqual(len(sl_entry), 1) @@ -66,47 +59,44 @@ class TestPurchaseReceipt(FrappeTestCase): sl_entry_cancelled = frappe.db.get_all( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name - }, - ['actual_qty'], - order_by='creation' + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + ["actual_qty"], + order_by="creation", ) self.assertEqual(len(sl_entry_cancelled), 2) self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) def test_make_purchase_invoice(self): - if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'): - frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Purchase Invoice', - 'allocate_payment_based_on_payment_terms': 1, - 'terms': [ - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 00 - }, - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - }] - }).insert() + if not frappe.db.exists( + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" + ): + frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Purchase Invoice", + "allocate_payment_based_on_payment_terms": 1, + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 00, + }, + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + ], + } + ).insert() template = frappe.db.get_value( - "Payment Terms Template", - "_Test Payment Terms Template For Purchase Invoice" - ) - old_template_in_supplier = frappe.db.get_value( - "Supplier", - "_Test Supplier", - "payment_terms" + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ) + old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms") frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template) pr = make_purchase_receipt(do_not_save=True) @@ -124,23 +114,17 @@ class TestPurchaseReceipt(FrappeTestCase): # test if payment terms are fetched and set in PI self.assertEqual(pi.payment_terms_template, template) - self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total) / 2) self.assertEqual(pi.payment_schedule[0].invoice_portion, 50) - self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2) + self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total) / 2) self.assertEqual(pi.payment_schedule[1].invoice_portion, 50) # teardown - pi.delete() # draft PI + pi.delete() # draft PI pr.cancel() - frappe.db.set_value( - "Supplier", - "_Test Supplier", - "payment_terms", - old_template_in_supplier - ) + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier) frappe.get_doc( - "Payment Terms Template", - "_Test Payment Terms Template For Purchase Invoice" + "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ).delete() def test_purchase_receipt_no_gl_entry(self): @@ -148,27 +132,19 @@ class TestPurchaseReceipt(FrappeTestCase): existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - ["actual_qty", "stock_value"] + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "stock_value"], ) if existing_bin_qty < 0: make_stock_entry( - item_code="_Test Item", - target="_Test Warehouse - _TC", - qty=abs(existing_bin_qty) + item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty) ) existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - ["actual_qty", "stock_value"] + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, + ["actual_qty", "stock_value"], ) pr = make_purchase_receipt() @@ -179,20 +155,15 @@ class TestPurchaseReceipt(FrappeTestCase): "voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" + "warehouse": "_Test Warehouse - _TC", }, - "stock_value_difference" + "stock_value_difference", ) self.assertEqual(stock_value_difference, 250) current_bin_stock_value = frappe.db.get_value( - "Bin", - { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC" - }, - "stock_value" + "Bin", {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, "stock_value" ) self.assertEqual(current_bin_stock_value, existing_bin_stock_value + 250) @@ -201,7 +172,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() def test_batched_serial_no_purchase(self): - item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'}) + item = frappe.db.exists("Item", {"item_name": "Batched Serialized Item"}) if not item: item = create_item("Batched Serialized Item") item.has_batch_no = 1 @@ -211,34 +182,30 @@ class TestPurchaseReceipt(FrappeTestCase): item.serial_no_series = "BS-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Batched Serialized Item'}) + item = frappe.get_doc("Item", {"item_name": "Batched Serialized Item"}) pr = make_purchase_receipt(item_code=item.name, qty=5, rate=500) - self.assertTrue( - frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) - ) + self.assertTrue(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) pr.load_from_db() batch_no = pr.items[0].batch_no pr.cancel() - self.assertFalse( - frappe.db.get_value('Batch', {'item': item.name, 'reference_name': pr.name}) - ) - self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no})) + self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) + self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no})) def test_duplicate_serial_nos(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - item = frappe.db.exists("Item", {'item_name': 'Test Serialized Item 123'}) + item = frappe.db.exists("Item", {"item_name": "Test Serialized Item 123"}) if not item: item = create_item("Test Serialized Item 123") item.has_serial_no = 1 item.serial_no_series = "TSI123-.####" item.save() else: - item = frappe.get_doc("Item", {'item_name': 'Test Serialized Item 123'}) + item = frappe.get_doc("Item", {"item_name": "Test Serialized Item 123"}) # First make purchase receipt pr = make_purchase_receipt(item_code=item.name, qty=2, rate=500) @@ -246,12 +213,8 @@ class TestPurchaseReceipt(FrappeTestCase): serial_nos = frappe.db.get_value( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": pr.name, - "item_code": item.name - }, - "serial_no" + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item.name}, + "serial_no", ) serial_nos = get_serial_nos(serial_nos) @@ -263,21 +226,16 @@ class TestPurchaseReceipt(FrappeTestCase): item_code=item.name, qty=2, rate=500, - serial_no='\n'.join(serial_nos), - company='_Test Company 1', + serial_no="\n".join(serial_nos), + company="_Test Company 1", do_not_submit=True, - warehouse = 'Stores - _TC1' + warehouse="Stores - _TC1", ) self.assertRaises(SerialNoDuplicateError, pr_different_company.submit) # Then made delivery note to remove the serial nos from stock - dn = create_delivery_note( - item_code=item.name, - qty=2, - rate=1500, - serial_no='\n'.join(serial_nos) - ) + dn = create_delivery_note(item_code=item.name, qty=2, rate=1500, serial_no="\n".join(serial_nos)) dn.load_from_db() self.assertEquals(get_serial_nos(dn.items[0].serial_no), serial_nos) @@ -289,8 +247,8 @@ class TestPurchaseReceipt(FrappeTestCase): qty=2, rate=500, posting_date=posting_date, - serial_no='\n'.join(serial_nos), - do_not_submit=True + serial_no="\n".join(serial_nos), + do_not_submit=True, ) self.assertRaises(SerialNoExistsInFutureTransaction, pr1.submit) @@ -301,29 +259,28 @@ class TestPurchaseReceipt(FrappeTestCase): qty=2, rate=500, posting_date=posting_date, - serial_no='\n'.join(serial_nos), + serial_no="\n".join(serial_nos), company="_Test Company 1", do_not_submit=True, - warehouse="Stores - _TC1" + warehouse="Stores - _TC1", ) self.assertRaises(SerialNoExistsInFutureTransaction, pr2.submit) # Receive the same serial nos after the delivery note posting date and time - make_purchase_receipt( - item_code=item.name, - qty=2, - rate=500, - serial_no='\n'.join(serial_nos) - ) + make_purchase_receipt(item_code=item.name, qty=2, rate=500, serial_no="\n".join(serial_nos)) # Raise the error for backdated deliver note entry cancel self.assertRaises(SerialNoExistsInFutureTransaction, dn.cancel) def test_purchase_receipt_gl_entry(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", - get_multiple_items = True, get_taxes_and_charges = True) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", + get_multiple_items=True, + get_taxes_and_charges=True, + ) self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1) @@ -339,14 +296,14 @@ class TestPurchaseReceipt(FrappeTestCase): stock_in_hand_account: [750.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], "_Test Account Shipping Charges - TCP1": [0.0, 100.0], - "_Test Account Customs Duty - TCP1": [0.0, 150.0] + "_Test Account Customs Duty - TCP1": [0.0, 150.0], } else: expected_values = { stock_in_hand_account: [375.0, 0.0], fixed_asset_account: [375.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "_Test Account Shipping Charges - TCP1": [0.0, 250.0] + "_Test Account Shipping Charges - TCP1": [0.0, 250.0], } for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.debit) @@ -359,22 +316,19 @@ class TestPurchaseReceipt(FrappeTestCase): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry frappe.db.set_value( - "Buying Settings", None, - "backflush_raw_materials_of_subcontract_based_on", "BOM" + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" ) make_stock_entry( - item_code="_Test Item", qty=100, - target="_Test Warehouse 1 - _TC", basic_rate=100 + item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 ) make_stock_entry( - item_code="_Test Item Home Desktop 100", qty=100, - target="_Test Warehouse 1 - _TC", basic_rate=100 - ) - pr = make_purchase_receipt( - item_code="_Test FG Item", qty=10, - rate=500, is_subcontracted="Yes" + item_code="_Test Item Home Desktop 100", + qty=100, + target="_Test Warehouse 1 - _TC", + basic_rate=100, ) + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes") self.assertEqual(len(pr.get("supplied_items")), 2) rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) @@ -384,32 +338,35 @@ class TestPurchaseReceipt(FrappeTestCase): def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + frappe.db.set_value( - "Buying Settings", None, - "backflush_raw_materials_of_subcontract_based_on", "BOM" + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" ) se1 = make_stock_entry( item_code="_Test Item", target="Work In Progress - TCP1", - qty=100, basic_rate=100, - company="_Test Company with perpetual inventory" + qty=100, + basic_rate=100, + company="_Test Company with perpetual inventory", ) se2 = make_stock_entry( item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", - qty=100, basic_rate=100, - company="_Test Company with perpetual inventory" + qty=100, + basic_rate=100, + company="_Test Company with perpetual inventory", ) pr = make_purchase_receipt( item_code="_Test FG Item", - qty=10, rate=0, + qty=10, + rate=0, is_subcontracted="Yes", company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work In Progress - TCP1" + supplier_warehouse="Work In Progress - TCP1", ) gl_entries = get_gl_entries("Purchase Receipt", pr.name) @@ -422,9 +379,9 @@ class TestPurchaseReceipt(FrappeTestCase): def test_subcontracting_over_receipt(self): """ - Behaviour: Raise multiple PRs against one PO that in total - receive more than the required qty in the PO. - Expected Result: Error Raised for Over Receipt against PO. + Behaviour: Raise multiple PRs against one PO that in total + receive more than the required qty in the PO. + Expected Result: Error Raised for Over Receipt against PO. """ from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.doctype.purchase_order.purchase_order import ( @@ -443,26 +400,21 @@ class TestPurchaseReceipt(FrappeTestCase): po = create_purchase_order( item_code=item_code, - qty=1, include_exploded_items=0, + qty=1, + include_exploded_items=0, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC" + supplier_warehouse="_Test Warehouse 1 - _TC", ) # stock raw materials in a warehouse before transfer se1 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Test Extra Item 1", - qty=10, basic_rate=100 + target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100 ) se2 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="_Test FG Item", - qty=1, basic_rate=100 + target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100 ) se3 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Test Extra Item 2", - qty=1, basic_rate=100 + target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100 ) rm_items = [ @@ -472,7 +424,7 @@ class TestPurchaseReceipt(FrappeTestCase): "item_name": "_Test FG Item", "qty": po.supplied_items[0].required_qty, "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos" + "stock_uom": "Nos", }, { "item_code": item_code, @@ -480,8 +432,8 @@ class TestPurchaseReceipt(FrappeTestCase): "item_name": "Test Extra Item 1", "qty": po.supplied_items[1].required_qty, "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos" - } + "stock_uom": "Nos", + }, ] rm_item_string = json.dumps(rm_items) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) @@ -503,8 +455,9 @@ class TestPurchaseReceipt(FrappeTestCase): po.reload() pr2.load_from_db() - if pr2.docstatus == 1 and frappe.db.get_value('Stock Ledger Entry', - {'voucher_no': pr2.name, 'is_cancelled': 0}, 'name'): + if pr2.docstatus == 1 and frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "name" + ): pr2.cancel() po.load_from_db() @@ -514,15 +467,10 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr_row_1_serial_no = pr.get("items")[0].serial_no - self.assertEqual( - frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), - pr.supplier - ) + self.assertEqual(frappe.db.get_value("Serial No", pr_row_1_serial_no, "supplier"), pr.supplier) pr.cancel() - self.assertFalse( - frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse") - ) + self.assertFalse(frappe.db.get_value("Serial No", pr_row_1_serial_no, "warehouse")) def test_rejected_serial_no(self): pr = frappe.copy_doc(test_records[0]) @@ -537,32 +485,34 @@ class TestPurchaseReceipt(FrappeTestCase): accepted_serial_nos = pr.get("items")[0].serial_no.split("\n") self.assertEqual(len(accepted_serial_nos), 3) for serial_no in accepted_serial_nos: - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), - pr.get("items")[0].warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].warehouse + ) rejected_serial_nos = pr.get("items")[0].rejected_serial_no.split("\n") self.assertEqual(len(rejected_serial_nos), 2) for serial_no in rejected_serial_nos: - self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), - pr.get("items")[0].rejected_warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", serial_no, "warehouse"), pr.get("items")[0].rejected_warehouse + ) pr.cancel() def test_purchase_return_partial(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, - do_not_submit=1 + do_not_submit=1, ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() @@ -570,16 +520,12 @@ class TestPurchaseReceipt(FrappeTestCase): # check sle outgoing_rate = frappe.db.get_value( "Stock Ledger Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name - }, - "outgoing_rate" + {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, + "outgoing_rate", ) self.assertEqual(outgoing_rate, 50) - # check gl entries for return gl_entries = get_gl_entries("Purchase Receipt", return_pr.name) @@ -605,6 +551,7 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.per_returned, 40) from erpnext.controllers.sales_and_purchase_return import make_return_doc + return_pr_2 = make_return_doc("Purchase Receipt", pr.name) # Check if unreturned amount is mapped in 2nd return @@ -627,7 +574,7 @@ class TestPurchaseReceipt(FrappeTestCase): # PR should be completed on billing all unreturned amount self.assertEqual(pr.items[0].billed_amt, 150) self.assertEqual(pr.per_billed, 100) - self.assertEqual(pr.status, 'Completed') + self.assertEqual(pr.status, "Completed") pi.load_from_db() pi.cancel() @@ -641,18 +588,18 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_return_full(self): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, - do_not_submit=1 + do_not_submit=1, ) return_pr.items[0].purchase_receipt_item = pr.items[0].name return_pr.submit() @@ -665,7 +612,7 @@ class TestPurchaseReceipt(FrappeTestCase): # Check if Original PR updated self.assertEqual(pr.items[0].returned_qty, 5) self.assertEqual(pr.per_returned, 100) - self.assertEqual(pr.status, 'Return Issued') + self.assertEqual(pr.status, "Return Issued") return_pr.cancel() pr.cancel() @@ -673,32 +620,32 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_return_for_rejected_qty(self): from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse - rejected_warehouse="_Test Rejected Warehouse - TCP1" + rejected_warehouse = "_Test Rejected Warehouse - TCP1" if not frappe.db.exists("Warehouse", rejected_warehouse): get_warehouse( - company = "_Test Company with perpetual inventory", - abbr = " - TCP1", - warehouse_name = "_Test Rejected Warehouse" + company="_Test Company with perpetual inventory", + abbr=" - TCP1", + warehouse_name="_Test Rejected Warehouse", ).name pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", qty=2, rejected_qty=2, - rejected_warehouse=rejected_warehouse + rejected_warehouse=rejected_warehouse, ) return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1", + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, - rejected_qty = -2, - rejected_warehouse=rejected_warehouse + rejected_qty=-2, + rejected_warehouse=rejected_warehouse, ) actual_qty = frappe.db.get_value( @@ -706,9 +653,9 @@ class TestPurchaseReceipt(FrappeTestCase): { "voucher_type": "Purchase Receipt", "voucher_no": return_pr.name, - "warehouse": return_pr.items[0].rejected_warehouse + "warehouse": return_pr.items[0].rejected_warehouse, }, - "actual_qty" + "actual_qty", ) self.assertEqual(actual_qty, -2) @@ -716,7 +663,6 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.cancel() pr.cancel() - def test_purchase_return_for_serialized_items(self): def _check_serial_no_values(serial_no, field_values): serial_no = frappe.get_doc("Serial No", serial_no) @@ -729,24 +675,22 @@ class TestPurchaseReceipt(FrappeTestCase): serial_no = get_serial_nos(pr.get("items")[0].serial_no)[0] - _check_serial_no_values(serial_no, { - "warehouse": "_Test Warehouse - _TC", - "purchase_document_no": pr.name - }) + _check_serial_no_values( + serial_no, {"warehouse": "_Test Warehouse - _TC", "purchase_document_no": pr.name} + ) return_pr = make_purchase_receipt( item_code="_Test Serialized Item With Series", qty=-1, is_return=1, return_against=pr.name, - serial_no=serial_no + serial_no=serial_no, ) - _check_serial_no_values(serial_no, { - "warehouse": "", - "purchase_document_no": pr.name, - "delivery_document_no": return_pr.name - }) + _check_serial_no_values( + serial_no, + {"warehouse": "", "purchase_document_no": pr.name, "delivery_document_no": return_pr.name}, + ) return_pr.cancel() pr.reload() @@ -754,20 +698,12 @@ class TestPurchaseReceipt(FrappeTestCase): def test_purchase_return_for_multi_uom(self): item_code = "_Test Purchase Return For Multi-UOM" - if not frappe.db.exists('Item', item_code): - item = make_item(item_code, {'stock_uom': 'Box'}) - row = item.append('uoms', { - 'uom': 'Unit', - 'conversion_factor': 0.1 - }) + if not frappe.db.exists("Item", item_code): + item = make_item(item_code, {"stock_uom": "Box"}) + row = item.append("uoms", {"uom": "Unit", "conversion_factor": 0.1}) row.db_update() - pr = make_purchase_receipt( - item_code=item_code, - qty=1, - uom="Box", - conversion_factor=1.0 - ) + pr = make_purchase_receipt(item_code=item_code, qty=1, uom="Box", conversion_factor=1.0) return_pr = make_purchase_receipt( item_code=item_code, qty=-10, @@ -775,7 +711,7 @@ class TestPurchaseReceipt(FrappeTestCase): stock_uom="Box", conversion_factor=0.1, is_return=1, - return_against=pr.name + return_against=pr.name, ) self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0) @@ -791,18 +727,16 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt() update_purchase_receipt_status(pr.name, "Closed") - self.assertEqual( - frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed" - ) + self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed") pr.reload() pr.cancel() def test_pr_billing_status(self): """Flow: - 1. PO -> PR1 -> PI - 2. PO -> PI - 3. PO -> PR2. + 1. PO -> PR1 -> PI + 2. PO -> PI + 3. PO -> PR2. """ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_purchase_invoice_from_po, @@ -864,19 +798,15 @@ class TestPurchaseReceipt(FrappeTestCase): item = make_item(item_code, dict(has_serial_no=1)) serial_no = "12903812901" - pr_doc = make_purchase_receipt(item_code=item_code, - qty=1, serial_no = serial_no) + pr_doc = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) self.assertEqual( serial_no, frappe.db.get_value( "Serial No", - { - "purchase_document_type": "Purchase Receipt", - "purchase_document_no": pr_doc.name - }, - "name" - ) + {"purchase_document_type": "Purchase Receipt", "purchase_document_no": pr_doc.name}, + "name", + ), ) pr_doc.cancel() @@ -893,12 +823,9 @@ class TestPurchaseReceipt(FrappeTestCase): serial_no, frappe.db.get_value( "Serial No", - { - "purchase_document_type": "Purchase Receipt", - "purchase_document_no": new_pr_doc.name - }, - "name" - ) + {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, + "name", + ), ) new_pr_doc.cancel() @@ -906,40 +833,52 @@ class TestPurchaseReceipt(FrappeTestCase): def test_auto_asset_creation(self): asset_item = "Test Asset Item" - if not frappe.db.exists('Item', asset_item): - asset_category = frappe.get_all('Asset Category') + if not frappe.db.exists("Item", asset_item): + asset_category = frappe.get_all("Asset Category") if asset_category: asset_category = asset_category[0].name if not asset_category: - doc = frappe.get_doc({ - 'doctype': 'Asset Category', - 'asset_category_name': 'Test Asset Category', - 'depreciation_method': 'Straight Line', - 'total_number_of_depreciations': 12, - 'frequency_of_depreciation': 1, - 'accounts': [{ - 'company_name': '_Test Company', - 'fixed_asset_account': '_Test Fixed Asset - _TC', - 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC', - 'depreciation_expense_account': '_Test Depreciations - _TC' - }] - }).insert() + doc = frappe.get_doc( + { + "doctype": "Asset Category", + "asset_category_name": "Test Asset Category", + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "accounts": [ + { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC", + } + ], + } + ).insert() asset_category = doc.name - item_data = make_item(asset_item, {'is_stock_item':0, - 'stock_uom': 'Box', 'is_fixed_asset': 1, 'auto_create_assets': 1, - 'asset_category': asset_category, 'asset_naming_series': 'ABC.###'}) + item_data = make_item( + asset_item, + { + "is_stock_item": 0, + "stock_uom": "Box", + "is_fixed_asset": 1, + "auto_create_assets": 1, + "asset_category": asset_category, + "asset_naming_series": "ABC.###", + }, + ) asset_item = item_data.item_code pr = make_purchase_receipt(item_code=asset_item, qty=3) - assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) + assets = frappe.db.get_all("Asset", filters={"purchase_receipt": pr.name}) self.assertEqual(len(assets), 3) - location = frappe.db.get_value('Asset', assets[0].name, 'location') + location = frappe.db.get_value("Asset", assets[0].name, "location") self.assertEqual(location, "Test Location") pr.cancel() @@ -949,17 +888,18 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt(item_code="Test Asset Item", qty=1) - asset = frappe.get_doc("Asset", { - 'purchase_receipt': pr.name - }) + asset = frappe.get_doc("Asset", {"purchase_receipt": pr.name}) asset.available_for_use_date = frappe.utils.nowdate() asset.gross_purchase_amount = 50.0 - asset.append("finance_books", { - "expected_value_after_useful_life": 10, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 1 - }) + asset.append( + "finance_books", + { + "expected_value_after_useful_life": 10, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 1, + }, + ) asset.submit() pr_return = make_purchase_return(pr.name) @@ -979,36 +919,27 @@ class TestPurchaseReceipt(FrappeTestCase): cost_center = "_Test Cost Center for BS Account - TCP1" create_cost_center( cost_center_name="_Test Cost Center for BS Account", - company="_Test Company with perpetual inventory" + company="_Test Company with perpetual inventory", ) - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() pr = make_purchase_receipt( cost_center=cost_center, company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) - stock_in_hand_account = get_inventory_account( - pr.company, pr.get("items")[0].warehouse - ) + stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) expected_values = { - "Stock Received But Not Billed - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -1016,33 +947,24 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() def test_purchase_receipt_cost_center_with_balance_sheet_account(self): - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() pr = make_purchase_receipt( company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - supplier_warehouse = "Work in Progress - TCP1" + warehouse="Stores - TCP1", + supplier_warehouse="Work in Progress - TCP1", ) - stock_in_hand_account = get_inventory_account( - pr.company, pr.get("items")[0].warehouse - ) + stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) gl_entries = get_gl_entries("Purchase Receipt", pr.name) self.assertTrue(gl_entries) - cost_center = pr.get('items')[0].cost_center + cost_center = pr.get("items")[0].cost_center expected_values = { - "Stock Received But Not Billed - TCP1": { - "cost_center": cost_center - }, - stock_in_hand_account: { - "cost_center": cost_center - } + "Stock Received But Not Billed - TCP1": {"cost_center": cost_center}, + stock_in_hand_account: {"cost_center": cost_center}, } for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) @@ -1058,11 +980,7 @@ class TestPurchaseReceipt(FrappeTestCase): po = create_purchase_order() pr = create_pr_against_po(po.name) - pr1 = make_purchase_receipt( - qty=-1, - is_return=1, return_against=pr.name, - do_not_submit=True - ) + pr1 = make_purchase_receipt(qty=-1, is_return=1, return_against=pr.name, do_not_submit=True) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name pr1.items[0].purchase_receipt_item = pr.items[0].name @@ -1079,14 +997,17 @@ class TestPurchaseReceipt(FrappeTestCase): def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self): pr1 = make_purchase_receipt(qty=8, do_not_submit=True) - pr1.append("items", { - "item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "received_qty": 1, - "rate": 100, - "conversion_factor": 1.0, - }) + pr1.append( + "items", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "received_qty": 1, + "rate": 100, + "conversion_factor": 1.0, + }, + ) pr1.submit() pi1 = make_purchase_invoice(pr1.name) @@ -1095,11 +1016,7 @@ class TestPurchaseReceipt(FrappeTestCase): pi1.save() pi1.submit() - pr2 = make_purchase_receipt( - qty=-2, - is_return=1, return_against=pr1.name, - do_not_submit=True - ) + pr2 = make_purchase_receipt(qty=-2, is_return=1, return_against=pr1.name, do_not_submit=True) pr2.items[0].purchase_receipt_item = pr1.items[0].name pr2.submit() @@ -1113,26 +1030,25 @@ class TestPurchaseReceipt(FrappeTestCase): pr1.cancel() def test_stock_transfer_from_purchase_receipt(self): - pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', - company="_Test Company with perpetual inventory") + pr1 = make_purchase_receipt( + warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory" + ) - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", do_not_save=1) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 + ) - pr.supplier_warehouse = '' - pr.items[0].from_warehouse = 'Work In Progress - TCP1' + pr.supplier_warehouse = "" + pr.items[0].from_warehouse = "Work In Progress - TCP1" pr.submit() - gl_entries = get_gl_entries('Purchase Receipt', pr.name) - sl_entries = get_sl_entries('Purchase Receipt', pr.name) + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) self.assertFalse(gl_entries) - expected_sle = { - 'Work In Progress - TCP1': -5, - 'Stores - TCP1': 5 - } + expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5} for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) @@ -1144,48 +1060,45 @@ class TestPurchaseReceipt(FrappeTestCase): create_warehouse( "_Test Warehouse for Valuation", company="_Test Company with perpetual inventory", - properties={"account": '_Test Account Stock In Hand - TCP1'} + properties={"account": "_Test Account Stock In Hand - TCP1"}, ) pr1 = make_purchase_receipt( - warehouse = '_Test Warehouse for Valuation - TCP1', - company="_Test Company with perpetual inventory" + warehouse="_Test Warehouse for Valuation - TCP1", + company="_Test Company with perpetual inventory", ) pr = make_purchase_receipt( - company="_Test Company with perpetual inventory", - warehouse = "Stores - TCP1", - do_not_save=1 + company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 ) - pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1' - pr.supplier_warehouse = '' + pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" + pr.supplier_warehouse = "" - - pr.append('taxes', { - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Shipping Charges - TCP1', - 'category': 'Valuation and Total', - 'cost_center': 'Main - TCP1', - 'description': 'Test', - 'rate': 9 - }) + pr.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Shipping Charges - TCP1", + "category": "Valuation and Total", + "cost_center": "Main - TCP1", + "description": "Test", + "rate": 9, + }, + ) pr.submit() - gl_entries = get_gl_entries('Purchase Receipt', pr.name) - sl_entries = get_sl_entries('Purchase Receipt', pr.name) + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + sl_entries = get_sl_entries("Purchase Receipt", pr.name) expected_gle = [ - ['Stock In Hand - TCP1', 272.5, 0.0], - ['_Test Account Stock In Hand - TCP1', 0.0, 250.0], - ['_Test Account Shipping Charges - TCP1', 0.0, 22.5] + ["Stock In Hand - TCP1", 272.5, 0.0], + ["_Test Account Stock In Hand - TCP1", 0.0, 250.0], + ["_Test Account Shipping Charges - TCP1", 0.0, 22.5], ] - expected_sle = { - '_Test Warehouse for Valuation - TCP1': -5, - 'Stores - TCP1': 5 - } + expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5} for sle in sl_entries: self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) @@ -1198,7 +1111,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() pr1.cancel() - def test_subcontracted_pr_for_multi_transfer_batches(self): from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_receipt, @@ -1213,49 +1125,57 @@ class TestPurchaseReceipt(FrappeTestCase): update_backflush_based_on("Material Transferred for Subcontract") item_code = "_Test Subcontracted FG Item 3" - make_item('Sub Contracted Raw Material 3', { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'has_batch_no': 1, - 'create_new_batch': 1 - }) + make_item( + "Sub Contracted Raw Material 3", + {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1}, + ) - create_subcontracted_item(item_code=item_code, has_batch_no=1, - raw_materials=["Sub Contracted Raw Material 3"]) + create_subcontracted_item( + item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"] + ) order_qty = 500 - po = create_purchase_order(item_code=item_code, qty=order_qty, - is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + item_code=item_code, + qty=order_qty, + is_subcontracted="Yes", + supplier_warehouse="_Test Warehouse 1 - _TC", + ) - ste1=make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100) - ste2=make_stock_entry(target="_Test Warehouse - _TC", - item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100) + ste1 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=300, + basic_rate=100, + ) + ste2 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=200, + basic_rate=100, + ) - transferred_batch = { - ste1.items[0].batch_no : 300, - ste2.items[0].batch_no : 200 - } + transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200} rm_items = [ { - "item_code":item_code, - "rm_item_code":"Sub Contracted Raw Material 3", - "item_name":"_Test Item", - "qty":300, - "warehouse":"_Test Warehouse - _TC", - "stock_uom":"Nos", - "name": po.supplied_items[0].name + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 300, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": po.supplied_items[0].name, }, { - "item_code":item_code, - "rm_item_code":"Sub Contracted Raw Material 3", - "item_name":"_Test Item", - "qty":200, - "warehouse":"_Test Warehouse - _TC", - "stock_uom":"Nos", - "name": po.supplied_items[0].name - } + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 200, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": po.supplied_items[0].name, + }, ] rm_item_string = json.dumps(rm_items) @@ -1267,11 +1187,8 @@ class TestPurchaseReceipt(FrappeTestCase): supplied_qty = frappe.db.get_value( "Purchase Order Item Supplied", - { - "parent": po.name, - "rm_item_code": "Sub Contracted Raw Material 3" - }, - "supplied_qty" + {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, + "supplied_qty", ) self.assertEqual(supplied_qty, 500.00) @@ -1291,12 +1208,11 @@ class TestPurchaseReceipt(FrappeTestCase): ste1.cancel() po.cancel() - def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - - Create PO - - Create PI from PO and submit - - Create PR from PO and submit + - Create PO + - Create PI from PO and submit + - Create PR from PO and submit """ from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order @@ -1315,16 +1231,16 @@ class TestPurchaseReceipt(FrappeTestCase): def test_po_to_pi_and_po_to_pr_worflow_partial(self): """Test following behaviour: - - Create PO - - Create partial PI from PO and submit - - Create PR from PO and submit + - Create PO + - Create partial PI from PO and submit + - Create PR from PO and submit """ from erpnext.buying.doctype.purchase_order import purchase_order, test_purchase_order po = test_purchase_order.create_purchase_order() pi = purchase_order.make_purchase_invoice(po.name) - pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item. + pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item. pi.submit() pr = purchase_order.make_purchase_receipt(po.name) @@ -1356,10 +1272,9 @@ class TestPurchaseReceipt(FrappeTestCase): automatically_fetch_payment_terms() - po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() - po.payment_terms_template = 'Test Receivable Template' + po.payment_terms_template = "Test Receivable Template" po.submit() pr = make_pr_against_po(po.name, received_qty=10) @@ -1386,7 +1301,9 @@ class TestPurchaseReceipt(FrappeTestCase): account = "Stock Received But Not Billed - TCP1" make_item(item_code) - se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0) + se = make_stock_entry( + item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0 + ) se.items[0].allow_zero_valuation_rate = 1 se.save() se.submit() @@ -1407,93 +1324,112 @@ class TestPurchaseReceipt(FrappeTestCase): def get_sl_entries(voucher_type, voucher_no): - return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference + return frappe.db.sql( + """ select actual_qty, warehouse, stock_value_difference from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s - order by posting_time desc""", (voucher_type, voucher_no), as_dict=1) + order by posting_time desc""", + (voucher_type, voucher_no), + as_dict=1, + ) + def get_gl_entries(voucher_type, voucher_no): - return frappe.db.sql("""select account, debit, credit, cost_center, is_cancelled + return frappe.db.sql( + """select account, debit, credit, cost_center, is_cancelled from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account desc""", (voucher_type, voucher_no), as_dict=1) + order by account desc""", + (voucher_type, voucher_no), + as_dict=1, + ) + def get_taxes(**args): args = frappe._dict(args) - return [{'account_head': '_Test Account Shipping Charges - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Valuation and Total', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'Shipping Charges', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 100.0, - 'tax_amount': 100.0}, - {'account_head': '_Test Account VAT - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Total', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'VAT', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 120.0, - 'tax_amount': 120.0}, - {'account_head': '_Test Account Customs Duty - TCP1', - 'add_deduct_tax': 'Add', - 'category': 'Valuation', - 'charge_type': 'Actual', - 'cost_center': args.cost_center or 'Main - TCP1', - 'description': 'Customs Duty', - 'doctype': 'Purchase Taxes and Charges', - 'parentfield': 'taxes', - 'rate': 150.0, - 'tax_amount': 150.0}] + return [ + { + "account_head": "_Test Account Shipping Charges - TCP1", + "add_deduct_tax": "Add", + "category": "Valuation and Total", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "Shipping Charges", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 100.0, + "tax_amount": 100.0, + }, + { + "account_head": "_Test Account VAT - TCP1", + "add_deduct_tax": "Add", + "category": "Total", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "VAT", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 120.0, + "tax_amount": 120.0, + }, + { + "account_head": "_Test Account Customs Duty - TCP1", + "add_deduct_tax": "Add", + "category": "Valuation", + "charge_type": "Actual", + "cost_center": args.cost_center or "Main - TCP1", + "description": "Customs Duty", + "doctype": "Purchase Taxes and Charges", + "parentfield": "taxes", + "rate": 150.0, + "tax_amount": 150.0, + }, + ] + def get_items(**args): args = frappe._dict(args) - return [{ - "base_amount": 250.0, - "conversion_factor": 1.0, - "description": "_Test Item", - "doctype": "Purchase Receipt Item", - "item_code": "_Test Item", - "item_name": "_Test Item", - "parentfield": "items", - "qty": 5.0, - "rate": 50.0, - "received_qty": 5.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "cost_center": args.cost_center or "Main - _TC" - }, - { - "base_amount": 250.0, - "conversion_factor": 1.0, - "description": "_Test Item Home Desktop 100", - "doctype": "Purchase Receipt Item", - "item_code": "_Test Item Home Desktop 100", - "item_name": "_Test Item Home Desktop 100", - "parentfield": "items", - "qty": 5.0, - "rate": 50.0, - "received_qty": 5.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": args.warehouse or "_Test Warehouse 1 - _TC", - "cost_center": args.cost_center or "Main - _TC" - }] + return [ + { + "base_amount": 250.0, + "conversion_factor": 1.0, + "description": "_Test Item", + "doctype": "Purchase Receipt Item", + "item_code": "_Test Item", + "item_name": "_Test Item", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + { + "base_amount": 250.0, + "conversion_factor": 1.0, + "description": "_Test Item Home Desktop 100", + "doctype": "Purchase Receipt Item", + "item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse 1 - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + ] + def make_purchase_receipt(**args): - if not frappe.db.exists('Location', 'Test Location'): - frappe.get_doc({ - 'doctype': 'Location', - 'location_name': 'Test Location' - }).insert() + if not frappe.db.exists("Location", "Test Location"): + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) pr = frappe.new_doc("Purchase Receipt") @@ -1517,27 +1453,33 @@ def make_purchase_receipt(**args): item_code = args.item or args.item_code or "_Test Item" uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" - pr.append("items", { - "item_code": item_code, - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": qty, - "received_qty": received_qty, - "rejected_qty": rejected_qty, - "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate if args.rate != None else 50, - "conversion_factor": args.conversion_factor or 1.0, - "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), - "serial_no": args.serial_no, - "stock_uom": args.stock_uom or "_Test UOM", - "uom": uom, - "cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'), - "asset_location": args.location or "Test Location" - }) + pr.append( + "items", + { + "item_code": item_code, + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": qty, + "received_qty": received_qty, + "rejected_qty": rejected_qty, + "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" + if rejected_qty != 0 + else "", + "rate": args.rate if args.rate != None else 50, + "conversion_factor": args.conversion_factor or 1.0, + "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0), + "serial_no": args.serial_no, + "stock_uom": args.stock_uom or "_Test UOM", + "uom": uom, + "cost_center": args.cost_center + or frappe.get_cached_value("Company", pr.company, "cost_center"), + "asset_location": args.location or "Test Location", + }, + ) if args.get_multiple_items: pr.items = [] - company_cost_center = frappe.get_cached_value('Company', pr.company, 'cost_center') + company_cost_center = frappe.get_cached_value("Company", pr.company, "cost_center") cost_center = args.cost_center or company_cost_center for item in get_items(warehouse=args.warehouse, cost_center=cost_center): @@ -1553,33 +1495,44 @@ def make_purchase_receipt(**args): pr.submit() return pr + def create_subcontracted_item(**args): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom args = frappe._dict(args) - if not frappe.db.exists('Item', args.item_code): - make_item(args.item_code, { - 'is_stock_item': 1, - 'is_sub_contracted_item': 1, - 'has_batch_no': args.get("has_batch_no") or 0 - }) + if not frappe.db.exists("Item", args.item_code): + make_item( + args.item_code, + { + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": args.get("has_batch_no") or 0, + }, + ) if not args.raw_materials: - if not frappe.db.exists('Item', "Test Extra Item 1"): - make_item("Test Extra Item 1", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item( + "Test Extra Item 1", + { + "is_stock_item": 1, + }, + ) - if not frappe.db.exists('Item', "Test Extra Item 2"): - make_item("Test Extra Item 2", { - 'is_stock_item': 1, - }) + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item( + "Test Extra Item 2", + { + "is_stock_item": 1, + }, + ) - args.raw_materials = ['_Test FG Item', 'Test Extra Item 1'] + args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] + + if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): + make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'): - make_bom(item = args.item_code, raw_materials = args.get("raw_materials")) test_dependencies = ["BOM", "Item Price", "Location"] -test_records = frappe.get_test_records('Purchase Receipt') +test_records = frappe.get_test_records("Purchase Receipt") diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 4e472a92dc1..623fbde2b0b 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -24,11 +24,16 @@ class PutawayRule(Document): self.set_stock_capacity() def validate_duplicate_rule(self): - existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse}) + existing_rule = frappe.db.exists( + "Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse} + ) if existing_rule and existing_rule != self.name: - frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.") - .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)), - title=_("Duplicate")) + frappe.throw( + _("Putaway Rule already exists for Item {0} in Warehouse {1}.").format( + frappe.bold(self.item_code), frappe.bold(self.warehouse) + ), + title=_("Duplicate"), + ) def validate_priority(self): if self.priority < 1: @@ -37,18 +42,24 @@ class PutawayRule(Document): def validate_warehouse_and_company(self): company = frappe.db.get_value("Warehouse", self.warehouse, "company") if company != self.company: - frappe.throw(_("Warehouse {0} does not belong to Company {1}.") - .format(frappe.bold(self.warehouse), frappe.bold(self.company)), - title=_("Invalid Warehouse")) + frappe.throw( + _("Warehouse {0} does not belong to Company {1}.").format( + frappe.bold(self.warehouse), frappe.bold(self.company) + ), + title=_("Invalid Warehouse"), + ) def validate_capacity(self): stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom") balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate()) if flt(self.stock_capacity) < flt(balance_qty): - frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.") - .format(self.item_code, frappe.bold(balance_qty), stock_uom), - title=_("Insufficient Capacity")) + frappe.throw( + _( + "Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}." + ).format(self.item_code, frappe.bold(balance_qty), stock_uom), + title=_("Insufficient Capacity"), + ) if not self.capacity: frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid")) @@ -56,23 +67,26 @@ class PutawayRule(Document): def set_stock_capacity(self): self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity) + @frappe.whitelist() def get_available_putaway_capacity(rule): - stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule, - ["stock_capacity", "item_code", "warehouse"]) + stock_capacity, item_code, warehouse = frappe.db.get_value( + "Putaway Rule", rule, ["stock_capacity", "item_code", "warehouse"] + ) balance_qty = get_stock_balance(item_code, warehouse, nowdate()) free_space = flt(stock_capacity) - flt(balance_qty) return free_space if free_space > 0 else 0 + @frappe.whitelist() def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): - """ Applies Putaway Rule on line items. + """Applies Putaway Rule on line items. - items: List of Purchase Receipt/Stock Entry Items - company: Company in the Purchase Receipt/Stock Entry - doctype: Doctype to apply rule on - purpose: Purpose of Stock Entry - sync (optional): Sync with client side only for client side calls + items: List of Purchase Receipt/Stock Entry Items + company: Company in the Purchase Receipt/Stock Entry + doctype: Doctype to apply rule on + purpose: Purpose of Stock Entry + sync (optional): Sync with client side only for client side calls """ if isinstance(items, str): items = json.loads(items) @@ -89,16 +103,18 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): item.conversion_factor = flt(item.conversion_factor) or 1.0 pending_qty, item_code = flt(item.qty), item.item_code pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty) - uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number') + uom_must_be_whole_number = frappe.db.get_value("UOM", item.uom, "must_be_whole_number") if not pending_qty or not item_code: updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table) continue - at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse) + at_capacity, rules = get_ordered_putaway_rules( + item_code, company, source_warehouse=source_warehouse + ) if not rules: - warehouse = source_warehouse or item.get('warehouse') + warehouse = source_warehouse or item.get("warehouse") if at_capacity: # rules available, but no free space items_not_accomodated.append([item_code, pending_qty]) @@ -117,23 +133,28 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): for rule in item_wise_rules[key]: if pending_stock_qty > 0 and rule.free_space: - stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + stock_qty_to_allocate = ( + flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty + ) qty_to_allocate = stock_qty_to_allocate / item.conversion_factor if uom_must_be_whole_number: qty_to_allocate = floor(qty_to_allocate) stock_qty_to_allocate = qty_to_allocate * item.conversion_factor - if not qty_to_allocate: break + if not qty_to_allocate: + break - updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, - rule.name, serial_nos=serial_nos) + updated_table = add_row( + item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos + ) pending_stock_qty -= stock_qty_to_allocate pending_qty -= qty_to_allocate rule["free_space"] -= stock_qty_to_allocate - if not pending_stock_qty > 0: break + if not pending_stock_qty > 0: + break # if pending qty after applying all rules, add row without warehouse if pending_stock_qty > 0: @@ -146,13 +167,14 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None): items[:] = updated_table frappe.msgprint(_("Applied putaway rules."), alert=True) - if sync and json.loads(sync): # sync with client side + if sync and json.loads(sync): # sync with client side return items -def _items_changed(old, new, doctype: str) -> bool: - """ Check if any items changed by application of putaway rules. - If not, changing item table can have side effects since `name` items also changes. +def _items_changed(old, new, doctype: str) -> bool: + """Check if any items changed by application of putaway rules. + + If not, changing item table can have side effects since `name` items also changes. """ if len(old) != len(new): return True @@ -161,13 +183,22 @@ def _items_changed(old, new, doctype: str) -> bool: if doctype == "Stock Entry": compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no") - sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa - flt(item.transfer_qty), cstr(item.serial_no)) + sort_key = lambda item: ( # noqa + item.item_code, + cstr(item.t_warehouse), + flt(item.transfer_qty), + cstr(item.serial_no), + ) else: # purchase receipt / invoice compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no") - sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa - flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no)) + sort_key = lambda item: ( # noqa + item.item_code, + cstr(item.warehouse), + flt(item.stock_qty), + flt(item.received_qty), + cstr(item.serial_no), + ) old_sorted = sorted(old, key=sort_key) new_sorted = sorted(new, key=sort_key) @@ -182,18 +213,16 @@ def _items_changed(old, new, doctype: str) -> bool: def get_ordered_putaway_rules(item_code, company, source_warehouse=None): """Returns an ordered list of putaway rules to apply on an item.""" - filters = { - "item_code": item_code, - "company": company, - "disable": 0 - } + filters = {"item_code": item_code, "company": company, "disable": 0} if source_warehouse: filters.update({"warehouse": ["!=", source_warehouse]}) - rules = frappe.get_all("Putaway Rule", + rules = frappe.get_all( + "Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"], filters=filters, - order_by="priority asc, capacity desc") + order_by="priority asc, capacity desc", + ) if not rules: return False, None @@ -211,10 +240,11 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None): # then there is not enough space left in any rule return True, None - vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space'])) + vacant_rules = sorted(vacant_rules, key=lambda i: (i["priority"], -i["free_space"])) return False, vacant_rules + def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None): new_updated_table_row = copy.deepcopy(item) new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1 @@ -223,7 +253,9 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N if item.doctype == "Stock Entry Detail": new_updated_table_row.t_warehouse = warehouse - new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) + new_updated_table_row.transfer_qty = flt(to_allocate) * flt( + new_updated_table_row.conversion_factor + ) else: new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor) new_updated_table_row.warehouse = warehouse @@ -238,6 +270,7 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=N updated_table.append(new_updated_table_row) return updated_table + def show_unassigned_items_message(items_not_accomodated): msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "

    " formatted_item_rows = "" @@ -247,7 +280,9 @@ def show_unassigned_items_message(items_not_accomodated): formatted_item_rows += """ {0} {1} - """.format(item_link, frappe.bold(entry[1])) + """.format( + item_link, frappe.bold(entry[1]) + ) msg += """ @@ -257,13 +292,17 @@ def show_unassigned_items_message(items_not_accomodated): {2}
    - """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows) + """.format( + _("Item"), _("Unassigned Qty"), formatted_item_rows + ) frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True) + def get_serial_nos_to_allocate(serial_nos, to_allocate): if serial_nos: - allocated_serial_nos = serial_nos[0: cint(to_allocate)] - serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list + allocated_serial_nos = serial_nos[0 : cint(to_allocate)] + serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" - else: return "" + else: + return "" diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index 4e8d71fe5e4..82c32c378ce 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -15,12 +15,9 @@ from erpnext.stock.get_item_details import get_conversion_factor class TestPutawayRule(FrappeTestCase): def setUp(self): if not frappe.db.exists("Item", "_Rice"): - make_item("_Rice", { - 'is_stock_item': 1, - 'has_batch_no' : 1, - 'create_new_batch': 1, - 'stock_uom': 'Kg' - }) + make_item( + "_Rice", {"is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, "stock_uom": "Kg"} + ) if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}): create_warehouse("Rack 1") @@ -36,10 +33,10 @@ class TestPutawayRule(FrappeTestCase): new_uom.save() def assertUnchangedItemsOnResave(self, doc): - """ Check if same items remain even after reapplication of rules. + """Check if same items remain even after reapplication of rules. - This is required since some business logic like subcontracting - depends on `name` of items to be same if item isn't changed. + This is required since some business logic like subcontracting + depends on `name` of items to be same if item isn't changed. """ doc.reload() old_items = {d.name for d in doc.items} @@ -49,13 +46,14 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_priority(self): """Test if rule is applied by priority, irrespective of free space.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=300, uom="Kg", priority=2 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, self.warehouse_1) @@ -71,16 +69,19 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_same_priority(self): """Test if rule with more free space is applied, among two rules with same priority and capacity.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=500, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=500, uom="Kg" + ) # out of 500 kg capacity, occupy 100 kg in warehouse_1 - stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50) + stock_receipt = make_stock_entry( + item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 500) # warehouse_2 has 500 kg free space, it is given priority @@ -96,13 +97,14 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_insufficient_capacity(self): """Test if qty exceeding capacity, is handled.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=100, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=200, uom="Kg" + ) - pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 200) self.assertEqual(pr.items[0].warehouse, self.warehouse_2) @@ -118,24 +120,32 @@ class TestPutawayRule(FrappeTestCase): """Test rules applied on uom other than stock uom.""" item = frappe.get_doc("Item", "_Rice") if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): - item.append("uoms", { - "uom": "Bag", - "conversion_factor": 1000 - }) + item.append("uoms", {"uom": "Bag", "conversion_factor": 1000}) item.save() - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3, - uom="Bag") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=3, uom="Bag" + ) self.assertEqual(rule_1.stock_capacity, 3000) - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4, - uom="Bag") + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=4, uom="Bag" + ) self.assertEqual(rule_2.stock_capacity, 4000) # populate 'Rack 1' with 1 Bag, making the free space 2 Bags - stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50) + stock_receipt = make_stock_entry( + item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50 + ) - pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg", - conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + pr = make_purchase_receipt( + item_code="_Rice", + qty=6, + uom="Bag", + stock_uom="Kg", + conversion_factor=1000, + apply_putaway_rule=1, + do_not_submit=1, + ) self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 4) self.assertEqual(pr.items[0].warehouse, self.warehouse_2) @@ -151,25 +161,30 @@ class TestPutawayRule(FrappeTestCase): """Test if whole UOMs are handled.""" item = frappe.get_doc("Item", "_Rice") if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}): - item.append("uoms", { - "uom": "Bag", - "conversion_factor": 1000 - }) + item.append("uoms", {"uom": "Bag", "conversion_factor": 1000}) item.save() frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1) # Putaway Rule in different UOM - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1, - uom="Bag") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=1, uom="Bag" + ) self.assertEqual(rule_1.stock_capacity, 1000) # Putaway Rule in Stock UOM rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500) self.assertEqual(rule_2.stock_capacity, 500) # total capacity is 1500 Kg - pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg", - conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1) + pr = make_purchase_receipt( + item_code="_Rice", + qty=2, + uom="Bag", + stock_uom="Kg", + conversion_factor=1000, + apply_putaway_rule=1, + do_not_submit=1, + ) self.assertEqual(len(pr.items), 1) self.assertEqual(pr.items[0].qty, 1) self.assertEqual(pr.items[0].warehouse, self.warehouse_1) @@ -184,23 +199,26 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rules_with_reoccurring_item(self): """Test rules on same item entered multiple times with different rate.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # total capacity is 200 Kg - pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, - do_not_submit=1) - pr.append("items", { - "item_code": "_Rice", - "warehouse": "_Test Warehouse - _TC", - "qty": 200, - "uom": "Kg", - "stock_uom": "Kg", - "stock_qty": 200, - "received_qty": 200, - "rate": 100, - "conversion_factor": 1.0, - }) # same item entered again in PR but with different rate + pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1, do_not_submit=1) + pr.append( + "items", + { + "item_code": "_Rice", + "warehouse": "_Test Warehouse - _TC", + "qty": 200, + "uom": "Kg", + "stock_uom": "Kg", + "stock_qty": 200, + "received_qty": 200, + "rate": 100, + "conversion_factor": 1.0, + }, + ) # same item entered again in PR but with different rate pr.save() self.assertEqual(len(pr.items), 2) self.assertEqual(pr.items[0].qty, 100) @@ -208,7 +226,7 @@ class TestPutawayRule(FrappeTestCase): self.assertEqual(pr.items[0].putaway_rule, rule_1.name) # same rule applied to second item row # with previous assignment considered - self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 + self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200 self.assertEqual(pr.items[1].warehouse, self.warehouse_1) self.assertEqual(pr.items[1].putaway_rule, rule_1.name) @@ -219,13 +237,13 @@ class TestPutawayRule(FrappeTestCase): def test_validate_over_receipt_in_warehouse(self): """Test if overreceipt is blocked in the presence of putaway rules.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) - pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, - do_not_submit=1) + pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1, do_not_submit=1) self.assertEqual(len(pr.items), 1) - self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg + self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg self.assertEqual(pr.items[0].warehouse, self.warehouse_1) self.assertEqual(pr.items[0].putaway_rule, rule_1.name) @@ -240,21 +258,29 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_transfer(self): """Test if source warehouse is considered while applying rules.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") # higher priority - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # higher priority + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg", priority=2 + ) - stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200, - target="_Test Warehouse - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + source=self.warehouse_1, + qty=200, + target="_Test Warehouse - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_submit=1, + ) stock_entry_item = stock_entry.get("items")[0] # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided # even though it has more free space and higher priority self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2) - self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg + self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) self.assertUnchangedItemsOnResave(stock_entry) @@ -265,37 +291,48 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self): """Test if reoccuring item is correctly considered.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300, - uom="Kg") - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600, - uom="Kg", priority=2) + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=300, uom="Kg" + ) + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=600, uom="Kg", priority=2 + ) # create SE with first row having source warehouse as Rack 2 - stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200, - target="_Test Warehouse - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + source=self.warehouse_2, + qty=200, + target="_Test Warehouse - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_submit=1, + ) # Add rows with source warehouse as Rack 1 - stock_entry.extend("items", [ - { - "item_code": "_Rice", - "s_warehouse": self.warehouse_1, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 100, - "basic_rate": 50, - "conversion_factor": 1.0, - "transfer_qty": 100 - }, - { - "item_code": "_Rice", - "s_warehouse": self.warehouse_1, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 200, - "basic_rate": 60, - "conversion_factor": 1.0, - "transfer_qty": 200 - } - ]) + stock_entry.extend( + "items", + [ + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 100, + "basic_rate": 50, + "conversion_factor": 1.0, + "transfer_qty": 100, + }, + { + "item_code": "_Rice", + "s_warehouse": self.warehouse_1, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 200, + "basic_rate": 60, + "conversion_factor": 1.0, + "transfer_qty": 200, + }, + ], + ) stock_entry.save() @@ -323,19 +360,24 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self): """Test if batch and serial items are split correctly.""" if not frappe.db.exists("Item", "Water Bottle"): - make_item("Water Bottle", { - "is_stock_item": 1, - "has_batch_no" : 1, - "create_new_batch": 1, - "has_serial_no": 1, - "serial_no_series": "BOTTL-.####", - "stock_uom": "Nos" - }) + make_item( + "Water Bottle", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "BOTTL-.####", + "stock_uom": "Nos", + }, + ) - rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, - uom="Nos") - rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, - uom="Nos") + rule_1 = create_putaway_rule( + item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3, uom="Nos" + ) + rule_2 = create_putaway_rule( + item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2, uom="Nos" + ) make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle") @@ -344,12 +386,20 @@ class TestPutawayRule(FrappeTestCase): pr.save() pr.submit() - serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"}) + serial_nos = frappe.get_list( + "Serial No", filters={"purchase_document_no": pr.name, "status": "Active"} + ) serial_nos = [d.name for d in serial_nos] - stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5, - target="Finished Goods - _TC", purpose="Material Transfer", - apply_putaway_rule=1, do_not_save=1) + stock_entry = make_stock_entry( + item_code="Water Bottle", + source="_Test Warehouse - _TC", + qty=5, + target="Finished Goods - _TC", + purpose="Material Transfer", + apply_putaway_rule=1, + do_not_save=1, + ) stock_entry.items[0].batch_no = "BOTTL-BATCH-1" stock_entry.items[0].serial_no = "\n".join(serial_nos) stock_entry.save() @@ -375,14 +425,21 @@ class TestPutawayRule(FrappeTestCase): def test_putaway_rule_on_stock_entry_material_receipt(self): """Test if rules are applied in Stock Entry of type Receipt.""" - rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200, - uom="Kg") # more capacity - rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100, - uom="Kg") + rule_1 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_1, capacity=200, uom="Kg" + ) # more capacity + rule_2 = create_putaway_rule( + item_code="_Rice", warehouse=self.warehouse_2, capacity=100, uom="Kg" + ) - stock_entry = make_stock_entry(item_code="_Rice", qty=100, - target="_Test Warehouse - _TC", purpose="Material Receipt", - apply_putaway_rule=1, do_not_submit=1) + stock_entry = make_stock_entry( + item_code="_Rice", + qty=100, + target="_Test Warehouse - _TC", + purpose="Material Receipt", + apply_putaway_rule=1, + do_not_submit=1, + ) stock_entry_item = stock_entry.get("items")[0] @@ -396,6 +453,7 @@ class TestPutawayRule(FrappeTestCase): rule_1.delete() rule_2.delete() + def create_putaway_rule(**args): args = frappe._dict(args) putaway = frappe.new_doc("Putaway Rule") @@ -408,7 +466,9 @@ def create_putaway_rule(**args): putaway.capacity = args.capacity or 1 putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom") putaway.uom = args.uom or putaway.stock_uom - putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor'] + putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)[ + "conversion_factor" + ] if not args.do_not_save: putaway.save() diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 4e3b80aa761..331d3e812b2 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -18,8 +18,8 @@ class QualityInspection(Document): if not self.readings and self.item_code: self.get_item_specification_details() - if self.inspection_type=="In Process" and self.reference_type=="Job Card": - item_qi_template = frappe.db.get_value("Item", self.item_code, 'quality_inspection_template') + if self.inspection_type == "In Process" and self.reference_type == "Job Card": + item_qi_template = frappe.db.get_value("Item", self.item_code, "quality_inspection_template") parameters = get_template_details(item_qi_template) for reading in self.readings: for d in parameters: @@ -33,26 +33,28 @@ class QualityInspection(Document): @frappe.whitelist() def get_item_specification_details(self): if not self.quality_inspection_template: - self.quality_inspection_template = frappe.db.get_value('Item', - self.item_code, 'quality_inspection_template') + self.quality_inspection_template = frappe.db.get_value( + "Item", self.item_code, "quality_inspection_template" + ) - if not self.quality_inspection_template: return + if not self.quality_inspection_template: + return - self.set('readings', []) + self.set("readings", []) parameters = get_template_details(self.quality_inspection_template) for d in parameters: - child = self.append('readings', {}) + child = self.append("readings", {}) child.update(d) child.status = "Accepted" @frappe.whitelist() def get_quality_inspection_template(self): - template = '' + template = "" if self.bom_no: - template = frappe.db.get_value('BOM', self.bom_no, 'quality_inspection_template') + template = frappe.db.get_value("BOM", self.bom_no, "quality_inspection_template") if not template: - template = frappe.db.get_value('BOM', self.item_code, 'quality_inspection_template') + template = frappe.db.get_value("BOM", self.item_code, "quality_inspection_template") self.quality_inspection_template = template self.get_item_specification_details() @@ -66,21 +68,25 @@ class QualityInspection(Document): def update_qc_reference(self): quality_inspection = self.name if self.docstatus == 1 else "" - if self.reference_type == 'Job Card': + if self.reference_type == "Job Card": if self.reference_name: - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{doctype}` SET quality_inspection = %s, modified = %s WHERE name = %s and production_item = %s - """.format(doctype=self.reference_type), - (quality_inspection, self.modified, self.reference_name, self.item_code)) + """.format( + doctype=self.reference_type + ), + (quality_inspection, self.modified, self.reference_name, self.item_code), + ) else: args = [quality_inspection, self.modified, self.reference_name, self.item_code] - doctype = self.reference_type + ' Item' + doctype = self.reference_type + " Item" - if self.reference_type == 'Stock Entry': - doctype = 'Stock Entry Detail' + if self.reference_type == "Stock Entry": + doctype = "Stock Entry Detail" if self.reference_type and self.reference_name: conditions = "" @@ -88,11 +94,12 @@ class QualityInspection(Document): conditions += " and t1.batch_no = %s" args.append(self.batch_no) - if self.docstatus == 2: # if cancel, then remove qi link wherever same name + if self.docstatus == 2: # if cancel, then remove qi link wherever same name conditions += " and t1.quality_inspection = %s" args.append(self.name) - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2 SET @@ -102,12 +109,15 @@ class QualityInspection(Document): and t1.item_code = %s and t1.parent = t2.name {conditions} - """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions), - args) + """.format( + parent_doc=self.reference_type, child_doc=doctype, conditions=conditions + ), + args, + ) def inspect_and_set_status(self): for reading in self.readings: - if not reading.manual_inspection: # dont auto set status if manual + if not reading.manual_inspection: # dont auto set status if manual if reading.formula_based_criteria: self.set_status_based_on_acceptance_formula(reading) else: @@ -129,13 +139,16 @@ class QualityInspection(Document): reading_value = reading.get("reading_" + str(i)) if reading_value is not None and reading_value.strip(): result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value")) - if not result: return False + if not result: + return False return True def set_status_based_on_acceptance_formula(self, reading): if not reading.acceptance_formula: - frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), - title=_("Missing Formula")) + frappe.throw( + _("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx), + title=_("Missing Formula"), + ) condition = reading.acceptance_formula data = self.get_formula_evaluation_data(reading) @@ -145,12 +158,17 @@ class QualityInspection(Document): reading.status = "Accepted" if result else "Rejected" except NameError as e: field = frappe.bold(e.args[0].split()[1]) - frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.") - .format(reading.idx, field), - title=_("Invalid Formula")) + frappe.throw( + _("Row #{0}: {1} is not a valid reading field. Please refer to the field description.").format( + reading.idx, field + ), + title=_("Invalid Formula"), + ) except Exception: - frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), - title=_("Invalid Formula")) + frappe.throw( + _("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx), + title=_("Invalid Formula"), + ) def get_formula_evaluation_data(self, reading): data = {} @@ -168,6 +186,7 @@ class QualityInspection(Document): def calculate_mean(self, reading): """Calculate mean of all non-empty readings.""" from statistics import mean + readings_list = [] for i in range(1, 11): @@ -178,65 +197,90 @@ class QualityInspection(Document): actual_mean = mean(readings_list) if readings_list else 0 return actual_mean + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): if filters.get("from"): from frappe.desk.reportview import get_match_cond + mcond = get_match_cond(filters["from"]) cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" if filters.get("parent"): - if filters.get('from') in ['Purchase Invoice Item', 'Purchase Receipt Item']\ - and filters.get("inspection_type") != "In Process": + if ( + filters.get("from") in ["Purchase Invoice Item", "Purchase Receipt Item"] + and filters.get("inspection_type") != "In Process" + ): cond = """and item_code in (select name from `tabItem` where inspection_required_before_purchase = 1)""" - elif filters.get('from') in ['Sales Invoice Item', 'Delivery Note Item']\ - and filters.get("inspection_type") != "In Process": + elif ( + filters.get("from") in ["Sales Invoice Item", "Delivery Note Item"] + and filters.get("inspection_type") != "In Process" + ): cond = """and item_code in (select name from `tabItem` where inspection_required_before_delivery = 1)""" - elif filters.get('from') == 'Stock Entry Detail': + elif filters.get("from") == "Stock Entry Detail": cond = """and s_warehouse is null""" - if filters.get('from') in ['Supplier Quotation Item']: + if filters.get("from") in ["Supplier Quotation Item"]: qi_condition = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT item_code FROM `tab{doc}` WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s {qi_condition} {cond} {mcond} ORDER BY item_code limit {start}, {page_len} - """.format(doc=filters.get('from'), - cond = cond, mcond = mcond, start = start, - page_len = page_len, qi_condition = qi_condition), - {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) + """.format( + doc=filters.get("from"), + cond=cond, + mcond=mcond, + start=start, + page_len=page_len, + qi_condition=qi_condition, + ), + {"parent": filters.get("parent"), "txt": "%%%s%%" % txt}, + ) elif filters.get("reference_name"): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT production_item FROM `tab{doc}` WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s {qi_condition} {cond} {mcond} ORDER BY production_item LIMIT {start}, {page_len} - """.format(doc=filters.get("from"), - cond = cond, mcond = mcond, start = start, - page_len = page_len, qi_condition = qi_condition), - {'reference_name': filters.get('reference_name'), 'txt': "%%%s%%" % txt}) + """.format( + doc=filters.get("from"), + cond=cond, + mcond=mcond, + start=start, + page_len=page_len, + qi_condition=qi_condition, + ), + {"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt}, + ) + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.get_all('Quality Inspection', + return frappe.get_all( + "Quality Inspection", limit_start=start, limit_page_length=page_len, - filters = { - 'docstatus': 1, - 'name': ('like', '%%%s%%' % txt), - 'item_code': filters.get("item_code"), - 'reference_name': ('in', [filters.get("reference_name", ''), '']) - }, as_list=1) + filters={ + "docstatus": 1, + "name": ("like", "%%%s%%" % txt), + "item_code": filters.get("item_code"), + "reference_name": ("in", [filters.get("reference_name", ""), ""]), + }, + as_list=1, + ) + @frappe.whitelist() def make_quality_inspection(source_name, target_doc=None): @@ -244,19 +288,18 @@ def make_quality_inspection(source_name, target_doc=None): doc.inspected_by = frappe.session.user doc.get_quality_inspection_template() - doc = get_mapped_doc("BOM", source_name, { - 'BOM': { - "doctype": "Quality Inspection", - "validation": { - "docstatus": ["=", 1] - }, - "field_map": { - "name": "bom_no", - "item": "item_code", - "stock_uom": "uom", - "stock_qty": "qty" - }, - } - }, target_doc, postprocess) + doc = get_mapped_doc( + "BOM", + source_name, + { + "BOM": { + "doctype": "Quality Inspection", + "validation": {"docstatus": ["=", 1]}, + "field_map": {"name": "bom_no", "item": "item_code", "stock_uom": "uom", "stock_qty": "qty"}, + } + }, + target_doc, + postprocess, + ) return doc diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 601ca054b53..144f13880b1 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -22,16 +22,11 @@ class TestQualityInspection(FrappeTestCase): def setUp(self): super().setUp() create_item("_Test Item with QA") - frappe.db.set_value( - "Item", "_Test Item with QA", "inspection_required_before_delivery", 1 - ) + frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1) def test_qa_for_delivery(self): make_stock_entry( - item_code="_Test Item with QA", - target="_Test Warehouse - _TC", - qty=1, - basic_rate=100 + item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100 ) dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) @@ -71,21 +66,18 @@ class TestQualityInspection(FrappeTestCase): "specification": "Iron Content", # numeric reading "min_value": 0.1, "max_value": 0.9, - "reading_1": "0.4" + "reading_1": "0.4", }, { "specification": "Particle Inspection Needed", # non-numeric reading "numeric": 0, "value": "Yes", - "reading_value": "Yes" - } + "reading_value": "Yes", + }, ] qa = create_quality_inspection( - reference_type="Delivery Note", - reference_name=dn.name, - readings=readings, - do_not_save=True + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True ) qa.save() @@ -104,13 +96,13 @@ class TestQualityInspection(FrappeTestCase): "specification": "Iron Content", # numeric reading "formula_based_criteria": 1, "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50", - "reading_1": "0.4" + "reading_1": "0.4", }, { "specification": "Calcium Content", # numeric reading "formula_based_criteria": 1, "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50", - "reading_1": "0.7" + "reading_1": "0.7", }, { "specification": "Mg Content", # numeric reading @@ -118,22 +110,19 @@ class TestQualityInspection(FrappeTestCase): "acceptance_formula": "mean < 0.9", "reading_1": "0.5", "reading_2": "0.7", - "reading_3": "random text" # check if random string input causes issues + "reading_3": "random text", # check if random string input causes issues }, { "specification": "Calcium Content", # non-numeric reading "formula_based_criteria": 1, "numeric": 0, "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')", - "reading_value": "Grade B" - } + "reading_value": "Grade B", + }, ] qa = create_quality_inspection( - reference_type="Delivery Note", - reference_name=dn.name, - readings=readings, - do_not_save=True + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True ) qa.save() @@ -167,32 +156,26 @@ class TestQualityInspection(FrappeTestCase): qty=1, basic_rate=100, inspection_required=True, - do_not_submit=True + do_not_submit=True, ) readings = [ - { - "specification": "Iron Content", - "min_value": 0.1, - "max_value": 0.9, - "reading_1": "0.4" - } + {"specification": "Iron Content", "min_value": 0.1, "max_value": 0.9, "reading_1": "0.4"} ] qa = create_quality_inspection( - reference_type="Stock Entry", - reference_name=se.name, - readings=readings, - status="Rejected" + reference_type="Stock Entry", reference_name=se.name, readings=readings, status="Rejected" ) frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") se.reload() - self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI + self.assertRaises( + QualityInspectionRejectedError, se.submit + ) # when blocked in Stock settings, block rejected QI frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn") se.reload() - se.submit() # when allowed in Stock settings, allow rejected QI + se.submit() # when allowed in Stock settings, allow rejected QI # teardown qa.reload() @@ -201,6 +184,7 @@ class TestQualityInspection(FrappeTestCase): se.cancel() frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop") + def create_quality_inspection(**args): args = frappe._dict(args) qa = frappe.new_doc("Quality Inspection") @@ -238,8 +222,6 @@ def create_quality_inspection(**args): def create_quality_inspection_parameter(parameter): if not frappe.db.exists("Quality Inspection Parameter", parameter): - frappe.get_doc({ - "doctype": "Quality Inspection Parameter", - "parameter": parameter, - "description": parameter - }).insert() + frappe.get_doc( + {"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter} + ).insert() diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py index 7f8c871a93d..9b8f5d6378c 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py @@ -9,11 +9,22 @@ from frappe.model.document import Document class QualityInspectionTemplate(Document): pass -def get_template_details(template): - if not template: return [] - return frappe.get_all('Item Quality Inspection Parameter', - fields=["specification", "value", "acceptance_formula", - "numeric", "formula_based_criteria", "min_value", "max_value"], - filters={'parenttype': 'Quality Inspection Template', 'parent': template}, - order_by="idx") +def get_template_details(template): + if not template: + return [] + + return frappe.get_all( + "Item Quality Inspection Parameter", + fields=[ + "specification", + "value", + "acceptance_formula", + "numeric", + "formula_based_criteria", + "min_value", + "max_value", + ], + filters={"parenttype": "Quality Inspection Template", "parent": template}, + order_by="idx", + ) diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py index 7a0f5d08210..846be0b9bdc 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py @@ -12,24 +12,25 @@ from erpnext.stock.utils import get_stock_balance, get_stock_value_on class QuickStockBalance(Document): pass + @frappe.whitelist() def get_stock_item_details(warehouse, date, item=None, barcode=None): out = {} if barcode: out["item"] = frappe.db.get_value( - "Item Barcode", filters={"barcode": barcode}, fieldname=["parent"]) + "Item Barcode", filters={"barcode": barcode}, fieldname=["parent"] + ) if not out["item"]: - frappe.throw( - _("Invalid Barcode. There is no Item attached to this barcode.")) + frappe.throw(_("Invalid Barcode. There is no Item attached to this barcode.")) else: out["item"] = item - barcodes = frappe.db.get_values("Item Barcode", filters={"parent": out["item"]}, - fieldname=["barcode"]) + barcodes = frappe.db.get_values( + "Item Barcode", filters={"parent": out["item"]}, fieldname=["barcode"] + ) out["barcodes"] = [x[0] for x in barcodes] out["qty"] = get_stock_balance(out["item"], warehouse, date) out["value"] = get_stock_value_on(warehouse, date, out["item"]) - out["image"] = frappe.db.get_value("Item", - filters={"name": out["item"]}, fieldname=["image"]) + out["image"] = frappe.db.get_value("Item", filters={"name": out["item"]}, fieldname=["image"]) return out 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 0dd867f4156..b11becd4cee 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -23,7 +23,7 @@ class RepostItemValuation(Document): self.set_company() def reset_field_values(self): - if self.based_on == 'Transaction': + if self.based_on == "Transaction": self.item_code = None self.warehouse = None @@ -38,20 +38,20 @@ class RepostItemValuation(Document): def set_status(self, status=None, write=True): status = status or self.status if not status: - self.status = 'Queued' + self.status = "Queued" else: self.status = status if write: - self.db_set('status', self.status) + self.db_set("status", self.status) def on_submit(self): """During tests reposts are executed immediately. Exceptions: - 1. "Repost Item Valuation" document has self.flags.dont_run_in_test - 2. global flag frappe.flags.dont_execute_stock_reposts is set + 1. "Repost Item Valuation" document has self.flags.dont_run_in_test + 2. global flag frappe.flags.dont_execute_stock_reposts is set - These flags are useful for asserting real time behaviour like quantity updates. + These flags are useful for asserting real time behaviour like quantity updates. """ if not frappe.flags.in_test: @@ -63,14 +63,14 @@ class RepostItemValuation(Document): @frappe.whitelist() def restart_reposting(self): - self.set_status('Queued', write=False) + self.set_status("Queued", write=False) self.current_index = 0 self.distinct_item_and_warehouse = None self.items_to_be_repost = None self.db_update() def deduplicate_similar_repost(self): - """ Deduplicate similar reposts based on item-warehouse-posting combination.""" + """Deduplicate similar reposts based on item-warehouse-posting combination.""" if self.based_on != "Item and Warehouse": return @@ -82,7 +82,8 @@ class RepostItemValuation(Document): "posting_time": self.posting_time, } - frappe.db.sql(""" + frappe.db.sql( + """ update `tabRepost Item Valuation` set status = 'Skipped' WHERE item_code = %(item_code)s @@ -93,9 +94,10 @@ class RepostItemValuation(Document): and status = 'Queued' and based_on = 'Item and Warehouse' """, - filters + filters, ) + def on_doctype_update(): frappe.db.add_index("Repost Item Valuation", ["warehouse", "item_code"], "item_warehouse") @@ -105,14 +107,14 @@ def repost(doc): if not frappe.db.exists("Repost Item Valuation", doc.name): return - doc.set_status('In Progress') + doc.set_status("In Progress") if not frappe.flags.in_test: frappe.db.commit() repost_sl_entries(doc) repost_gl_entries(doc) - doc.set_status('Completed') + doc.set_status("Completed") except (Exception, JobTimeoutException): frappe.db.rollback() @@ -122,32 +124,47 @@ def repost(doc): message = frappe.message_log.pop() if traceback: message += "
    " + "Traceback:
    " + traceback - frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) + frappe.db.set_value(doc.doctype, doc.name, "error_log", message) notify_error_to_stock_managers(doc, message) - doc.set_status('Failed') + doc.set_status("Failed") raise finally: if not frappe.flags.in_test: frappe.db.commit() + def repost_sl_entries(doc): - if doc.based_on == 'Transaction': - repost_future_sle(doc=doc, voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, - allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) + if doc.based_on == "Transaction": + repost_future_sle( + doc=doc, + voucher_type=doc.voucher_type, + voucher_no=doc.voucher_no, + allow_negative_stock=doc.allow_negative_stock, + via_landed_cost_voucher=doc.via_landed_cost_voucher, + ) else: - repost_future_sle(args=[frappe._dict({ - "item_code": doc.item_code, - "warehouse": doc.warehouse, - "posting_date": doc.posting_date, - "posting_time": doc.posting_time - })], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) + repost_future_sle( + args=[ + frappe._dict( + { + "item_code": doc.item_code, + "warehouse": doc.warehouse, + "posting_date": doc.posting_date, + "posting_time": doc.posting_time, + } + ) + ], + allow_negative_stock=doc.allow_negative_stock, + via_landed_cost_voucher=doc.via_landed_cost_voucher, + ) + def repost_gl_entries(doc): if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)): return - if doc.based_on == 'Transaction': + if doc.based_on == "Transaction": ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() @@ -161,8 +178,14 @@ def repost_gl_entries(doc): items = [doc.item_code] warehouses = [doc.warehouse] - update_gl_entries_after(doc.posting_date, doc.posting_time, - for_warehouses=warehouses, for_items=items, company=doc.company) + update_gl_entries_after( + doc.posting_date, + doc.posting_time, + for_warehouses=warehouses, + for_items=items, + company=doc.company, + ) + def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") @@ -170,13 +193,20 @@ def notify_error_to_stock_managers(doc, traceback): get_users_with_role("System Manager") subject = _("Error while reposting item valuation") - message = (_("Hi,") + "
    " - + _("An error has been appeared while reposting item valuation via {0}") - .format(get_link_to_form(doc.doctype, doc.name)) + "
    " - + _("Please check the error message and take necessary actions to fix the error and then restart the reposting again.") + message = ( + _("Hi,") + + "
    " + + _("An error has been appeared while reposting item valuation via {0}").format( + get_link_to_form(doc.doctype, doc.name) + ) + + "
    " + + _( + "Please check the error message and take necessary actions to fix the error and then restart the reposting again." + ) ) frappe.sendmail(recipients=recipients, subject=subject, message=message) + def repost_entries(): if not in_configured_timeslot(): return @@ -184,8 +214,8 @@ def repost_entries(): riv_entries = get_repost_item_valuation_entries() for row in riv_entries: - doc = frappe.get_doc('Repost Item Valuation', row.name) - if doc.status in ('Queued', 'In Progress'): + doc = frappe.get_doc("Repost Item Valuation", row.name) + if doc.status in ("Queued", "In Progress"): repost(doc) doc.deduplicate_similar_repost() @@ -193,14 +223,19 @@ def repost_entries(): if riv_entries: return - for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + for d in frappe.get_all("Company", filters={"enable_perpetual_inventory": 1}): check_if_stock_and_account_balance_synced(today(), d.name) + def get_repost_item_valuation_entries(): - return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` + return frappe.db.sql( + """ SELECT name from `tabRepost Item Valuation` WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, now(), as_dict=1) + """, + now(), + as_dict=1, + ) def in_configured_timeslot(repost_settings=None, current_time=None): 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 78b432d564c..f3bebad5c09 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 @@ -153,7 +153,7 @@ class TestRepostItemValuation(unittest.TestCase): posting_date=today, posting_time="00:01:00", ) - riv.flags.dont_run_in_test = True # keep it queued + riv.flags.dont_run_in_test = True # keep it queued riv.submit() stock_settings = frappe.get_doc("Stock Settings") diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 22d84e9cb7d..bc30878789f 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -24,16 +24,45 @@ from erpnext.controllers.stock_controller import StockController from erpnext.stock.get_item_details import get_reserved_qty_for_so -class SerialNoCannotCreateDirectError(ValidationError): pass -class SerialNoCannotCannotChangeError(ValidationError): pass -class SerialNoNotRequiredError(ValidationError): pass -class SerialNoRequiredError(ValidationError): pass -class SerialNoQtyError(ValidationError): pass -class SerialNoItemError(ValidationError): pass -class SerialNoWarehouseError(ValidationError): pass -class SerialNoBatchError(ValidationError): pass -class SerialNoNotExistsError(ValidationError): pass -class SerialNoDuplicateError(ValidationError): pass +class SerialNoCannotCreateDirectError(ValidationError): + pass + + +class SerialNoCannotCannotChangeError(ValidationError): + pass + + +class SerialNoNotRequiredError(ValidationError): + pass + + +class SerialNoRequiredError(ValidationError): + pass + + +class SerialNoQtyError(ValidationError): + pass + + +class SerialNoItemError(ValidationError): + pass + + +class SerialNoWarehouseError(ValidationError): + pass + + +class SerialNoBatchError(ValidationError): + pass + + +class SerialNoNotExistsError(ValidationError): + pass + + +class SerialNoDuplicateError(ValidationError): + pass + class SerialNo(StockController): def __init__(self, *args, **kwargs): @@ -42,7 +71,12 @@ class SerialNo(StockController): def validate(self): if self.get("__islocal") and self.warehouse and not self.via_stock_ledger: - frappe.throw(_("New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt"), SerialNoCannotCreateDirectError) + frappe.throw( + _( + "New Serial No cannot have Warehouse. Warehouse must be set by Stock Entry or Purchase Receipt" + ), + SerialNoCannotCreateDirectError, + ) self.set_maintenance_status() self.validate_warehouse() @@ -77,22 +111,21 @@ class SerialNo(StockController): def validate_warehouse(self): if not self.get("__islocal"): - item_code, warehouse = frappe.db.get_value("Serial No", - self.name, ["item_code", "warehouse"]) + item_code, warehouse = frappe.db.get_value("Serial No", self.name, ["item_code", "warehouse"]) if not self.via_stock_ledger and item_code != self.item_code: - frappe.throw(_("Item Code cannot be changed for Serial No."), - SerialNoCannotCannotChangeError) + frappe.throw(_("Item Code cannot be changed for Serial No."), SerialNoCannotCannotChangeError) if not self.via_stock_ledger and warehouse != self.warehouse: - frappe.throw(_("Warehouse cannot be changed for Serial No."), - SerialNoCannotCannotChangeError) + frappe.throw(_("Warehouse cannot be changed for Serial No."), SerialNoCannotCannotChangeError) def validate_item(self): """ - Validate whether serial no is required for this item + Validate whether serial no is required for this item """ item = frappe.get_cached_doc("Item", self.item_code) - if item.has_serial_no!=1: - frappe.throw(_("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code)) + if item.has_serial_no != 1: + frappe.throw( + _("Item {0} is not setup for Serial Nos. Check Item master").format(self.item_code) + ) self.item_group = item.item_group self.description = item.description @@ -108,17 +141,24 @@ class SerialNo(StockController): self.purchase_time = purchase_sle.posting_time self.purchase_rate = purchase_sle.incoming_rate if purchase_sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): - self.supplier, self.supplier_name = \ - frappe.db.get_value(purchase_sle.voucher_type, purchase_sle.voucher_no, - ["supplier", "supplier_name"]) + self.supplier, self.supplier_name = frappe.db.get_value( + purchase_sle.voucher_type, purchase_sle.voucher_no, ["supplier", "supplier_name"] + ) # If sales return entry - if self.purchase_document_type == 'Delivery Note': + if self.purchase_document_type == "Delivery Note": self.sales_invoice = None else: - for fieldname in ("purchase_document_type", "purchase_document_no", - "purchase_date", "purchase_time", "purchase_rate", "supplier", "supplier_name"): - self.set(fieldname, None) + for fieldname in ( + "purchase_document_type", + "purchase_document_no", + "purchase_date", + "purchase_time", + "purchase_rate", + "supplier", + "supplier_name", + ): + self.set(fieldname, None) def set_sales_details(self, delivery_sle): if delivery_sle: @@ -126,18 +166,25 @@ class SerialNo(StockController): self.delivery_document_no = delivery_sle.voucher_no self.delivery_date = delivery_sle.posting_date self.delivery_time = delivery_sle.posting_time - if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"): - self.customer, self.customer_name = \ - frappe.db.get_value(delivery_sle.voucher_type, delivery_sle.voucher_no, - ["customer", "customer_name"]) + if delivery_sle.voucher_type in ("Delivery Note", "Sales Invoice"): + self.customer, self.customer_name = frappe.db.get_value( + delivery_sle.voucher_type, delivery_sle.voucher_no, ["customer", "customer_name"] + ) if self.warranty_period: - self.warranty_expiry_date = add_days(cstr(delivery_sle.posting_date), - cint(self.warranty_period)) + self.warranty_expiry_date = add_days( + cstr(delivery_sle.posting_date), cint(self.warranty_period) + ) else: - for fieldname in ("delivery_document_type", "delivery_document_no", - "delivery_date", "delivery_time", "customer", "customer_name", - "warranty_expiry_date"): - self.set(fieldname, None) + for fieldname in ( + "delivery_document_type", + "delivery_document_no", + "delivery_date", + "delivery_time", + "customer", + "customer_name", + "warranty_expiry_date", + ): + self.set(fieldname, None) def get_last_sle(self, serial_no=None): entries = {} @@ -159,7 +206,8 @@ class SerialNo(StockController): if not serial_no: serial_no = self.name - for sle in frappe.db.sql(""" + for sle in frappe.db.sql( + """ SELECT voucher_type, voucher_no, posting_date, posting_time, incoming_rate, actual_qty, serial_no FROM @@ -175,25 +223,30 @@ class SerialNo(StockController): ORDER BY posting_date desc, posting_time desc, creation desc""", ( - self.item_code, self.company, + self.item_code, + self.company, serial_no, - serial_no+'\n%', - '%\n'+serial_no, - '%\n'+serial_no+'\n%' + serial_no + "\n%", + "%\n" + serial_no, + "%\n" + serial_no + "\n%", ), - as_dict=1): - if serial_no.upper() in get_serial_nos(sle.serial_no): - if cint(sle.actual_qty) > 0: - sle_dict.setdefault("incoming", []).append(sle) - else: - sle_dict.setdefault("outgoing", []).append(sle) + as_dict=1, + ): + if serial_no.upper() in get_serial_nos(sle.serial_no): + if cint(sle.actual_qty) > 0: + sle_dict.setdefault("incoming", []).append(sle) + else: + sle_dict.setdefault("outgoing", []).append(sle) return sle_dict def on_trash(self): - sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry` + sl_entries = frappe.db.sql( + """select serial_no from `tabStock Ledger Entry` where serial_no like %s and item_code=%s and is_cancelled=0""", - ("%%%s%%" % self.name, self.item_code), as_dict=True) + ("%%%s%%" % self.name, self.item_code), + as_dict=True, + ) # Find the exact match sle_exists = False @@ -203,7 +256,9 @@ class SerialNo(StockController): break if sle_exists: - frappe.throw(_("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name)) + frappe.throw( + _("Cannot delete Serial No {0}, as it is used in stock transactions").format(self.name) + ) def before_rename(self, old, new, merge=False): if merge: @@ -211,16 +266,24 @@ class SerialNo(StockController): def after_rename(self, old, new, merge=False): """rename serial_no text fields""" - for dt in frappe.db.sql("""select parent from tabDocField - where fieldname='serial_no' and fieldtype in ('Text', 'Small Text', 'Long Text')"""): + for dt in frappe.db.sql( + """select parent from tabDocField + where fieldname='serial_no' and fieldtype in ('Text', 'Small Text', 'Long Text')""" + ): - for item in frappe.db.sql("""select name, serial_no from `tab%s` - where serial_no like %s""" % (dt[0], frappe.db.escape('%' + old + '%'))): + for item in frappe.db.sql( + """select name, serial_no from `tab%s` + where serial_no like %s""" + % (dt[0], frappe.db.escape("%" + old + "%")) + ): - serial_nos = map(lambda i: new if i.upper()==old.upper() else i, item[1].split('\n')) - frappe.db.sql("""update `tab%s` set serial_no = %s - where name=%s""" % (dt[0], '%s', '%s'), - ('\n'.join(list(serial_nos)), item[0])) + serial_nos = map(lambda i: new if i.upper() == old.upper() else i, item[1].split("\n")) + frappe.db.sql( + """update `tab%s` set serial_no = %s + where name=%s""" + % (dt[0], "%s", "%s"), + ("\n".join(list(serial_nos)), item[0]), + ) def update_serial_no_reference(self, serial_no=None): last_sle = self.get_last_sle(serial_no) @@ -229,57 +292,95 @@ class SerialNo(StockController): self.set_maintenance_status() self.set_status() + def process_serial_no(sle): item_det = get_item_details(sle.item_code) validate_serial_no(sle, item_det) update_serial_nos(sle, item_det) + def validate_serial_no(sle, item_det): serial_nos = get_serial_nos(sle.serial_no) if sle.serial_no else [] validate_material_transfer_entry(sle) - if item_det.has_serial_no==0: + if item_det.has_serial_no == 0: if serial_nos: - frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), - SerialNoNotRequiredError) + frappe.throw( + _("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), + SerialNoNotRequiredError, + ) elif not sle.is_cancelled: if serial_nos: if cint(sle.actual_qty) != flt(sle.actual_qty): - frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)) + frappe.throw( + _("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty) + ) if len(serial_nos) and len(serial_nos) != abs(cint(sle.actual_qty)): - frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.").format(abs(sle.actual_qty), sle.item_code, len(serial_nos)), - SerialNoQtyError) + frappe.throw( + _("{0} Serial Numbers required for Item {1}. You have provided {2}.").format( + abs(sle.actual_qty), sle.item_code, len(serial_nos) + ), + SerialNoQtyError, + ) if len(serial_nos) != len(set(serial_nos)): - frappe.throw(_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError) + frappe.throw( + _("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError + ) for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): - sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order", - "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type", - "purchase_document_no", "company", "status"], as_dict=1) + sr = frappe.db.get_value( + "Serial No", + serial_no, + [ + "name", + "item_code", + "batch_no", + "sales_order", + "delivery_document_no", + "delivery_document_type", + "warehouse", + "purchase_document_type", + "purchase_document_no", + "company", + "status", + ], + as_dict=1, + ) - if sr.item_code!=sle.item_code: + if sr.item_code != sle.item_code: if not allow_serial_nos_with_different_item(serial_no, sle): - frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, - sle.item_code), SerialNoItemError) + frappe.throw( + _("Serial No {0} does not belong to Item {1}").format(serial_no, sle.item_code), + SerialNoItemError, + ) if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle): doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)) - frappe.throw(_("Serial No {0} has already been received in the {1} #{2}") - .format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError) + frappe.throw( + _("Serial No {0} has already been received in the {1} #{2}").format( + frappe.bold(serial_no), sr.purchase_document_type, doc_name + ), + SerialNoDuplicateError, + ) - if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation'] - and sle.voucher_type == sr.delivery_document_type): - return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, 'return_against') + if ( + sr.delivery_document_no + and sle.voucher_type not in ["Stock Entry", "Stock Reconciliation"] + and sle.voucher_type == sr.delivery_document_type + ): + return_against = frappe.db.get_value(sle.voucher_type, sle.voucher_no, "return_against") if return_against and return_against != sr.delivery_document_no: frappe.throw(_("Serial no {0} has been already returned").format(sr.name)) if cint(sle.actual_qty) < 0: - if sr.warehouse!=sle.warehouse: - frappe.throw(_("Serial No {0} does not belong to Warehouse {1}").format(serial_no, - sle.warehouse), SerialNoWarehouseError) + if sr.warehouse != sle.warehouse: + frappe.throw( + _("Serial No {0} does not belong to Warehouse {1}").format(serial_no, sle.warehouse), + SerialNoWarehouseError, + ) if not sr.purchase_document_no: frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) @@ -287,66 +388,100 @@ def validate_serial_no(sle, item_det): if sle.voucher_type in ("Delivery Note", "Sales Invoice"): if sr.batch_no and sr.batch_no != sle.batch_no: - frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no, - sle.batch_no), SerialNoBatchError) + frappe.throw( + _("Serial No {0} does not belong to Batch {1}").format(serial_no, sle.batch_no), + SerialNoBatchError, + ) if not sle.is_cancelled and not sr.warehouse: - frappe.throw(_("Serial No {0} does not belong to any Warehouse") - .format(serial_no), SerialNoWarehouseError) + frappe.throw( + _("Serial No {0} does not belong to any Warehouse").format(serial_no), + SerialNoWarehouseError, + ) # if Sales Order reference in Serial No validate the Delivery Note or Invoice is against the same if sr.sales_order: if sle.voucher_type == "Sales Invoice": - if not frappe.db.exists("Sales Invoice Item", {"parent": sle.voucher_no, - "item_code": sle.item_code, "sales_order": sr.sales_order}): + if not frappe.db.exists( + "Sales Invoice Item", + {"parent": sle.voucher_no, "item_code": sle.item_code, "sales_order": sr.sales_order}, + ): frappe.throw( - _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") - .format(sr.name, sle.item_code, sr.sales_order) + _( + "Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}" + ).format(sr.name, sle.item_code, sr.sales_order) ) elif sle.voucher_type == "Delivery Note": - if not frappe.db.exists("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code, "against_sales_order": sr.sales_order}): - invoice = frappe.db.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_invoice") - if not invoice or frappe.db.exists("Sales Invoice Item", - {"parent": invoice, "item_code": sle.item_code, - "sales_order": sr.sales_order}): + if not frappe.db.exists( + "Delivery Note Item", + { + "parent": sle.voucher_no, + "item_code": sle.item_code, + "against_sales_order": sr.sales_order, + }, + ): + invoice = frappe.db.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_invoice", + ) + if not invoice or frappe.db.exists( + "Sales Invoice Item", + {"parent": invoice, "item_code": sle.item_code, "sales_order": sr.sales_order}, + ): frappe.throw( - _("Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}") - .format(sr.name, sle.item_code, sr.sales_order) + _( + "Cannot deliver Serial No {0} of item {1} as it is reserved to fullfill Sales Order {2}" + ).format(sr.name, sle.item_code, sr.sales_order) ) # if Sales Order reference in Delivery Note or Invoice validate SO reservations for item if sle.voucher_type == "Sales Invoice": - sales_order = frappe.db.get_value("Sales Invoice Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "sales_order") + sales_order = frappe.db.get_value( + "Sales Invoice Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) elif sle.voucher_type == "Delivery Note": - sales_order = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_order") + sales_order = frappe.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) else: - sales_invoice = frappe.get_value("Delivery Note Item", {"parent": sle.voucher_no, - "item_code": sle.item_code}, "against_sales_invoice") + sales_invoice = frappe.get_value( + "Delivery Note Item", + {"parent": sle.voucher_no, "item_code": sle.item_code}, + "against_sales_invoice", + ) if sales_invoice: - sales_order = frappe.db.get_value("Sales Invoice Item", { - "parent": sales_invoice, "item_code": sle.item_code}, "sales_order") + sales_order = frappe.db.get_value( + "Sales Invoice Item", + {"parent": sales_invoice, "item_code": sle.item_code}, + "sales_order", + ) if sales_order and get_reserved_qty_for_so(sales_order, sle.item_code): validate_so_serial_no(sr, sales_order) elif cint(sle.actual_qty) < 0: # transfer out frappe.throw(_("Serial No {0} not in stock").format(serial_no), SerialNoNotExistsError) elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series: - frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), - SerialNoRequiredError) + frappe.throw( + _("Serial Nos Required for Serialized Item {0}").format(sle.item_code), SerialNoRequiredError + ) elif serial_nos: # SLE is being cancelled and has serial nos for serial_no in serial_nos: check_serial_no_validity_on_cancel(serial_no, sle) + def check_serial_no_validity_on_cancel(serial_no, sle): - sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1) + sr = frappe.db.get_value( + "Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1 + ) sr_link = frappe.utils.get_link_to_form("Serial No", serial_no) doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no) actual_qty = cint(sle.actual_qty) @@ -356,57 +491,65 @@ def check_serial_no_validity_on_cancel(serial_no, sle): if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse): # receipt(inward) is being cancelled msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse) + ) elif sr and actual_qty > 0 and not is_stock_reco: # delivery is being cancelled, check for warehouse. if sr.warehouse: # serial no is active in another warehouse/company. msg = _("Cannot cancel {0} {1} as Serial No {2} is active in warehouse {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse) + ) elif sr.company != sle.company and sr.status == "Delivered": # serial no is inactive (allowed) or delivered from another company (block). msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the company {3}").format( - sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company)) + sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company) + ) if msg: frappe.throw(msg, title=_("Cannot cancel")) -def validate_material_transfer_entry(sle_doc): - sle_doc.update({ - "skip_update_serial_no": False, - "skip_serial_no_validaiton": False - }) - if (sle_doc.voucher_type == "Stock Entry" and not sle_doc.is_cancelled and - frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"): +def validate_material_transfer_entry(sle_doc): + sle_doc.update({"skip_update_serial_no": False, "skip_serial_no_validaiton": False}) + + if ( + sle_doc.voucher_type == "Stock Entry" + and not sle_doc.is_cancelled + and frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer" + ): if sle_doc.actual_qty < 0: sle_doc.skip_update_serial_no = True else: sle_doc.skip_serial_no_validaiton = True -def validate_so_serial_no(sr, sales_order): - if not sr.sales_order or sr.sales_order!= sales_order: - msg = (_("Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}.") - .format(sales_order, sr.item_code)) - frappe.throw(_("""{0} Serial No {1} cannot be delivered""") - .format(msg, sr.name)) +def validate_so_serial_no(sr, sales_order): + if not sr.sales_order or sr.sales_order != sales_order: + msg = _( + "Sales Order {0} has reservation for the item {1}, you can only deliver reserved {1} against {0}." + ).format(sales_order, sr.item_code) + + frappe.throw(_("""{0} Serial No {1} cannot be delivered""").format(msg, sr.name)) + def has_serial_no_exists(sn, sle): - if (sn.warehouse and not sle.skip_serial_no_validaiton - and sle.voucher_type != 'Stock Reconciliation'): + if ( + sn.warehouse and not sle.skip_serial_no_validaiton and sle.voucher_type != "Stock Reconciliation" + ): return True if sn.company != sle.company: return False + def allow_serial_nos_with_different_item(sle_serial_no, sle): """ - Allows same serial nos for raw materials and finished goods - in Manufacture / Repack type Stock Entry + Allows same serial nos for raw materials and finished goods + in Manufacture / Repack type Stock Entry """ allow_serial_nos = False - if sle.voucher_type=="Stock Entry" and cint(sle.actual_qty) > 0: + if sle.voucher_type == "Stock Entry" and cint(sle.actual_qty) > 0: stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no) if stock_entry.purpose in ("Repack", "Manufacture"): for d in stock_entry.get("items"): @@ -417,16 +560,24 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle): return allow_serial_nos + def update_serial_nos(sle, item_det): - if sle.skip_update_serial_no: return - if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \ - and item_det.has_serial_no == 1 and item_det.serial_no_series: + if sle.skip_update_serial_no: + return + if ( + not sle.is_cancelled + and not sle.serial_no + and cint(sle.actual_qty) > 0 + and item_det.has_serial_no == 1 + and item_det.serial_no_series + ): serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) sle.db_set("serial_no", serial_nos) validate_serial_no(sle, item_det) if sle.serial_no: auto_make_serial_nos(sle) + def get_auto_serial_nos(serial_no_series, qty): serial_nos = [] for i in range(cint(qty)): @@ -434,22 +585,24 @@ def get_auto_serial_nos(serial_no_series, qty): return "\n".join(serial_nos) + def get_new_serial_number(series): sr_no = make_autoname(series, "Serial No") if frappe.db.exists("Serial No", sr_no): sr_no = get_new_serial_number(series) return sr_no + def auto_make_serial_nos(args): - serial_nos = get_serial_nos(args.get('serial_no')) + serial_nos = get_serial_nos(args.get("serial_no")) created_numbers = [] - voucher_type = args.get('voucher_type') - item_code = args.get('item_code') + voucher_type = args.get("voucher_type") + item_code = args.get("item_code") for serial_no in serial_nos: is_new = False if frappe.db.exists("Serial No", serial_no): sr = frappe.get_cached_doc("Serial No", serial_no) - elif args.get('actual_qty', 0) > 0: + elif args.get("actual_qty", 0) > 0: sr = frappe.new_doc("Serial No") is_new = True @@ -457,7 +610,7 @@ def auto_make_serial_nos(args): if is_new: created_numbers.append(sr.name) - form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers)) + form_links = list(map(lambda d: get_link_to_form("Serial No", d), created_numbers)) # Setting up tranlated title field for all cases singular_title = _("Serial Number Created") @@ -469,29 +622,41 @@ def auto_make_serial_nos(args): if len(form_links) == 1: frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title) elif len(form_links) > 0: - message = _("The following serial numbers were created:

    {0}").format(get_items_html(form_links, item_code)) + message = _("The following serial numbers were created:

    {0}").format( + get_items_html(form_links, item_code) + ) frappe.msgprint(message, multiple_title) + def get_items_html(serial_nos, item_code): - body = ', '.join(serial_nos) - return '''
    + body = ", ".join(serial_nos) + return """
    {0}: {1} Serial Numbers
    {2}
    - '''.format(item_code, len(serial_nos), body) + """.format( + item_code, len(serial_nos), body + ) def get_item_details(item_code): - return frappe.db.sql("""select name, has_batch_no, docstatus, + return frappe.db.sql( + """select name, has_batch_no, docstatus, is_stock_item, has_serial_no, serial_no_series - from tabItem where name=%s""", item_code, as_dict=True)[0] + from tabItem where name=%s""", + item_code, + as_dict=True, + )[0] + def get_serial_nos(serial_no): if isinstance(serial_no, list): return serial_no - return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n') - if s.strip()] + return [ + s.strip() for s in cstr(serial_no).strip().upper().replace(",", "\n").split("\n") if s.strip() + ] + def clean_serial_no_string(serial_no: str) -> str: if not serial_no: @@ -500,20 +665,23 @@ def clean_serial_no_string(serial_no: str) -> str: serial_no_list = get_serial_nos(serial_no) return "\n".join(serial_no_list) + def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]: if args.get(field): serial_no_doc.set(field, args.get(field)) serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True - serial_no_doc.warehouse = (args.get("warehouse") - if args.get("actual_qty", 0) > 0 else None) + serial_no_doc.warehouse = args.get("warehouse") if args.get("actual_qty", 0) > 0 else None if is_new: serial_no_doc.serial_no = serial_no - if (serial_no_doc.sales_order and args.get("voucher_type") == "Stock Entry" - and not args.get("actual_qty", 0) > 0): + if ( + serial_no_doc.sales_order + and args.get("voucher_type") == "Stock Entry" + and not args.get("actual_qty", 0) > 0 + ): serial_no_doc.sales_order = None serial_no_doc.validate_item() @@ -526,19 +694,27 @@ def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False): return serial_no_doc -def update_serial_nos_after_submit(controller, parentfield): - stock_ledger_entries = frappe.db.sql("""select voucher_detail_no, serial_no, actual_qty, warehouse - from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", - (controller.doctype, controller.name), as_dict=True) - if not stock_ledger_entries: return +def update_serial_nos_after_submit(controller, parentfield): + stock_ledger_entries = frappe.db.sql( + """select voucher_detail_no, serial_no, actual_qty, warehouse + from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s""", + (controller.doctype, controller.name), + as_dict=True, + ) + + if not stock_ledger_entries: + return for d in controller.get(parentfield): if d.serial_no: continue - update_rejected_serial_nos = True if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") - and d.rejected_qty) else False + update_rejected_serial_nos = ( + True + if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty) + else False + ) accepted_serial_nos_updated = False if controller.doctype == "Stock Entry": @@ -549,58 +725,73 @@ def update_serial_nos_after_submit(controller, parentfield): qty = d.stock_qty else: warehouse = d.warehouse - qty = (d.qty if controller.doctype == "Stock Reconciliation" - else d.stock_qty) + qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty for sle in stock_ledger_entries: - if sle.voucher_detail_no==d.name: - if not accepted_serial_nos_updated and qty and abs(sle.actual_qty) == abs(qty) \ - and sle.warehouse == warehouse and sle.serial_no != d.serial_no: - d.serial_no = sle.serial_no - frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no) - accepted_serial_nos_updated = True - if not update_rejected_serial_nos: - break - elif update_rejected_serial_nos and abs(sle.actual_qty)==d.rejected_qty \ - and sle.warehouse == d.rejected_warehouse and sle.serial_no != d.rejected_serial_no: - d.rejected_serial_no = sle.serial_no - frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no) - update_rejected_serial_nos = False - if accepted_serial_nos_updated: - break + if sle.voucher_detail_no == d.name: + if ( + not accepted_serial_nos_updated + and qty + and abs(sle.actual_qty) == abs(qty) + and sle.warehouse == warehouse + and sle.serial_no != d.serial_no + ): + d.serial_no = sle.serial_no + frappe.db.set_value(d.doctype, d.name, "serial_no", sle.serial_no) + accepted_serial_nos_updated = True + if not update_rejected_serial_nos: + break + elif ( + update_rejected_serial_nos + and abs(sle.actual_qty) == d.rejected_qty + and sle.warehouse == d.rejected_warehouse + and sle.serial_no != d.rejected_serial_no + ): + d.rejected_serial_no = sle.serial_no + frappe.db.set_value(d.doctype, d.name, "rejected_serial_no", sle.serial_no) + update_rejected_serial_nos = False + if accepted_serial_nos_updated: + break + def update_maintenance_status(): - serial_nos = frappe.db.sql('''select name from `tabSerial No` where (amc_expiry_date<%s or - warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')''', - (nowdate(), nowdate())) + serial_nos = frappe.db.sql( + """select name from `tabSerial No` where (amc_expiry_date<%s or + warranty_expiry_date<%s) and maintenance_status not in ('Out of Warranty', 'Out of AMC')""", + (nowdate(), nowdate()), + ) for serial_no in serial_nos: doc = frappe.get_doc("Serial No", serial_no[0]) doc.set_maintenance_status() - frappe.db.set_value('Serial No', doc.name, 'maintenance_status', doc.maintenance_status) + frappe.db.set_value("Serial No", doc.name, "maintenance_status", doc.maintenance_status) + def get_delivery_note_serial_no(item_code, qty, delivery_note): - serial_nos = '' - dn_serial_nos = frappe.db.sql_list(""" select name from `tabSerial No` + serial_nos = "" + dn_serial_nos = frappe.db.sql_list( + """ select name from `tabSerial No` where item_code = %(item_code)s and delivery_document_no = %(delivery_note)s - and sales_invoice is null limit {0}""".format(cint(qty)), { - 'item_code': item_code, - 'delivery_note': delivery_note - }) + and sales_invoice is null limit {0}""".format( + cint(qty) + ), + {"item_code": item_code, "delivery_note": delivery_note}, + ) - if dn_serial_nos and len(dn_serial_nos)>0: - serial_nos = '\n'.join(dn_serial_nos) + if dn_serial_nos and len(dn_serial_nos) > 0: + serial_nos = "\n".join(dn_serial_nos) return serial_nos + @frappe.whitelist() def auto_fetch_serial_number( - qty: float, - item_code: str, - warehouse: str, - posting_date: Optional[str] = None, - batch_nos: Optional[Union[str, List[str]]] = None, - for_doctype: Optional[str] = None, - exclude_sr_nos: Optional[List[str]] = None - ) -> List[str]: + qty: float, + item_code: str, + warehouse: str, + posting_date: Optional[str] = None, + batch_nos: Optional[Union[str, List[str]]] = None, + for_doctype: Optional[str] = None, + exclude_sr_nos: Optional[List[str]] = None, +) -> List[str]: filters = frappe._dict({"item_code": item_code, "warehouse": warehouse}) @@ -621,19 +812,21 @@ def auto_fetch_serial_number( filters.expiry_date = posting_date serial_numbers = [] - if for_doctype == 'POS Invoice': + if for_doctype == "POS Invoice": exclude_sr_nos.extend(get_pos_reserved_serial_nos(filters)) serial_numbers = fetch_serial_numbers(filters, qty, do_not_include=exclude_sr_nos) - return sorted([d.get('name') for d in serial_numbers]) + return sorted([d.get("name") for d in serial_numbers]) + @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): filters = json.loads(filters) - pos_transacted_sr_nos = frappe.db.sql("""select item.serial_no as serial_no + pos_transacted_sr_nos = frappe.db.sql( + """select item.serial_no as serial_no from `tabPOS Invoice` p, `tabPOS Invoice Item` item where p.name = item.parent and p.consolidated_invoice is NULL @@ -642,7 +835,10 @@ def get_pos_reserved_serial_nos(filters): and item.item_code = %(item_code)s and item.warehouse = %(warehouse)s and item.serial_no is NOT NULL and item.serial_no != '' - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) reserved_sr_nos = [] for d in pos_transacted_sr_nos: @@ -650,6 +846,7 @@ def get_pos_reserved_serial_nos(filters): return reserved_sr_nos + def fetch_serial_numbers(filters, qty, do_not_include=None): if do_not_include is None: do_not_include = [] @@ -659,17 +856,16 @@ def fetch_serial_numbers(filters, qty, do_not_include=None): serial_no = frappe.qb.DocType("Serial No") query = ( - frappe.qb - .from_(serial_no) - .select(serial_no.name) - .where( - (serial_no.item_code == filters["item_code"]) - & (serial_no.warehouse == filters["warehouse"]) - & (Coalesce(serial_no.sales_invoice, "") == "") - & (Coalesce(serial_no.delivery_document_no, "") == "") - ) - .orderby(serial_no.creation) - .limit(qty or 1) + frappe.qb.from_(serial_no) + .select(serial_no.name) + .where( + (serial_no.item_code == filters["item_code"]) + & (serial_no.warehouse == filters["warehouse"]) + & (Coalesce(serial_no.sales_invoice, "") == "") + & (Coalesce(serial_no.delivery_document_no, "") == "") + ) + .orderby(serial_no.creation) + .limit(qty or 1) ) if do_not_include: @@ -680,8 +876,9 @@ def fetch_serial_numbers(filters, qty, do_not_include=None): if expiry_date: batch = frappe.qb.DocType("Batch") - query = (query - .left_join(batch).on(serial_no.batch_no == batch.name) + query = ( + query.left_join(batch) + .on(serial_no.batch_no == batch.name) .where(Coalesce(batch.expiry_date, "4000-12-31") >= expiry_date) ) diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 7df0a56b7f3..68623fba11e 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -18,12 +18,10 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_i from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse test_dependencies = ["Item"] -test_records = frappe.get_test_records('Serial No') - +test_records = frappe.get_test_records("Serial No") class TestSerialNo(FrappeTestCase): - def tearDown(self): frappe.db.rollback() @@ -48,7 +46,9 @@ class TestSerialNo(FrappeTestCase): se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) serial_no = frappe.get_doc("Serial No", serial_nos[0]) @@ -60,8 +60,13 @@ class TestSerialNo(FrappeTestCase): self.assertEqual(serial_no.delivery_document_no, dn.name) wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0], - company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) serial_no.reload() @@ -74,9 +79,9 @@ class TestSerialNo(FrappeTestCase): def test_inter_company_transfer_intermediate_cancellation(self): """ - Receive into and Deliver Serial No from one company. - Then Receive into and Deliver from second company. - Try to cancel intermediate receipts/deliveries to test if it is blocked. + Receive into and Deliver Serial No from one company. + Then Receive into and Deliver from second company. + Try to cancel intermediate receipts/deliveries to test if it is blocked. """ se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = get_serial_nos(se.get("items")[0].serial_no) @@ -89,8 +94,9 @@ class TestSerialNo(FrappeTestCase): self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.purchase_document_no, se.name) - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) sn_doc.reload() # check Serial No details after delivery from **first** company self.assertEqual(sn_doc.status, "Delivered") @@ -104,8 +110,13 @@ class TestSerialNo(FrappeTestCase): # receive serial no in second company wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() self.assertEqual(sn_doc.warehouse, wh) @@ -114,8 +125,13 @@ class TestSerialNo(FrappeTestCase): self.assertRaises(frappe.ValidationError, dn.cancel) # deliver from second company - dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + dn_2 = create_delivery_note( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() # check Serial No details after delivery from **second** company @@ -131,9 +147,9 @@ class TestSerialNo(FrappeTestCase): def test_inter_company_transfer_fallback_on_cancel(self): """ - Test Serial No state changes on cancellation. - If Delivery cancelled, it should fall back on last Receipt in the same company. - If Receipt is cancelled, it should be Inactive in the same company. + Test Serial No state changes on cancellation. + If Delivery cancelled, it should fall back on last Receipt in the same company. + If Receipt is cancelled, it should be Inactive in the same company. """ # Receipt in **first** company se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") @@ -141,17 +157,28 @@ class TestSerialNo(FrappeTestCase): sn_doc = frappe.get_doc("Serial No", serial_nos[0]) # Delivery from first company - dn = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0]) + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + ) # Receipt in **second** company wh = create_warehouse("_Test Warehouse", company="_Test Company 1") - pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + pr = make_purchase_receipt( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) # Delivery from second company - dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series", - qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh) + dn_2 = create_delivery_note( + item_code="_Test Serialized Item With Series", + qty=1, + serial_no=serial_nos[0], + company="_Test Company 1", + warehouse=wh, + ) sn_doc.reload() self.assertEqual(sn_doc.status, "Delivered") @@ -184,12 +211,11 @@ class TestSerialNo(FrappeTestCase): def test_auto_creation_of_serial_no(self): """ - Test if auto created Serial No excludes existing serial numbers + Test if auto created Serial No excludes existing serial numbers """ - item_code = make_item("_Test Auto Serial Item ", { - "has_serial_no": 1, - "serial_no_series": "XYZ.###" - }).item_code + item_code = make_item( + "_Test Auto Serial Item ", {"has_serial_no": 1, "serial_no_series": "XYZ.###"} + ).item_code # Reserve XYZ005 pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005") @@ -203,7 +229,7 @@ class TestSerialNo(FrappeTestCase): def test_serial_no_sanitation(self): "Test if Serial No input is sanitised before entering the DB." item_code = "_Test Serialized Item" - test_records = frappe.get_test_records('Stock Entry') + test_records = frappe.get_test_records("Stock Entry") se = frappe.copy_doc(test_records[0]) se.get("items")[0].item_code = item_code @@ -217,37 +243,43 @@ class TestSerialNo(FrappeTestCase): self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021") def test_correct_serial_no_incoming_rate(self): - """ Check correct consumption rate based on serial no record. - """ + """Check correct consumption rate based on serial no record.""" item_code = "_Test Serialized Item" warehouse = "_Test Warehouse - _TC" serial_nos = ["LOWVALUATION", "HIGHVALUATION"] - in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, - serial_no=serial_nos[0]) - in2 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, - serial_no=serial_nos[1]) + in1 = make_stock_entry( + item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0] + ) + in2 = make_stock_entry( + item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1] + ) - out = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True) + out = create_delivery_note( + item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True + ) # change serial no out.items[0].serial_no = serial_nos[1] out.save() out.submit() - value_diff = frappe.db.get_value("Stock Ledger Entry", - {"voucher_no": out.name, "voucher_type": "Delivery Note"}, - "stock_value_difference" - ) + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": out.name, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) self.assertEqual(value_diff, -113) def test_auto_fetch(self): - item_code = make_item(properties={ - "has_serial_no": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "serial_no_series": "TEST.#######" - }).name + item_code = make_item( + properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "serial_no_series": "TEST.#######", + } + ).name warehouse = "_Test Warehouse - _TC" in1 = make_stock_entry(item_code=item_code, to_warehouse=warehouse, qty=5) @@ -260,8 +292,8 @@ class TestSerialNo(FrappeTestCase): batch2 = in2.items[0].batch_no batch_wise_serials = { - batch1 : get_serial_nos(in1.items[0].serial_no), - batch2: get_serial_nos(in2.items[0].serial_no) + batch1: get_serial_nos(in1.items[0].serial_no), + batch2: get_serial_nos(in2.items[0].serial_no), } # Test FIFO @@ -270,12 +302,15 @@ class TestSerialNo(FrappeTestCase): # partial FIFO partial_fetch = auto_fetch_serial_number(2, item_code, warehouse) - self.assertTrue(set(partial_fetch).issubset(set(first_fetch)), - msg=f"{partial_fetch} should be subset of {first_fetch}") + self.assertTrue( + set(partial_fetch).issubset(set(first_fetch)), + msg=f"{partial_fetch} should be subset of {first_fetch}", + ) # exclusion - remaining = auto_fetch_serial_number(3, item_code, warehouse, - exclude_sr_nos=json.dumps(partial_fetch)) + remaining = auto_fetch_serial_number( + 3, item_code, warehouse, exclude_sr_nos=json.dumps(partial_fetch) + ) self.assertEqual(sorted(remaining + partial_fetch), first_fetch) # batchwise @@ -288,10 +323,14 @@ class TestSerialNo(FrappeTestCase): # multi batch all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list] - fetched_serials = auto_fetch_serial_number(10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys())) + fetched_serials = auto_fetch_serial_number( + 10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()) + ) self.assertEqual(sorted(all_serials), fetched_serials) # expiry date frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01") - non_expired_serials = auto_fetch_serial_number(5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1) + non_expired_serials = auto_fetch_serial_number( + 5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1 + ) self.assertEqual(non_expired_serials, []) diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py index 666de57f34f..42a67f42bec 100644 --- a/erpnext/stock/doctype/shipment/shipment.py +++ b/erpnext/stock/doctype/shipment/shipment.py @@ -17,22 +17,22 @@ class Shipment(Document): self.validate_pickup_time() self.set_value_of_goods() if self.docstatus == 0: - self.status = 'Draft' + self.status = "Draft" def on_submit(self): if not self.shipment_parcel: - frappe.throw(_('Please enter Shipment Parcel information')) + frappe.throw(_("Please enter Shipment Parcel information")) if self.value_of_goods == 0: - frappe.throw(_('Value of goods cannot be 0')) - self.db_set('status', 'Submitted') + frappe.throw(_("Value of goods cannot be 0")) + self.db_set("status", "Submitted") def on_cancel(self): - self.db_set('status', 'Cancelled') + self.db_set("status", "Cancelled") def validate_weight(self): for parcel in self.shipment_parcel: if flt(parcel.weight) <= 0: - frappe.throw(_('Parcel weight cannot be 0')) + frappe.throw(_("Parcel weight cannot be 0")) def validate_pickup_time(self): if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from): @@ -44,26 +44,34 @@ class Shipment(Document): value_of_goods += flt(entry.get("grand_total")) self.value_of_goods = value_of_goods if value_of_goods else self.value_of_goods + @frappe.whitelist() def get_address_name(ref_doctype, docname): # Return address name return get_party_shipping_address(ref_doctype, docname) + @frappe.whitelist() def get_contact_name(ref_doctype, docname): # Return address name return get_default_contact(ref_doctype, docname) + @frappe.whitelist() def get_company_contact(user): - contact = frappe.db.get_value('User', user, [ - 'first_name', - 'last_name', - 'email', - 'phone', - 'mobile_no', - 'gender', - ], as_dict=1) + contact = frappe.db.get_value( + "User", + user, + [ + "first_name", + "last_name", + "email", + "phone", + "mobile_no", + "gender", + ], + as_dict=1, + ) if not contact.phone: contact.phone = contact.mobile_no return contact diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index 317abb6d03e..ae97e7af361 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -13,13 +13,14 @@ class TestShipment(FrappeTestCase): def test_shipment_from_delivery_note(self): delivery_note = create_test_delivery_note() delivery_note.submit() - shipment = create_test_shipment([ delivery_note ]) + shipment = create_test_shipment([delivery_note]) shipment.submit() second_shipment = make_shipment(delivery_note.name) self.assertEqual(second_shipment.value_of_goods, delivery_note.grand_total) self.assertEqual(len(second_shipment.shipment_delivery_note), 1) self.assertEqual(second_shipment.shipment_delivery_note[0].delivery_note, delivery_note.name) + def create_test_delivery_note(): company = get_shipment_company() customer = get_shipment_customer() @@ -30,25 +31,26 @@ def create_test_delivery_note(): delivery_note = frappe.new_doc("Delivery Note") delivery_note.company = company.name delivery_note.posting_date = posting_date.strftime("%Y-%m-%d") - delivery_note.posting_time = '10:00' + delivery_note.posting_time = "10:00" delivery_note.customer = customer.name - delivery_note.append('items', + delivery_note.append( + "items", { "item_code": item.name, "item_name": item.item_name, - "description": 'Test delivery note for shipment', + "description": "Test delivery note for shipment", "qty": 5, - "uom": 'Nos', - "warehouse": 'Stores - _TC', + "uom": "Nos", + "warehouse": "Stores - _TC", "rate": item.standard_rate, - "cost_center": 'Main - _TC' - } + "cost_center": "Main - _TC", + }, ) delivery_note.insert() return delivery_note -def create_test_shipment(delivery_notes = None): +def create_test_shipment(delivery_notes=None): company = get_shipment_company() company_address = get_shipment_company_address(company.name) customer = get_shipment_customer() @@ -57,45 +59,35 @@ def create_test_shipment(delivery_notes = None): posting_date = date.today() + timedelta(days=5) shipment = frappe.new_doc("Shipment") - shipment.pickup_from_type = 'Company' + shipment.pickup_from_type = "Company" shipment.pickup_company = company.name shipment.pickup_address_name = company_address.name - shipment.delivery_to_type = 'Customer' + shipment.delivery_to_type = "Customer" shipment.delivery_customer = customer.name shipment.delivery_address_name = customer_address.name shipment.delivery_contact_name = customer_contact.name - shipment.pallets = 'No' - shipment.shipment_type = 'Goods' + shipment.pallets = "No" + shipment.shipment_type = "Goods" shipment.value_of_goods = 1000 - shipment.pickup_type = 'Pickup' + shipment.pickup_type = "Pickup" shipment.pickup_date = posting_date.strftime("%Y-%m-%d") - shipment.pickup_from = '09:00' - shipment.pickup_to = '17:00' - shipment.description_of_content = 'unit test entry' + shipment.pickup_from = "09:00" + shipment.pickup_to = "17:00" + shipment.description_of_content = "unit test entry" for delivery_note in delivery_notes: - shipment.append('shipment_delivery_note', - { - "delivery_note": delivery_note.name - } - ) - shipment.append('shipment_parcel', - { - "length": 5, - "width": 5, - "height": 5, - "weight": 5, - "count": 5 - } + shipment.append("shipment_delivery_note", {"delivery_note": delivery_note.name}) + shipment.append( + "shipment_parcel", {"length": 5, "width": 5, "height": 5, "weight": 5, "count": 5} ) shipment.insert() return shipment def get_shipment_customer_contact(customer_name): - contact_fname = 'Customer Shipment' - contact_lname = 'Testing' - customer_name = contact_fname + ' ' + contact_lname - contacts = frappe.get_all("Contact", fields=["name"], filters = {"name": customer_name}) + contact_fname = "Customer Shipment" + contact_lname = "Testing" + customer_name = contact_fname + " " + contact_lname + contacts = frappe.get_all("Contact", fields=["name"], filters={"name": customer_name}) if len(contacts): return contacts[0] else: @@ -103,104 +95,106 @@ def get_shipment_customer_contact(customer_name): def get_shipment_customer_address(customer_name): - address_title = customer_name + ' address 123' - customer_address = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + address_title = customer_name + " address 123" + customer_address = frappe.get_all( + "Address", fields=["name"], filters={"address_title": address_title} + ) if len(customer_address): return customer_address[0] else: return create_shipment_address(address_title, customer_name, 81929) + def get_shipment_customer(): - customer_name = 'Shipment Customer' - customer = frappe.get_all("Customer", fields=["name"], filters = {"name": customer_name}) + customer_name = "Shipment Customer" + customer = frappe.get_all("Customer", fields=["name"], filters={"name": customer_name}) if len(customer): return customer[0] else: return create_shipment_customer(customer_name) + def get_shipment_company_address(company_name): - address_title = company_name + ' address 123' - addresses = frappe.get_all("Address", fields=["name"], filters = {"address_title": address_title}) + address_title = company_name + " address 123" + addresses = frappe.get_all("Address", fields=["name"], filters={"address_title": address_title}) if len(addresses): return addresses[0] else: return create_shipment_address(address_title, company_name, 80331) + def get_shipment_company(): return frappe.get_doc("Company", "_Test Company") + def get_shipment_item(company_name): - item_name = 'Testing Shipment item' - items = frappe.get_all("Item", + item_name = "Testing Shipment item" + items = frappe.get_all( + "Item", fields=["name", "item_name", "item_code", "standard_rate"], - filters = {"item_name": item_name} + filters={"item_name": item_name}, ) if len(items): return items[0] else: return create_shipment_item(item_name, company_name) + def create_shipment_address(address_title, company_name, postal_code): address = frappe.new_doc("Address") address.address_title = address_title - address.address_type = 'Shipping' - address.address_line1 = company_name + ' address line 1' - address.city = 'Random City' + address.address_type = "Shipping" + address.address_line1 = company_name + " address line 1" + address.city = "Random City" address.postal_code = postal_code - address.country = 'Germany' + address.country = "Germany" address.insert() return address def create_customer_contact(fname, lname): customer = frappe.new_doc("Contact") - customer.customer_name = fname + ' ' + lname + customer.customer_name = fname + " " + lname customer.first_name = fname customer.last_name = lname customer.is_primary_contact = 1 customer.is_billing_contact = 1 - customer.append('email_ids', - { - 'email_id': 'randomme@email.com', - 'is_primary': 1 - } + customer.append("email_ids", {"email_id": "randomme@email.com", "is_primary": 1}) + customer.append( + "phone_nos", {"phone": "123123123", "is_primary_phone": 1, "is_primary_mobile_no": 1} ) - customer.append('phone_nos', - { - 'phone': '123123123', - 'is_primary_phone': 1, - 'is_primary_mobile_no': 1 - } - ) - customer.status = 'Passive' + customer.status = "Passive" customer.insert() return customer + def create_shipment_customer(customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name - customer.customer_type = 'Company' - customer.customer_group = 'All Customer Groups' - customer.territory = 'All Territories' - customer.gst_category = 'Unregistered' + customer.customer_type = "Company" + customer.customer_group = "All Customer Groups" + customer.territory = "All Territories" + customer.gst_category = "Unregistered" customer.insert() return customer + def create_material_receipt(item, company): posting_date = date.today() stock = frappe.new_doc("Stock Entry") stock.company = company - stock.stock_entry_type = 'Material Receipt' + stock.stock_entry_type = "Material Receipt" stock.posting_date = posting_date.strftime("%Y-%m-%d") - stock.append('items', + stock.append( + "items", { - "t_warehouse": 'Stores - _TC', + "t_warehouse": "Stores - _TC", "item_code": item.name, "qty": 5, - "uom": 'Nos', + "uom": "Nos", "basic_rate": item.standard_rate, - "cost_center": 'Main - _TC' - } + "cost_center": "Main - _TC", + }, ) stock.insert() stock.submit() @@ -210,14 +204,9 @@ def create_shipment_item(item_name, company_name): item = frappe.new_doc("Item") item.item_name = item_name item.item_code = item_name - item.item_group = 'All Item Groups' - item.stock_uom = 'Nos' + item.item_group = "All Item Groups" + item.stock_uom = "Nos" item.standard_rate = 50 - item.append('item_defaults', - { - "company": company_name, - "default_warehouse": 'Stores - _TC' - } - ) + item.append("item_defaults", {"company": company_name, "default_warehouse": "Stores - _TC"}) item.insert() return item diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f7109ab6b0d..60ce65eda11 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -39,20 +39,28 @@ from erpnext.stock.utils import get_bin, get_incoming_rate class FinishedGoodError(frappe.ValidationError): pass + + class IncorrectValuationRateError(frappe.ValidationError): pass + + class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass + + class OperationsNotCompleteError(frappe.ValidationError): pass + + class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass + from erpnext.controllers.stock_controller import StockController -form_grid_templates = { - "items": "templates/form_grid/stock_entry_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"} + class StockEntry(StockController): def get_feed(self): @@ -64,16 +72,18 @@ class StockEntry(StockController): def before_validate(self): from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule - apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"]) + + apply_rule = self.apply_putaway_rule and ( + self.purpose in ["Material Transfer", "Material Receipt"] + ) if self.get("items") and apply_rule: - apply_putaway_rule(self.doctype, self.get("items"), self.company, - purpose=self.purpose) + apply_putaway_rule(self.doctype, self.get("items"), self.company, purpose=self.purpose) def validate(self): self.pro_doc = frappe._dict() if self.work_order: - self.pro_doc = frappe.get_doc('Work Order', self.work_order) + self.pro_doc = frappe.get_doc("Work Order", self.work_order) self.validate_posting_time() self.validate_purpose() @@ -104,10 +114,10 @@ class StockEntry(StockController): if not self.from_bom: self.fg_completed_qty = 0.0 - if self._action == 'submit': - self.make_batches('t_warehouse') + if self._action == "submit": + self.make_batches("t_warehouse") else: - set_batch_nos(self, 's_warehouse') + set_batch_nos(self, "s_warehouse") self.validate_serialized_batch() self.set_actual_qty() @@ -139,10 +149,10 @@ class StockEntry(StockController): if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() - if self.purpose == 'Material Transfer' and self.add_to_transit: - self.set_material_request_transfer_status('In Transit') - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - self.set_material_request_transfer_status('Completed') + if self.purpose == "Material Transfer" and self.add_to_transit: + self.set_material_request_transfer_status("In Transit") + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + self.set_material_request_transfer_status("Completed") def on_cancel(self): self.update_purchase_order_supplied_items() @@ -153,7 +163,7 @@ class StockEntry(StockController): self.update_work_order() self.update_stock_ledger() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() @@ -163,15 +173,16 @@ class StockEntry(StockController): self.delete_auto_created_batches() self.delete_linked_stock_entry() - if self.purpose == 'Material Transfer' and self.add_to_transit: - self.set_material_request_transfer_status('Not Started') - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - self.set_material_request_transfer_status('In Transit') + if self.purpose == "Material Transfer" and self.add_to_transit: + self.set_material_request_transfer_status("Not Started") + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + self.set_material_request_transfer_status("In Transit") def set_job_card_data(self): if self.job_card and not self.work_order: - data = frappe.db.get_value('Job Card', - self.job_card, ['for_quantity', 'work_order', 'bom_no'], as_dict=1) + data = frappe.db.get_value( + "Job Card", self.job_card, ["for_quantity", "work_order", "bom_no"], as_dict=1 + ) self.fg_completed_qty = data.for_quantity self.work_order = data.work_order self.from_bom = 1 @@ -179,25 +190,37 @@ class StockEntry(StockController): def validate_work_order_status(self): pro_doc = frappe.get_doc("Work Order", self.work_order) - if pro_doc.status == 'Completed': + if pro_doc.status == "Completed": frappe.throw(_("Cannot cancel transaction for Completed Work Order.")) def validate_purpose(self): - valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", - "Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor", - "Material Consumption for Manufacture"] + valid_purposes = [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Material Consumption for Manufacture", + ] if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) - if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']: - frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") - .format(self.job_card)) + if self.job_card and self.purpose not in ["Material Transfer for Manufacture", "Repack"]: + frappe.throw( + _( + "For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry" + ).format(self.job_card) + ) def delete_linked_stock_entry(self): if self.purpose == "Send to Warehouse": - for d in frappe.get_all("Stock Entry", filters={"docstatus": 0, - "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}): + for d in frappe.get_all( + "Stock Entry", + filters={"docstatus": 0, "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}, + ): frappe.delete_doc("Stock Entry", d.name) def set_transfer_qty(self): @@ -206,80 +229,117 @@ class StockEntry(StockController): frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx)) if not flt(item.conversion_factor): frappe.throw(_("Row {0}: UOM Conversion Factor is mandatory").format(item.idx)) - item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), - self.precision("transfer_qty", item)) + item.transfer_qty = flt( + flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) + ) def update_cost_in_project(self): - if (self.work_order and not frappe.db.get_value("Work Order", - self.work_order, "update_consumed_material_cost_in_project")): + if self.work_order and not frappe.db.get_value( + "Work Order", self.work_order, "update_consumed_material_cost_in_project" + ): return if self.project: - amount = frappe.db.sql(""" select ifnull(sum(sed.amount), 0) + amount = frappe.db.sql( + """ select ifnull(sum(sed.amount), 0) from `tabStock Entry` se, `tabStock Entry Detail` sed where se.docstatus = 1 and se.project = %s and sed.parent = se.name - and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1) + and (sed.t_warehouse is null or sed.t_warehouse = '')""", + self.project, + as_list=1, + ) amount = amount[0][0] if amount else 0 - additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0) + additional_costs = frappe.db.sql( + """ select ifnull(sum(sed.base_amount), 0) from `tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed where se.docstatus = 1 and se.project = %s and sed.parent = se.name - and se.purpose = 'Manufacture'""", self.project, as_list=1) + and se.purpose = 'Manufacture'""", + self.project, + as_list=1, + ) additional_cost_amt = additional_costs[0][0] if additional_costs else 0 amount += additional_cost_amt - frappe.db.set_value('Project', self.project, 'total_consumed_material_cost', amount) + frappe.db.set_value("Project", self.project, "total_consumed_material_cost", amount) def validate_item(self): stock_items = self.get_stock_items() serialized_items = self.get_serialized_items() for item in self.get("items"): if flt(item.qty) and flt(item.qty) < 0: - frappe.throw(_("Row {0}: The item {1}, quantity must be positive number") - .format(item.idx, frappe.bold(item.item_code))) + frappe.throw( + _("Row {0}: The item {1}, quantity must be positive number").format( + item.idx, frappe.bold(item.item_code) + ) + ) if item.item_code not in stock_items: frappe.throw(_("{0} is not a stock Item").format(item.item_code)) - item_details = self.get_item_details(frappe._dict( - {"item_code": item.item_code, "company": self.company, - "project": self.project, "uom": item.uom, 's_warehouse': item.s_warehouse}), - for_update=True) + item_details = self.get_item_details( + frappe._dict( + { + "item_code": item.item_code, + "company": self.company, + "project": self.project, + "uom": item.uom, + "s_warehouse": item.s_warehouse, + } + ), + for_update=True, + ) - for f in ("uom", "stock_uom", "description", "item_name", "expense_account", - "cost_center", "conversion_factor"): - if f == "stock_uom" or not item.get(f): - item.set(f, item_details.get(f)) - if f == 'conversion_factor' and item.uom == item_details.get('stock_uom'): - item.set(f, item_details.get(f)) + for f in ( + "uom", + "stock_uom", + "description", + "item_name", + "expense_account", + "cost_center", + "conversion_factor", + ): + if f == "stock_uom" or not item.get(f): + item.set(f, item_details.get(f)) + if f == "conversion_factor" and item.uom == item_details.get("stock_uom"): + item.set(f, item_details.get(f)) if not item.transfer_qty and item.qty: - item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), - self.precision("transfer_qty", item)) + item.transfer_qty = flt( + flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) + ) - if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture") + if ( + self.purpose in ("Material Transfer", "Material Transfer for Manufacture") and not item.serial_no - and item.item_code in serialized_items): - frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), - frappe.MandatoryError) + and item.item_code in serialized_items + ): + frappe.throw( + _("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code), + frappe.MandatoryError, + ) def validate_qty(self): manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"] if self.purpose in manufacture_purpose and self.work_order: - if not frappe.get_value('Work Order', self.work_order, 'skip_transfer'): + if not frappe.get_value("Work Order", self.work_order, "skip_transfer"): item_code = [] for item in self.items: - if cstr(item.t_warehouse) == '': - req_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order, 'item_code': item.item_code}, fields=["item_code"]) + if cstr(item.t_warehouse) == "": + req_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order, "item_code": item.item_code}, + fields=["item_code"], + ) - transferred_materials = frappe.db.sql(""" + transferred_materials = frappe.db.sql( + """ select sum(qty) as qty from `tabStock Entry` se,`tabStock Entry Detail` sed @@ -287,7 +347,10 @@ class StockEntry(StockController): se.name = sed.parent and se.docstatus=1 and (se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture') and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' - """, (item.item_code, self.work_order), as_dict=1) + """, + (item.item_code, self.work_order), + as_dict=1, + ) stock_qty = flt(item.qty) trans_qty = flt(transferred_materials[0].qty) @@ -305,8 +368,11 @@ class StockEntry(StockController): for item_code, qty_list in iteritems(item_wise_qty): total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) if self.fg_completed_qty != total: - frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different") - .format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty))) + frappe.throw( + _("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( + frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty) + ) + ) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -314,33 +380,53 @@ class StockEntry(StockController): for d in self.get("items"): if not d.expense_account: - frappe.throw(_("Please enter Difference Account or set default Stock Adjustment Account for company {0}") - .format(frappe.bold(self.company))) + frappe.throw( + _( + "Please enter Difference Account or set default Stock Adjustment Account for company {0}" + ).format(frappe.bold(self.company)) + ) - elif self.is_opening == "Yes" and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss": - frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry"), OpeningEntryAccountError) + elif ( + self.is_opening == "Yes" + and frappe.db.get_value("Account", d.expense_account, "report_type") == "Profit and Loss" + ): + frappe.throw( + _( + "Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry" + ), + OpeningEntryAccountError, + ) def validate_warehouse(self): """perform various (sometimes conditional) validations on warehouse""" - source_mandatory = ["Material Issue", "Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture", - "Material Consumption for Manufacture"] + source_mandatory = [ + "Material Issue", + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ] - target_mandatory = ["Material Receipt", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture"] + target_mandatory = [ + "Material Receipt", + "Material Transfer", + "Send to Subcontractor", + "Material Transfer for Manufacture", + ] validate_for_manufacture = any([d.bom_no for d in self.get("items")]) if self.purpose in source_mandatory and self.purpose not in target_mandatory: self.to_warehouse = None - for d in self.get('items'): + for d in self.get("items"): d.t_warehouse = None elif self.purpose in target_mandatory and self.purpose not in source_mandatory: self.from_warehouse = None - for d in self.get('items'): + for d in self.get("items"): d.s_warehouse = None - for d in self.get('items'): + for d in self.get("items"): if not d.s_warehouse and not d.t_warehouse: d.s_warehouse = self.from_warehouse d.t_warehouse = self.to_warehouse @@ -357,7 +443,6 @@ class StockEntry(StockController): else: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) - if self.purpose == "Manufacture": if validate_for_manufacture: if d.is_finished_item or d.is_scrap_item or d.is_process_loss: @@ -369,18 +454,26 @@ class StockEntry(StockController): if not d.s_warehouse: frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx)) - if cstr(d.s_warehouse) == cstr(d.t_warehouse) and not self.purpose == "Material Transfer for Manufacture": + if ( + cstr(d.s_warehouse) == cstr(d.t_warehouse) + and not self.purpose == "Material Transfer for Manufacture" + ): frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx)) if not (d.s_warehouse or d.t_warehouse): frappe.throw(_("Atleast one warehouse is mandatory")) def validate_work_order(self): - if self.purpose in ("Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture"): + if self.purpose in ( + "Manufacture", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ): # check if work order is entered - if (self.purpose=="Manufacture" or self.purpose=="Material Consumption for Manufacture") \ - and self.work_order: + if ( + self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture" + ) and self.work_order: if not self.fg_completed_qty: frappe.throw(_("For Quantity (Manufactured Qty) is mandatory")) self.check_if_operations_completed() @@ -391,40 +484,66 @@ class StockEntry(StockController): def check_if_operations_completed(self): """Check if Time Sheets are completed against before manufacturing to capture operating costs.""" prod_order = frappe.get_doc("Work Order", self.work_order) - allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + allowance_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) for d in prod_order.get("operations"): total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) - completed_qty = d.completed_qty + (allowance_percentage/100 * d.completed_qty) + completed_qty = d.completed_qty + (allowance_percentage / 100 * d.completed_qty) if total_completed_qty > flt(completed_qty): - job_card = frappe.db.get_value('Job Card', {'operation_id': d.name}, 'name') + job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name") if not job_card: - frappe.throw(_("Work Order {0}: Job Card not found for the operation {1}") - .format(self.work_order, d.operation)) + frappe.throw( + _("Work Order {0}: Job Card not found for the operation {1}").format( + self.work_order, d.operation + ) + ) - work_order_link = frappe.utils.get_link_to_form('Work Order', self.work_order) - job_card_link = frappe.utils.get_link_to_form('Job Card', job_card) - frappe.throw(_("Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}.") - .format(d.idx, frappe.bold(d.operation), frappe.bold(total_completed_qty), work_order_link, job_card_link), OperationsNotCompleteError) + work_order_link = frappe.utils.get_link_to_form("Work Order", self.work_order) + job_card_link = frappe.utils.get_link_to_form("Job Card", job_card) + frappe.throw( + _( + "Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}." + ).format( + d.idx, + frappe.bold(d.operation), + frappe.bold(total_completed_qty), + work_order_link, + job_card_link, + ), + OperationsNotCompleteError, + ) def check_duplicate_entry_for_work_order(self): - other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", { - "work_order": self.work_order, - "purpose": self.purpose, - "docstatus": ["!=", 2], - "name": ["!=", self.name] - }, "name")] + other_ste = [ + t[0] + for t in frappe.db.get_values( + "Stock Entry", + { + "work_order": self.work_order, + "purpose": self.purpose, + "docstatus": ["!=", 2], + "name": ["!=", self.name], + }, + "name", + ) + ] if other_ste: - production_item, qty = frappe.db.get_value("Work Order", - self.work_order, ["production_item", "qty"]) + production_item, qty = frappe.db.get_value( + "Work Order", self.work_order, ["production_item", "qty"] + ) args = other_ste + [production_item] - fg_qty_already_entered = frappe.db.sql("""select sum(transfer_qty) + fg_qty_already_entered = frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where parent in (%s) and item_code = %s - and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0] + and ifnull(s_warehouse,'')='' """ + % (", ".join(["%s" * len(other_ste)]), "%s"), + args, + )[0][0] if fg_qty_already_entered and fg_qty_already_entered >= qty: frappe.throw( _("Stock Entries already created for Work Order {0}: {1}").format( @@ -436,33 +555,56 @@ class StockEntry(StockController): def set_actual_qty(self): allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) - for d in self.get('items'): - previous_sle = get_previous_sle({ - "item_code": d.item_code, - "warehouse": d.s_warehouse or d.t_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time - }) + for d in self.get("items"): + previous_sle = get_previous_sle( + { + "item_code": d.item_code, + "warehouse": d.s_warehouse or d.t_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) # get actual stock at source warehouse d.actual_qty = previous_sle.get("qty_after_transaction") or 0 # validate qty during submit - if d.docstatus==1 and d.s_warehouse and not allow_negative_stock and flt(d.actual_qty, d.precision("actual_qty")) < flt(d.transfer_qty, d.precision("actual_qty")): - frappe.throw(_("Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})").format(d.idx, - frappe.bold(d.s_warehouse), formatdate(self.posting_date), - format_time(self.posting_time), frappe.bold(d.item_code)) - + '

    ' + _("Available quantity is {0}, you need {1}").format(frappe.bold(d.actual_qty), - frappe.bold(d.transfer_qty)), - NegativeStockError, title=_('Insufficient Stock')) + if ( + d.docstatus == 1 + and d.s_warehouse + and not allow_negative_stock + and flt(d.actual_qty, d.precision("actual_qty")) + < flt(d.transfer_qty, d.precision("actual_qty")) + ): + frappe.throw( + _( + "Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3})" + ).format( + d.idx, + frappe.bold(d.s_warehouse), + formatdate(self.posting_date), + format_time(self.posting_time), + frappe.bold(d.item_code), + ) + + "

    " + + _("Available quantity is {0}, you need {1}").format( + frappe.bold(d.actual_qty), frappe.bold(d.transfer_qty) + ), + NegativeStockError, + title=_("Insufficient Stock"), + ) def set_serial_nos(self, work_order): - previous_se = frappe.db.get_value("Stock Entry", {"work_order": work_order, - "purpose": "Material Transfer for Manufacture"}, "name") + previous_se = frappe.db.get_value( + "Stock Entry", + {"work_order": work_order, "purpose": "Material Transfer for Manufacture"}, + "name", + ) - for d in self.get('items'): - transferred_serial_no = frappe.db.get_value("Stock Entry Detail",{"parent": previous_se, - "item_code": d.item_code}, "serial_no") + for d in self.get("items"): + transferred_serial_no = frappe.db.get_value( + "Stock Entry Detail", {"parent": previous_se, "item_code": d.item_code}, "serial_no" + ) if transferred_serial_no: d.serial_no = transferred_serial_no @@ -470,8 +612,8 @@ class StockEntry(StockController): @frappe.whitelist() def get_stock_and_rate(self): """ - Updates rate and availability of all the items. - Called from Update Rate and Availability button. + Updates rate and availability of all the items. + Called from Update Rate and Availability button. """ self.set_work_order_details() self.set_transfer_qty() @@ -488,38 +630,51 @@ class StockEntry(StockController): def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): """ - Set rate for outgoing, scrapped and finished items + Set rate for outgoing, scrapped and finished items """ # Set rate for outgoing items - outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) - finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss) + outgoing_items_cost = self.set_rate_for_outgoing_items( + reset_outgoing_rate, raise_error_if_no_rate + ) + finished_item_qty = sum( + d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss + ) # Set basic rate for incoming items - for d in self.get('items'): - if d.s_warehouse or d.set_basic_rate_manually: continue + for d in self.get("items"): + if d.s_warehouse or d.set_basic_rate_manually: + continue if d.allow_zero_valuation_rate: d.basic_rate = 0.0 elif d.is_finished_item: if self.purpose == "Manufacture": - d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost) + d.basic_rate = self.get_basic_rate_for_manufactured_item( + finished_item_qty, outgoing_items_cost + ) elif self.purpose == "Repack": d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) if not d.basic_rate and not d.allow_zero_valuation_rate: - d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, - self.doctype, self.name, d.allow_zero_valuation_rate, - currency=erpnext.get_company_currency(self.company), company=self.company, - raise_error_if_no_rate=raise_error_if_no_rate) + d.basic_rate = get_valuation_rate( + d.item_code, + d.t_warehouse, + self.doctype, + self.name, + d.allow_zero_valuation_rate, + currency=erpnext.get_company_currency(self.company), + company=self.company, + raise_error_if_no_rate=raise_error_if_no_rate, + ) d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) if d.is_process_loss: - d.basic_rate = flt(0.) + d.basic_rate = flt(0.0) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): outgoing_items_cost = 0.0 - for d in self.get('items'): + for d in self.get("items"): if d.s_warehouse: if reset_outgoing_rate: args = self.get_args_for_incoming_rate(d) @@ -534,18 +689,20 @@ class StockEntry(StockController): return outgoing_items_cost def get_args_for_incoming_rate(self, item): - return frappe._dict({ - "item_code": item.item_code, - "warehouse": item.s_warehouse or item.t_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty), - "serial_no": item.serial_no, - "voucher_type": self.doctype, - "voucher_no": self.name, - "company": self.company, - "allow_zero_valuation": item.allow_zero_valuation_rate, - }) + return frappe._dict( + { + "item_code": item.item_code, + "warehouse": item.s_warehouse or item.t_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": item.s_warehouse and -1 * flt(item.transfer_qty) or flt(item.transfer_qty), + "serial_no": item.serial_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": item.allow_zero_valuation_rate, + } + ) def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost): finished_items = [d.item_code for d in self.get("items") if d.is_finished_item] @@ -561,9 +718,11 @@ class StockEntry(StockController): scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) # Get raw materials cost from BOM if multiple material consumption entries - if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): + if not outgoing_items_cost and frappe.db.get_single_value( + "Manufacturing Settings", "material_consumption", cache=True + ): bom_items = self.get_bom_raw_materials(finished_item_qty) - outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) + outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()]) return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty) @@ -595,8 +754,10 @@ class StockEntry(StockController): for d in self.get("items"): if d.transfer_qty: d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount")) - d.valuation_rate = flt(flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), - d.precision("valuation_rate")) + d.valuation_rate = flt( + flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), + d.precision("valuation_rate"), + ) def set_total_incoming_outgoing_value(self): self.total_incoming_value = self.total_outgoing_value = 0.0 @@ -610,92 +771,120 @@ class StockEntry(StockController): def set_total_amount(self): self.total_amount = None - if self.purpose not in ['Manufacture', 'Repack']: + if self.purpose not in ["Manufacture", "Repack"]: self.total_amount = sum([flt(item.amount) for item in self.get("items")]) def set_stock_entry_type(self): if self.purpose: - self.stock_entry_type = frappe.get_cached_value('Stock Entry Type', - {'purpose': self.purpose}, 'name') + self.stock_entry_type = frappe.get_cached_value( + "Stock Entry Type", {"purpose": self.purpose}, "name" + ) def set_purpose_for_stock_entry(self): if self.stock_entry_type and not self.purpose: - self.purpose = frappe.get_cached_value('Stock Entry Type', - self.stock_entry_type, 'purpose') + self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose") def validate_duplicate_serial_no(self): warehouse_wise_serial_nos = {} # In case of repack the source and target serial nos could be same - for warehouse in ['s_warehouse', 't_warehouse']: + for warehouse in ["s_warehouse", "t_warehouse"]: serial_nos = [] for row in self.items: - if not (row.serial_no and row.get(warehouse)): continue + if not (row.serial_no and row.get(warehouse)): + continue for sn in get_serial_nos(row.serial_no): if sn in serial_nos: - frappe.throw(_('The serial no {0} has added multiple times in the stock entry {1}') - .format(frappe.bold(sn), self.name)) + frappe.throw( + _("The serial no {0} has added multiple times in the stock entry {1}").format( + frappe.bold(sn), self.name + ) + ) serial_nos.append(sn) def validate_purchase_order(self): """Throw exception if more raw material is transferred against Purchase Order than in the raw materials supplied table""" - backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings", - "backflush_raw_materials_of_subcontract_based_on") + backflush_raw_materials_based_on = frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ) - qty_allowance = flt(frappe.db.get_single_value("Buying Settings", - "over_transfer_allowance")) + qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) - if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return + if not (self.purpose == "Send to Subcontractor" and self.purchase_order): + return - if (backflush_raw_materials_based_on == 'BOM'): + if backflush_raw_materials_based_on == "BOM": purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) for se_item in self.items: item_code = se_item.original_item or se_item.item_code precision = cint(frappe.db.get_default("float_precision")) or 3 - required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \ - if d.rm_item_code == item_code]) + required_qty = sum( + [flt(d.required_qty) for d in purchase_order.supplied_items if d.rm_item_code == item_code] + ) - total_allowed = required_qty + (required_qty * (qty_allowance/100)) + total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: - bom_no = frappe.db.get_value("Purchase Order Item", + bom_no = frappe.db.get_value( + "Purchase Order Item", {"parent": self.purchase_order, "item_code": se_item.subcontracted_item}, - "bom") + "bom", + ) if se_item.allow_alternative_item: - original_item_code = frappe.get_value("Item Alternative", {"alternative_item_code": item_code}, "item_code") + original_item_code = frappe.get_value( + "Item Alternative", {"alternative_item_code": item_code}, "item_code" + ) - required_qty = sum([flt(d.required_qty) for d in purchase_order.supplied_items \ - if d.rm_item_code == original_item_code]) + required_qty = sum( + [ + flt(d.required_qty) + for d in purchase_order.supplied_items + if d.rm_item_code == original_item_code + ] + ) - total_allowed = required_qty + (required_qty * (qty_allowance/100)) + total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: - frappe.throw(_("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}") - .format(se_item.item_code, self.purchase_order)) - total_supplied = frappe.db.sql("""select sum(transfer_qty) + frappe.throw( + _("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format( + se_item.item_code, self.purchase_order + ) + ) + total_supplied = frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail`, `tabStock Entry` where `tabStock Entry`.purchase_order = %s and `tabStock Entry`.docstatus = 1 and `tabStock Entry Detail`.item_code = %s and `tabStock Entry Detail`.parent = `tabStock Entry`.name""", - (self.purchase_order, se_item.item_code))[0][0] + (self.purchase_order, se_item.item_code), + )[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): - frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}") - .format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order)) + frappe.throw( + _("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format( + se_item.idx, se_item.item_code, total_allowed, self.purchase_order + ) + ) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: - frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}") - .format(row.idx, frappe.bold(row.item_code))) + frappe.throw( + _("Row {0}: Subcontracted Item is mandatory for the raw material {1}").format( + row.idx, frappe.bold(row.item_code) + ) + ) elif not row.po_detail: filters = { - "parent": self.purchase_order, "docstatus": 1, - "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item + "parent": self.purchase_order, + "docstatus": 1, + "rm_item_code": row.item_code, + "main_item_code": row.subcontracted_item, } po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") @@ -703,7 +892,7 @@ class StockEntry(StockController): row.db_set("po_detail", po_detail) def validate_bom(self): - for d in self.get('items'): + for d in self.get("items"): if d.bom_no and d.is_finished_item: item_code = d.original_item or d.item_code validate_bom_no(item_code, d.bom_no) @@ -721,7 +910,7 @@ class StockEntry(StockController): for d in self.items: if d.t_warehouse and not d.s_warehouse: - if self.purpose=="Repack" or d.item_code == finished_item: + if self.purpose == "Repack" or d.item_code == finished_item: d.is_finished_item = 1 else: d.is_scrap_item = 1 @@ -740,19 +929,17 @@ class StockEntry(StockController): def validate_finished_goods(self): """ - 1. Check if FG exists (mfg, repack) - 2. Check if Multiple FG Items are present (mfg) - 3. Check FG Item and Qty against WO if present (mfg) + 1. Check if FG exists (mfg, repack) + 2. Check if Multiple FG Items are present (mfg) + 3. Check FG Item and Qty against WO if present (mfg) """ production_item, wo_qty, finished_items = None, 0, [] - wo_details = frappe.db.get_value( - "Work Order", self.work_order, ["production_item", "qty"] - ) + wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) if wo_details: production_item, wo_qty = wo_details - for d in self.get('items'): + for d in self.get("items"): if d.is_finished_item: if not self.work_order: # Independent MFG Entry/ Repack Entry, no WO to match against @@ -760,12 +947,16 @@ class StockEntry(StockController): continue if d.item_code != production_item: - frappe.throw(_("Finished Item {0} does not match with Work Order {1}") - .format(d.item_code, self.work_order) + frappe.throw( + _("Finished Item {0} does not match with Work Order {1}").format( + d.item_code, self.work_order + ) ) elif flt(d.transfer_qty) > flt(self.fg_completed_qty): - frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}") - .format(d.idx, d.transfer_qty, self.fg_completed_qty) + frappe.throw( + _("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format( + d.idx, d.transfer_qty, self.fg_completed_qty + ) ) finished_items.append(d.item_code) @@ -773,28 +964,31 @@ class StockEntry(StockController): if not finished_items: frappe.throw( msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), - title=_("Missing Finished Good"), exc=FinishedGoodError + title=_("Missing Finished Good"), + exc=FinishedGoodError, ) if self.purpose == "Manufacture": if len(set(finished_items)) > 1: frappe.throw( msg=_("Multiple items cannot be marked as finished item"), - title=_("Note"), exc=FinishedGoodError + title=_("Note"), + exc=FinishedGoodError, ) allowance_percentage = flt( frappe.db.get_single_value( - "Manufacturing Settings","overproduction_percentage_for_work_order" + "Manufacturing Settings", "overproduction_percentage_for_work_order" ) ) - allowed_qty = wo_qty + ((allowance_percentage/100) * wo_qty) + allowed_qty = wo_qty + ((allowance_percentage / 100) * wo_qty) # No work order could mean independent Manufacture entry, if so skip validation if self.work_order and self.fg_completed_qty > allowed_qty: frappe.throw( - _("For quantity {0} should not be greater than work order quantity {1}") - .format(flt(self.fg_completed_qty), wo_qty) + _("For quantity {0} should not be greater than work order quantity {1}").format( + flt(self.fg_completed_qty), wo_qty + ) ) def update_stock_ledger(self): @@ -816,35 +1010,38 @@ class StockEntry(StockController): def get_finished_item_row(self): finished_item_row = None if self.purpose in ("Manufacture", "Repack"): - for d in self.get('items'): + for d in self.get("items"): if d.is_finished_item: finished_item_row = d return finished_item_row def get_sle_for_source_warehouse(self, sl_entries, finished_item_row): - for d in self.get('items'): + for d in self.get("items"): if cstr(d.s_warehouse): - sle = self.get_sl_entries(d, { - "warehouse": cstr(d.s_warehouse), - "actual_qty": -flt(d.transfer_qty), - "incoming_rate": 0 - }) + sle = self.get_sl_entries( + d, {"warehouse": cstr(d.s_warehouse), "actual_qty": -flt(d.transfer_qty), "incoming_rate": 0} + ) if cstr(d.t_warehouse): sle.dependant_sle_voucher_detail_no = d.name - elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse): + elif finished_item_row and ( + finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse + ): sle.dependant_sle_voucher_detail_no = finished_item_row.name sl_entries.append(sle) def get_sle_for_target_warehouse(self, sl_entries, finished_item_row): - for d in self.get('items'): + for d in self.get("items"): if cstr(d.t_warehouse): - sle = self.get_sl_entries(d, { - "warehouse": cstr(d.t_warehouse), - "actual_qty": flt(d.transfer_qty), - "incoming_rate": flt(d.valuation_rate) - }) + sle = self.get_sl_entries( + d, + { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate), + }, + ) if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name): sle.recalculate_rate = 1 @@ -874,40 +1071,55 @@ class StockEntry(StockController): continue item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) - item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, { - "amount": 0.0, - "base_amount": 0.0 - }) + item_account_wise_additional_cost[(d.item_code, d.name)].setdefault( + t.expense_account, {"amount": 0.0, "base_amount": 0.0} + ) multiply_based_on = d.basic_amount if total_basic_amount else d.qty - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += ( flt(t.amount * multiply_based_on) / divide_based_on + ) - item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \ + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += ( flt(t.base_amount * multiply_based_on) / divide_based_on + ) if item_account_wise_additional_cost: for d in self.get("items"): - for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})): - if not amount: continue + for account, amount in iteritems( + item_account_wise_additional_cost.get((d.item_code, d.name), {}) + ): + if not amount: + continue - gl_entries.append(self.get_gl_dict({ - "account": account, - "against": d.expense_account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit_in_account_currency": flt(amount["amount"]), - "credit": flt(amount["base_amount"]) - }, item=d)) + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": d.expense_account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit_in_account_currency": flt(amount["amount"]), + "credit": flt(amount["base_amount"]), + }, + item=d, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": d.expense_account, - "against": account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully - }, item=d)) + gl_entries.append( + self.get_gl_dict( + { + "account": d.expense_account, + "against": account, + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": -1 + * amount["base_amount"], # put it as negative credit instead of debit purposefully + }, + item=d, + ) + ) return process_gl_map(gl_entries) @@ -916,11 +1128,13 @@ class StockEntry(StockController): if flt(pro_doc.docstatus) != 1: frappe.throw(_("Work Order {0} must be submitted").format(self.work_order)) - if pro_doc.status == 'Stopped': - frappe.throw(_("Transaction not allowed against stopped Work Order {0}").format(self.work_order)) + if pro_doc.status == "Stopped": + frappe.throw( + _("Transaction not allowed against stopped Work Order {0}").format(self.work_order) + ) if self.job_card: - job_doc = frappe.get_doc('Job Card', self.job_card) + job_doc = frappe.get_doc("Job Card", self.job_card) job_doc.set_transferred_qty(update_status=True) job_doc.set_transferred_qty_in_job_card(self) @@ -940,73 +1154,95 @@ class StockEntry(StockController): @frappe.whitelist() def get_item_details(self, args=None, for_update=False): - item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, + item = frappe.db.sql( + """select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item, id.expense_account, id.buying_cost_center from `tabItem` i LEFT JOIN `tabItem Default` id ON i.name=id.parent and id.company=%s where i.name=%s and i.disabled=0 and (i.end_of_life is null or i.end_of_life='0000-00-00' or i.end_of_life > %s)""", - (self.company, args.get('item_code'), nowdate()), as_dict = 1) + (self.company, args.get("item_code"), nowdate()), + as_dict=1, + ) if not item: - frappe.throw(_("Item {0} is not active or end of life has been reached").format(args.get("item_code"))) + frappe.throw( + _("Item {0} is not active or end of life has been reached").format(args.get("item_code")) + ) item = item[0] item_group_defaults = get_item_group_defaults(item.name, self.company) brand_defaults = get_brand_defaults(item.name, self.company) - ret = frappe._dict({ - 'uom' : item.stock_uom, - 'stock_uom' : item.stock_uom, - 'description' : item.description, - 'image' : item.image, - 'item_name' : item.item_name, - 'cost_center' : get_default_cost_center(args, item, item_group_defaults, brand_defaults, self.company), - 'qty' : args.get("qty"), - 'transfer_qty' : args.get('qty'), - 'conversion_factor' : 1, - 'batch_no' : '', - 'actual_qty' : 0, - 'basic_rate' : 0, - 'serial_no' : '', - 'has_serial_no' : item.has_serial_no, - 'has_batch_no' : item.has_batch_no, - 'sample_quantity' : item.sample_quantity, - 'expense_account' : item.expense_account - }) + ret = frappe._dict( + { + "uom": item.stock_uom, + "stock_uom": item.stock_uom, + "description": item.description, + "image": item.image, + "item_name": item.item_name, + "cost_center": get_default_cost_center( + args, item, item_group_defaults, brand_defaults, self.company + ), + "qty": args.get("qty"), + "transfer_qty": args.get("qty"), + "conversion_factor": 1, + "batch_no": "", + "actual_qty": 0, + "basic_rate": 0, + "serial_no": "", + "has_serial_no": item.has_serial_no, + "has_batch_no": item.has_batch_no, + "sample_quantity": item.sample_quantity, + "expense_account": item.expense_account, + } + ) - if self.purpose == 'Send to Subcontractor': + if self.purpose == "Send to Subcontractor": ret["allow_alternative_item"] = item.allow_alternative_item # update uom if args.get("uom") and for_update: - ret.update(get_uom_details(args.get('item_code'), args.get('uom'), args.get('qty'))) + ret.update(get_uom_details(args.get("item_code"), args.get("uom"), args.get("qty"))) - if self.purpose == 'Material Issue': - ret["expense_account"] = (item.get("expense_account") or - item_group_defaults.get("expense_account") or - frappe.get_cached_value('Company', self.company, "default_expense_account")) + if self.purpose == "Material Issue": + ret["expense_account"] = ( + item.get("expense_account") + or item_group_defaults.get("expense_account") + or frappe.get_cached_value("Company", self.company, "default_expense_account") + ) - for company_field, field in {'stock_adjustment_account': 'expense_account', - 'cost_center': 'cost_center'}.items(): + for company_field, field in { + "stock_adjustment_account": "expense_account", + "cost_center": "cost_center", + }.items(): if not ret.get(field): - ret[field] = frappe.get_cached_value('Company', self.company, company_field) + ret[field] = frappe.get_cached_value("Company", self.company, company_field) - args['posting_date'] = self.posting_date - args['posting_time'] = self.posting_time + args["posting_date"] = self.posting_date + args["posting_time"] = self.posting_time - stock_and_rate = get_warehouse_details(args) if args.get('warehouse') else {} + stock_and_rate = get_warehouse_details(args) if args.get("warehouse") else {} ret.update(stock_and_rate) # automatically select batch for outgoing item - if (args.get('s_warehouse', None) and args.get('qty') and - ret.get('has_batch_no') and not args.get('batch_no')): - args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty']) + if ( + args.get("s_warehouse", None) + and args.get("qty") + and ret.get("has_batch_no") + and not args.get("batch_no") + ): + args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]) - if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'): - subcontract_items = frappe.get_all("Purchase Order Item Supplied", - {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code") + if ( + self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code") + ): + subcontract_items = frappe.get_all( + "Purchase Order Item Supplied", + {"parent": self.purchase_order, "rm_item_code": args.get("item_code")}, + "main_item_code", + ) if subcontract_items and len(subcontract_items) == 1: ret["subcontracted_item"] = subcontract_items[0].main_item_code @@ -1017,46 +1253,57 @@ class StockEntry(StockController): def set_items_for_stock_in(self): self.items = [] - if self.outgoing_stock_entry and self.purpose == 'Material Transfer': - doc = frappe.get_doc('Stock Entry', self.outgoing_stock_entry) + if self.outgoing_stock_entry and self.purpose == "Material Transfer": + doc = frappe.get_doc("Stock Entry", self.outgoing_stock_entry) if doc.per_transferred == 100: - frappe.throw(_("Goods are already received against the outward entry {0}") - .format(doc.name)) + frappe.throw(_("Goods are already received against the outward entry {0}").format(doc.name)) for d in doc.items: - self.append('items', { - 's_warehouse': d.t_warehouse, - 'item_code': d.item_code, - 'qty': d.qty, - 'uom': d.uom, - 'against_stock_entry': d.parent, - 'ste_detail': d.name, - 'stock_uom': d.stock_uom, - 'conversion_factor': d.conversion_factor, - 'serial_no': d.serial_no, - 'batch_no': d.batch_no - }) + self.append( + "items", + { + "s_warehouse": d.t_warehouse, + "item_code": d.item_code, + "qty": d.qty, + "uom": d.uom, + "against_stock_entry": d.parent, + "ste_detail": d.name, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "serial_no": d.serial_no, + "batch_no": d.batch_no, + }, + ) @frappe.whitelist() def get_items(self): - self.set('items', []) + self.set("items", []) self.validate_work_order() if not self.posting_date or not self.posting_time: frappe.throw(_("Posting date and posting time is mandatory")) self.set_work_order_details() - self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on") + self.flags.backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) if self.bom_no: - backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on") + backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) - if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack", - "Send to Subcontractor", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]: + if self.purpose in [ + "Material Issue", + "Material Transfer", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + ]: if self.work_order and self.purpose == "Material Transfer for Manufacture": item_dict = self.get_pending_raw_materials(backflush_based_on) @@ -1065,14 +1312,20 @@ class StockEntry(StockController): item["to_warehouse"] = self.pro_doc.wip_warehouse self.add_to_stock_entry_detail(item_dict) - elif (self.work_order and (self.purpose == "Manufacture" - or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer - and self.flags.backflush_based_on == "Material Transferred for Manufacture"): + elif ( + self.work_order + and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and not self.pro_doc.skip_transfer + and self.flags.backflush_based_on == "Material Transferred for Manufacture" + ): self.get_transfered_raw_materials() - elif (self.work_order and (self.purpose == "Manufacture" or - self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM" - and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1): + elif ( + self.work_order + and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") + and self.flags.backflush_based_on == "BOM" + and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1 + ): self.get_unconsumed_raw_materials() else: @@ -1081,31 +1334,36 @@ class StockEntry(StockController): item_dict = self.get_bom_raw_materials(self.fg_completed_qty) - #Get PO Supplied Items Details + # Get PO Supplied Items Details if self.purchase_order and self.purpose == "Send to Subcontractor": - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" + # Get PO Supplied Items Details + item_wh = frappe._dict( + frappe.db.sql( + """ SELECT rm_item_code, reserve_warehouse FROM `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup WHERE - po.name = poitemsup.parent and po.name = %s """,self.purchase_order)) + po.name = poitemsup.parent and po.name = %s """, + self.purchase_order, + ) + ) for item in itervalues(item_dict): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): item["from_warehouse"] = self.pro_doc.wip_warehouse - #Get Reserve Warehouse from PO - if self.purchase_order and self.purpose=="Send to Subcontractor": + # Get Reserve Warehouse from PO + if self.purchase_order and self.purpose == "Send to Subcontractor": item["from_warehouse"] = item_wh.get(item.item_code) - item["to_warehouse"] = self.to_warehouse if self.purpose=="Send to Subcontractor" else "" + item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else "" self.add_to_stock_entry_detail(item_dict) # fetch the serial_no of the first stock entry for the second stock entry if self.work_order and self.purpose == "Manufacture": self.set_serial_nos(self.work_order) - work_order = frappe.get_doc('Work Order', self.work_order) + work_order = frappe.get_doc("Work Order", self.work_order) add_additional_cost(self, work_order) # add finished goods item @@ -1122,7 +1380,7 @@ class StockEntry(StockController): if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) for item in itervalues(scrap_item_dict): - item.idx = '' + item.idx = "" if self.pro_doc and self.pro_doc.scrap_warehouse: item["to_warehouse"] = self.pro_doc.scrap_warehouse @@ -1135,7 +1393,7 @@ class StockEntry(StockController): if self.work_order: # common validations if not self.pro_doc: - self.pro_doc = frappe.get_doc('Work Order', self.work_order) + self.pro_doc = frappe.get_doc("Work Order", self.work_order) if self.pro_doc: self.bom_no = self.pro_doc.bom_no @@ -1166,11 +1424,18 @@ class StockEntry(StockController): "stock_uom": item.stock_uom, "expense_account": item.get("expense_account"), "cost_center": item.get("buying_cost_center"), - "is_finished_item": 1 + "is_finished_item": 1, } - if self.work_order and self.pro_doc.has_batch_no and cint(frappe.db.get_single_value('Manufacturing Settings', - 'make_serial_no_batch_from_work_order', cache=True)): + if ( + self.work_order + and self.pro_doc.has_batch_no + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "make_serial_no_batch_from_work_order", cache=True + ) + ) + ): self.set_batchwise_finished_goods(args, item) else: self.add_finished_goods(args, item) @@ -1179,12 +1444,12 @@ class StockEntry(StockController): filters = { "reference_name": self.pro_doc.name, "reference_doctype": self.pro_doc.doctype, - "qty_to_produce": (">", 0) + "qty_to_produce": (">", 0), } fields = ["qty_to_produce as qty", "produced_qty", "name"] - data = frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc") + data = frappe.get_all("Batch", filters=filters, fields=fields, order_by="creation asc") if not data: self.add_finished_goods(args, item) @@ -1199,7 +1464,7 @@ class StockEntry(StockController): if not batch_qty: continue - if qty <=0: + if qty <= 0: break fg_qty = batch_qty @@ -1213,23 +1478,27 @@ class StockEntry(StockController): self.add_finished_goods(args, item) def add_finished_goods(self, args, item): - self.add_to_stock_entry_detail({ - item.name: args - }, bom_no = self.bom_no) + self.add_to_stock_entry_detail({item.name: args}, bom_no=self.bom_no) def get_bom_raw_materials(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict # item dict = { item_code: {qty, description, stock_uom} } - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = self.use_multi_level_bom, fetch_qty_in_stock_uom=False) + item_dict = get_bom_items_as_dict( + self.bom_no, + self.company, + qty=qty, + fetch_exploded=self.use_multi_level_bom, + fetch_qty_in_stock_uom=False, + ) - used_alternative_items = get_used_alternative_items(work_order = self.work_order) + used_alternative_items = get_used_alternative_items(work_order=self.work_order) for item in itervalues(item_dict): # if source warehouse presents in BOM set from_warehouse as bom source_warehouse if item["allow_alternative_item"]: - item["allow_alternative_item"] = frappe.db.get_value('Work Order', - self.work_order, "allow_alternative_item") + item["allow_alternative_item"] = frappe.db.get_value( + "Work Order", self.work_order, "allow_alternative_item" + ) item.from_warehouse = self.from_warehouse or item.source_warehouse or item.default_warehouse if item.item_code in used_alternative_items: @@ -1247,8 +1516,10 @@ class StockEntry(StockController): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict # item dict = { item_code: {qty, description, stock_uom} } - item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = 0, fetch_scrap_items = 1) or {} + item_dict = ( + get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) + or {} + ) for item in itervalues(item_dict): item.from_warehouse = "" @@ -1262,16 +1533,18 @@ class StockEntry(StockController): if not item_row: item_row = frappe._dict({}) - item_row.update({ - 'uom': row.stock_uom, - 'from_warehouse': '', - 'qty': row.stock_qty + flt(item_row.stock_qty), - 'converison_factor': 1, - 'is_scrap_item': 1, - 'item_name': row.item_name, - 'description': row.description, - 'allow_zero_valuation_rate': 1 - }) + item_row.update( + { + "uom": row.stock_uom, + "from_warehouse": "", + "qty": row.stock_qty + flt(item_row.stock_qty), + "converison_factor": 1, + "is_scrap_item": 1, + "item_name": row.item_name, + "description": row.description, + "allow_zero_valuation_rate": 1, + } + ) item_dict[row.item_code] = item_row @@ -1284,21 +1557,25 @@ class StockEntry(StockController): if not self.pro_doc.operations: return [] - job_card = frappe.qb.DocType('Job Card') - job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item') + job_card = frappe.qb.DocType("Job Card") + job_card_scrap_item = frappe.qb.DocType("Job Card Scrap Item") scrap_items = ( frappe.qb.from_(job_card) .select( - Sum(job_card_scrap_item.stock_qty).as_('stock_qty'), - job_card_scrap_item.item_code, job_card_scrap_item.item_name, - job_card_scrap_item.description, job_card_scrap_item.stock_uom) + Sum(job_card_scrap_item.stock_qty).as_("stock_qty"), + job_card_scrap_item.item_code, + job_card_scrap_item.item_name, + job_card_scrap_item.description, + job_card_scrap_item.stock_uom, + ) .join(job_card_scrap_item) .on(job_card_scrap_item.parent == job_card.name) .where( (job_card_scrap_item.item_code.isnotnull()) & (job_card.work_order == self.work_order) - & (job_card.docstatus == 1)) + & (job_card.docstatus == 1) + ) .groupby(job_card_scrap_item.item_code) ).run(as_dict=1) @@ -1312,7 +1589,7 @@ class StockEntry(StockController): if used_scrap_items.get(row.item_code): used_scrap_items[row.item_code] -= row.stock_qty - if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')): + if cint(frappe.get_cached_value("UOM", row.stock_uom, "must_be_whole_number")): row.stock_qty = frappe.utils.ceil(row.stock_qty) return scrap_items @@ -1323,16 +1600,14 @@ class StockEntry(StockController): def get_used_scrap_items(self): used_scrap_items = defaultdict(float) data = frappe.get_all( - 'Stock Entry', - fields = [ - '`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`' + "Stock Entry", + fields=["`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`qty`"], + filters=[ + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry Detail", "is_scrap_item", "=", 1], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purpose", "in", ["Repack", "Manufacture"]], ], - filters = [ - ['Stock Entry', 'work_order', '=', self.work_order], - ['Stock Entry Detail', 'is_scrap_item', '=', 1], - ['Stock Entry', 'docstatus', '=', 1], - ['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']] - ] ) for row in data: @@ -1342,10 +1617,11 @@ class StockEntry(StockController): def get_unconsumed_raw_materials(self): wo = frappe.get_doc("Work Order", self.work_order) - wo_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order}, - fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"] - ) + wo_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order}, + fields=["item_code", "source_warehouse", "required_qty", "consumed_qty", "transferred_qty"], + ) work_order_qty = wo.material_transferred_for_manufacturing or wo.qty for item in wo_items: @@ -1362,21 +1638,24 @@ class StockEntry(StockController): qty = req_qty_each * flt(self.fg_completed_qty) if qty > 0: - self.add_to_stock_entry_detail({ - item.item_code: { - "from_warehouse": wo.wip_warehouse or item.source_warehouse, - "to_warehouse": "", - "qty": qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item_account_details.stock_uom, - "expense_account": item_account_details.get("expense_account"), - "cost_center": item_account_details.get("buying_cost_center"), + self.add_to_stock_entry_detail( + { + item.item_code: { + "from_warehouse": wo.wip_warehouse or item.source_warehouse, + "to_warehouse": "", + "qty": qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item_account_details.stock_uom, + "expense_account": item_account_details.get("expense_account"), + "cost_center": item_account_details.get("buying_cost_center"), + } } - }) + ) def get_transfered_raw_materials(self): - transferred_materials = frappe.db.sql(""" + transferred_materials = frappe.db.sql( + """ select item_name, original_item, item_code, sum(qty) as qty, sed.t_warehouse as warehouse, description, stock_uom, expense_account, cost_center @@ -1385,9 +1664,13 @@ class StockEntry(StockController): se.name = sed.parent and se.docstatus=1 and se.purpose='Material Transfer for Manufacture' and se.work_order= %s and ifnull(sed.t_warehouse, '') != '' group by sed.item_code, sed.t_warehouse - """, self.work_order, as_dict=1) + """, + self.work_order, + as_dict=1, + ) - materials_already_backflushed = frappe.db.sql(""" + materials_already_backflushed = frappe.db.sql( + """ select item_code, sed.s_warehouse as warehouse, sum(qty) as qty from @@ -1397,26 +1680,34 @@ class StockEntry(StockController): and (se.purpose='Manufacture' or se.purpose='Material Consumption for Manufacture') and se.work_order= %s and ifnull(sed.s_warehouse, '') != '' group by sed.item_code, sed.s_warehouse - """, self.work_order, as_dict=1) + """, + self.work_order, + as_dict=1, + ) - backflushed_materials= {} + backflushed_materials = {} for d in materials_already_backflushed: - backflushed_materials.setdefault(d.item_code,[]).append({d.warehouse: d.qty}) + backflushed_materials.setdefault(d.item_code, []).append({d.warehouse: d.qty}) - po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from - `tabWork Order` where name=%s""", self.work_order, as_dict=1)[0] + po_qty = frappe.db.sql( + """select qty, produced_qty, material_transferred_for_manufacturing from + `tabWork Order` where name=%s""", + self.work_order, + as_dict=1, + )[0] manufacturing_qty = flt(po_qty.qty) or 1 produced_qty = flt(po_qty.produced_qty) trans_qty = flt(po_qty.material_transferred_for_manufacturing) or 1 for item in transferred_materials: - qty= item.qty + qty = item.qty item_code = item.original_item or item.item_code - req_items = frappe.get_all('Work Order Item', - filters={'parent': self.work_order, 'item_code': item_code}, - fields=["required_qty", "consumed_qty"] - ) + req_items = frappe.get_all( + "Work Order Item", + filters={"parent": self.work_order, "item_code": item_code}, + fields=["required_qty", "consumed_qty"], + ) req_qty = flt(req_items[0].required_qty) if req_items else flt(4) req_qty_each = flt(req_qty / manufacturing_qty) @@ -1424,23 +1715,23 @@ class StockEntry(StockController): if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)): if qty >= req_qty: - qty = (req_qty/trans_qty) * flt(self.fg_completed_qty) + qty = (req_qty / trans_qty) * flt(self.fg_completed_qty) else: qty = qty - consumed_qty - if self.purpose == 'Manufacture': + if self.purpose == "Manufacture": # If Material Consumption is booked, must pull only remaining components to finish product if consumed_qty != 0: remaining_qty = consumed_qty - (produced_qty * req_qty_each) exhaust_qty = req_qty_each * produced_qty - if remaining_qty > exhaust_qty : - if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: - qty =0 + if remaining_qty > exhaust_qty: + if (remaining_qty / (req_qty_each * flt(self.fg_completed_qty))) >= 1: + qty = 0 else: qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty else: if self.flags.backflush_based_on == "Material Transferred for Manufacture": - qty = (item.qty/trans_qty) * flt(self.fg_completed_qty) + qty = (item.qty / trans_qty) * flt(self.fg_completed_qty) else: qty = req_qty_each * flt(self.fg_completed_qty) @@ -1448,45 +1739,51 @@ class StockEntry(StockController): precision = frappe.get_precision("Stock Entry Detail", "qty") for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse) > 0: - if (qty > req_qty): - qty = ((flt(qty, precision) - flt(d.get(item.warehouse), precision)) + if qty > req_qty: + qty = ( + (flt(qty, precision) - flt(d.get(item.warehouse), precision)) / (flt(trans_qty, precision) - flt(produced_qty, precision)) ) * flt(self.fg_completed_qty) d[item.warehouse] -= qty - if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')): + if cint(frappe.get_cached_value("UOM", item.stock_uom, "must_be_whole_number")): qty = frappe.utils.ceil(qty) if qty > 0: - self.add_to_stock_entry_detail({ - item.item_code: { - "from_warehouse": item.warehouse, - "to_warehouse": "", - "qty": qty, - "item_name": item.item_name, - "description": item.description, - "stock_uom": item.stock_uom, - "expense_account": item.expense_account, - "cost_center": item.buying_cost_center, - "original_item": item.original_item + self.add_to_stock_entry_detail( + { + item.item_code: { + "from_warehouse": item.warehouse, + "to_warehouse": "", + "qty": qty, + "item_name": item.item_name, + "description": item.description, + "stock_uom": item.stock_uom, + "expense_account": item.expense_account, + "cost_center": item.buying_cost_center, + "original_item": item.original_item, + } } - }) + ) def get_pending_raw_materials(self, backflush_based_on=None): """ - issue (item quantity) that is pending to issue or desire to transfer, - whichever is less + issue (item quantity) that is pending to issue or desire to transfer, + whichever is less """ item_dict = self.get_pro_order_required_items(backflush_based_on) max_qty = flt(self.pro_doc.qty) allow_overproduction = False - overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", - "overproduction_percentage_for_work_order")) + overproduction_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) - to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty) + to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt( + self.fg_completed_qty + ) transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100) if transfer_limit_qty >= to_transfer_qty: @@ -1496,9 +1793,11 @@ class StockEntry(StockController): pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty - if (desire_to_transfer <= pending_to_issue + if ( + desire_to_transfer <= pending_to_issue or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") - or allow_overproduction): + or allow_overproduction + ): item_dict[item]["qty"] = desire_to_transfer elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue @@ -1519,7 +1818,7 @@ class StockEntry(StockController): def get_pro_order_required_items(self, backflush_based_on=None): """ - Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**. + Gets Work Order Required Items only if Stock Entry purpose is **Material Transferred for Manufacture**. """ item_dict, job_card_items = frappe._dict(), [] work_order = frappe.get_doc("Work Order", self.work_order) @@ -1538,7 +1837,9 @@ class StockEntry(StockController): continue transfer_pending = flt(d.required_qty) > flt(d.transferred_qty) - can_transfer = transfer_pending or (backflush_based_on == "Material Transferred for Manufacture") + can_transfer = transfer_pending or ( + backflush_based_on == "Material Transferred for Manufacture" + ) if not can_transfer: continue @@ -1549,11 +1850,7 @@ class StockEntry(StockController): if consider_job_card: job_card_item = frappe.db.get_value( - "Job Card Item", - { - "item_code": d.item_code, - "parent": self.get("job_card") - } + "Job Card Item", {"item_code": d.item_code, "parent": self.get("job_card")} ) item_row["job_card_item"] = job_card_item or None @@ -1573,12 +1870,7 @@ class StockEntry(StockController): return [] job_card_items = frappe.get_all( - "Job Card Item", - filters={ - "parent": job_card - }, - fields=["item_code"], - distinct=True + "Job Card Item", filters={"parent": job_card}, fields=["item_code"], distinct=True ) return [d.item_code for d in job_card_items] @@ -1587,60 +1879,87 @@ class StockEntry(StockController): item_row = item_dict[d] stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom") - se_child = self.append('items') + se_child = self.append("items") se_child.s_warehouse = item_row.get("from_warehouse") se_child.t_warehouse = item_row.get("to_warehouse") - se_child.item_code = item_row.get('item_code') or cstr(d) + se_child.item_code = item_row.get("item_code") or cstr(d) se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom se_child.stock_uom = stock_uom se_child.qty = flt(item_row["qty"], se_child.precision("qty")) se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0) se_child.subcontracted_item = item_row.get("main_item_code") - se_child.cost_center = (item_row.get("cost_center") or - get_default_cost_center(item_row, company = self.company)) + se_child.cost_center = item_row.get("cost_center") or get_default_cost_center( + item_row, company=self.company + ) se_child.is_finished_item = item_row.get("is_finished_item", 0) se_child.is_scrap_item = item_row.get("is_scrap_item", 0) se_child.is_process_loss = item_row.get("is_process_loss", 0) - for field in ["idx", "po_detail", "original_item", "expense_account", - "description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]: + for field in [ + "idx", + "po_detail", + "original_item", + "expense_account", + "description", + "item_name", + "serial_no", + "batch_no", + "allow_zero_valuation_rate", + ]: if item_row.get(field): se_child.set(field, item_row.get(field)) - if se_child.s_warehouse==None: + if se_child.s_warehouse == None: se_child.s_warehouse = self.from_warehouse - if se_child.t_warehouse==None: + if se_child.t_warehouse == None: se_child.t_warehouse = self.to_warehouse # in stock uom se_child.conversion_factor = flt(item_row.get("conversion_factor")) or 1 - se_child.transfer_qty = flt(item_row["qty"]*se_child.conversion_factor, se_child.precision("qty")) + se_child.transfer_qty = flt( + item_row["qty"] * se_child.conversion_factor, se_child.precision("qty") + ) - se_child.bom_no = bom_no # to be assigned for finished item + se_child.bom_no = bom_no # to be assigned for finished item se_child.job_card_item = item_row.get("job_card_item") if self.get("job_card") else None def validate_with_material_request(self): for item in self.get("items"): material_request = item.material_request or None material_request_item = item.material_request_item or None - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: - parent_se = frappe.get_value("Stock Entry Detail", item.ste_detail, ['material_request','material_request_item'],as_dict=True) + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: + parent_se = frappe.get_value( + "Stock Entry Detail", + item.ste_detail, + ["material_request", "material_request_item"], + as_dict=True, + ) if parent_se: material_request = parent_se.material_request material_request_item = parent_se.material_request_item if material_request: - mreq_item = frappe.db.get_value("Material Request Item", + mreq_item = frappe.db.get_value( + "Material Request Item", {"name": material_request_item, "parent": material_request}, - ["item_code", "warehouse", "idx"], as_dict=True) + ["item_code", "warehouse", "idx"], + as_dict=True, + ) if mreq_item.item_code != item.item_code: - frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx), - frappe.MappingMismatchError) + frappe.throw( + _("Item for row {0} does not match Material Request").format(item.idx), + frappe.MappingMismatchError, + ) elif self.purpose == "Material Transfer" and self.add_to_transit: continue def validate_batch(self): - if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]: + if self.purpose in [ + "Material Transfer for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + ]: for item in self.get("items"): if item.batch_no: disabled = frappe.db.get_value("Batch", item.batch_no, "disabled") @@ -1648,30 +1967,34 @@ class StockEntry(StockController): expiry_date = frappe.db.get_value("Batch", item.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): - frappe.throw(_("Batch {0} of Item {1} has expired.") - .format(item.batch_no, item.item_code)) + frappe.throw(_("Batch {0} of Item {1} has expired.").format(item.batch_no, item.item_code)) else: - frappe.throw(_("Batch {0} of Item {1} is disabled.") - .format(item.batch_no, item.item_code)) + frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) def update_purchase_order_supplied_items(self): - if (self.purchase_order and - (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)): + if self.purchase_order and ( + self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return + ): - #Get PO Supplied Items Details - item_wh = frappe._dict(frappe.db.sql(""" + # Get PO Supplied Items Details + item_wh = frappe._dict( + frappe.db.sql( + """ select rm_item_code, reserve_warehouse from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup where po.name = poitemsup.parent - and po.name = %s""", self.purchase_order)) + and po.name = %s""", + self.purchase_order, + ) + ) supplied_items = get_supplied_items(self.purchase_order) for name, item in supplied_items.items(): - frappe.db.set_value('Purchase Order Item Supplied', name, item) + frappe.db.set_value("Purchase Order Item Supplied", name, item) - #Update reserved sub contracted quantity in bin based on Supplied Item Details and + # Update reserved sub contracted quantity in bin based on Supplied Item Details and for d in self.get("items"): - item_code = d.get('original_item') or d.get('item_code') + item_code = d.get("original_item") or d.get("item_code") reserve_warehouse = item_wh.get(item_code) if not (reserve_warehouse and item_code): continue @@ -1679,12 +2002,17 @@ class StockEntry(StockController): stock_bin.update_reserved_qty_for_sub_contracting() def update_so_in_serial_number(self): - so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"]) + so_name, item_code = frappe.db.get_value( + "Work Order", self.work_order, ["sales_order", "production_item"] + ) if so_name and item_code: qty_to_reserve = get_reserved_qty_for_so(so_name, item_code) if qty_to_reserve: - reserved_qty = frappe.db.sql("""select count(name) from `tabSerial No` where item_code=%s and - sales_order=%s""", (item_code, so_name)) + reserved_qty = frappe.db.sql( + """select count(name) from `tabSerial No` where item_code=%s and + sales_order=%s""", + (item_code, so_name), + ) if reserved_qty and reserved_qty[0][0]: qty_to_reserve -= reserved_qty[0][0] if qty_to_reserve > 0: @@ -1695,7 +2023,7 @@ class StockEntry(StockController): for serial_no in serial_nos: if qty_to_reserve > 0: frappe.db.set_value("Serial No", serial_no, "sales_order", so_name) - qty_to_reserve -=1 + qty_to_reserve -= 1 def validate_reserved_serial_no_consumption(self): for item in self.items: @@ -1703,13 +2031,14 @@ class StockEntry(StockController): for sr in get_serial_nos(item.serial_no): sales_order = frappe.db.get_value("Serial No", sr, "sales_order") if sales_order: - msg = (_("(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}.") - .format(sr, sales_order)) + msg = _( + "(Serial No: {0}) cannot be consumed as it's reserverd to fullfill Sales Order {1}." + ).format(sr, sales_order) frappe.throw(_("Item {0} {1}").format(item.item_code, msg)) def update_transferred_qty(self): - if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + if self.purpose == "Material Transfer" and self.outgoing_stock_entry: stock_entries = {} stock_entries_child_list = [] for d in self.items: @@ -1717,70 +2046,87 @@ class StockEntry(StockController): continue stock_entries_child_list.append(d.ste_detail) - transferred_qty = frappe.get_all("Stock Entry Detail", fields = ["sum(qty) as qty"], - filters = { 'against_stock_entry': d.against_stock_entry, - 'ste_detail': d.ste_detail,'docstatus': 1}) + transferred_qty = frappe.get_all( + "Stock Entry Detail", + fields=["sum(qty) as qty"], + filters={ + "against_stock_entry": d.against_stock_entry, + "ste_detail": d.ste_detail, + "docstatus": 1, + }, + ) - stock_entries[(d.against_stock_entry, d.ste_detail)] = (transferred_qty[0].qty - if transferred_qty and transferred_qty[0] else 0.0) or 0.0 + stock_entries[(d.against_stock_entry, d.ste_detail)] = ( + transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0 + ) or 0.0 - if not stock_entries: return None + if not stock_entries: + return None - cond = '' + cond = "" for data, transferred_qty in stock_entries.items(): cond += """ WHEN (parent = %s and name = %s) THEN %s - """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) + """ % ( + frappe.db.escape(data[0]), + frappe.db.escape(data[1]), + transferred_qty, + ) if stock_entries_child_list: - frappe.db.sql(""" UPDATE `tabStock Entry Detail` + frappe.db.sql( + """ UPDATE `tabStock Entry Detail` SET transferred_qty = CASE {cond} END WHERE - name in ({ste_details}) """.format(cond=cond, - ste_details = ','.join(['%s'] * len(stock_entries_child_list))), - tuple(stock_entries_child_list)) + name in ({ste_details}) """.format( + cond=cond, ste_details=",".join(["%s"] * len(stock_entries_child_list)) + ), + tuple(stock_entries_child_list), + ) args = { - 'source_dt': 'Stock Entry Detail', - 'target_field': 'transferred_qty', - 'target_ref_field': 'qty', - 'target_dt': 'Stock Entry Detail', - 'join_field': 'ste_detail', - 'target_parent_dt': 'Stock Entry', - 'target_parent_field': 'per_transferred', - 'source_field': 'qty', - 'percent_join_field': 'against_stock_entry' + "source_dt": "Stock Entry Detail", + "target_field": "transferred_qty", + "target_ref_field": "qty", + "target_dt": "Stock Entry Detail", + "join_field": "ste_detail", + "target_parent_dt": "Stock Entry", + "target_parent_field": "per_transferred", + "source_field": "qty", + "percent_join_field": "against_stock_entry", } self._update_percent_field_in_targets(args, update_modified=True) def update_quality_inspection(self): if self.inspection_required: - reference_type = reference_name = '' + reference_type = reference_name = "" if self.docstatus == 1: reference_name = self.name - reference_type = 'Stock Entry' + reference_type = "Stock Entry" for d in self.items: if d.quality_inspection: - frappe.db.set_value("Quality Inspection", d.quality_inspection, { - 'reference_type': reference_type, - 'reference_name': reference_name - }) + frappe.db.set_value( + "Quality Inspection", + d.quality_inspection, + {"reference_type": reference_type, "reference_name": reference_name}, + ) + def set_material_request_transfer_status(self, status): material_requests = [] if self.outgoing_stock_entry: - parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit') + parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit") for item in self.items: material_request = item.material_request or None if self.purpose == "Material Transfer" and material_request not in material_requests: if self.outgoing_stock_entry and parent_se: - material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, 'material_request') + material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, "material_request") if material_request and material_request not in material_requests: material_requests.append(material_request) - frappe.db.set_value('Material Request', material_request, 'transfer_status', status) + frappe.db.set_value("Material Request", material_request, "transfer_status", status) def update_items_for_process_loss(self): process_loss_dict = {} @@ -1788,7 +2134,9 @@ class StockEntry(StockController): if not d.is_process_loss: continue - scrap_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_scrap_warehouse") + scrap_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_scrap_warehouse" + ) if scrap_warehouse is not None: d.t_warehouse = scrap_warehouse d.is_scrap_item = 0 @@ -1805,7 +2153,6 @@ class StockEntry(StockController): d.transfer_qty -= process_loss_dict[d.item_code][0] d.qty -= process_loss_dict[d.item_code][1] - def set_serial_no_batch_for_finished_good(self): args = {} if self.pro_doc.serial_no: @@ -1814,14 +2161,22 @@ class StockEntry(StockController): for row in self.items: if row.is_finished_item and row.item_code == self.pro_doc.production_item: if args.get("serial_no"): - row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)]) + row.serial_no = "\n".join(args["serial_no"][0 : cint(row.qty)]) def get_serial_nos_for_fg(self, args): - fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`", - "`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"] + fields = [ + "`tabStock Entry`.`name`", + "`tabStock Entry Detail`.`qty`", + "`tabStock Entry Detail`.`serial_no`", + "`tabStock Entry Detail`.`batch_no`", + ] - filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"], - ["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]] + filters = [ + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry", "purpose", "=", "Manufacture"], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry Detail", "item_code", "=", self.pro_doc.production_item], + ] stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters) @@ -1836,85 +2191,98 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): items = json.loads(items) - retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse') + retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse") stock_entry = frappe.new_doc("Stock Entry") stock_entry.company = company stock_entry.purpose = "Material Transfer" stock_entry.set_stock_entry_type() for item in items: - if item.get('sample_quantity') and item.get('batch_no'): - sample_quantity = validate_sample_quantity(item.get('item_code'), item.get('sample_quantity'), - item.get('transfer_qty') or item.get('qty'), item.get('batch_no')) + if item.get("sample_quantity") and item.get("batch_no"): + sample_quantity = validate_sample_quantity( + item.get("item_code"), + item.get("sample_quantity"), + item.get("transfer_qty") or item.get("qty"), + item.get("batch_no"), + ) if sample_quantity: - sample_serial_nos = '' - if item.get('serial_no'): - serial_nos = (item.get('serial_no')).split() - if serial_nos and len(serial_nos) > item.get('sample_quantity'): - serial_no_list = serial_nos[:-(len(serial_nos)-item.get('sample_quantity'))] - sample_serial_nos = '\n'.join(serial_no_list) + sample_serial_nos = "" + if item.get("serial_no"): + serial_nos = (item.get("serial_no")).split() + if serial_nos and len(serial_nos) > item.get("sample_quantity"): + serial_no_list = serial_nos[: -(len(serial_nos) - item.get("sample_quantity"))] + sample_serial_nos = "\n".join(serial_no_list) - stock_entry.append("items", { - "item_code": item.get('item_code'), - "s_warehouse": item.get('t_warehouse'), - "t_warehouse": retention_warehouse, - "qty": item.get('sample_quantity'), - "basic_rate": item.get('valuation_rate'), - 'uom': item.get('uom'), - 'stock_uom': item.get('stock_uom'), - "conversion_factor": 1.0, - "serial_no": sample_serial_nos, - 'batch_no': item.get('batch_no') - }) - if stock_entry.get('items'): + stock_entry.append( + "items", + { + "item_code": item.get("item_code"), + "s_warehouse": item.get("t_warehouse"), + "t_warehouse": retention_warehouse, + "qty": item.get("sample_quantity"), + "basic_rate": item.get("valuation_rate"), + "uom": item.get("uom"), + "stock_uom": item.get("stock_uom"), + "conversion_factor": 1.0, + "serial_no": sample_serial_nos, + "batch_no": item.get("batch_no"), + }, + ) + if stock_entry.get("items"): return stock_entry.as_dict() + @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): - def set_missing_values(source, target): target.set_stock_entry_type() def update_item(source_doc, target_doc, source_parent): - target_doc.t_warehouse = '' + target_doc.t_warehouse = "" - if source_doc.material_request_item and source_doc.material_request : - add_to_transit = frappe.db.get_value('Stock Entry', source_name, 'add_to_transit') + if source_doc.material_request_item and source_doc.material_request: + add_to_transit = frappe.db.get_value("Stock Entry", source_name, "add_to_transit") if add_to_transit: - warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse') + warehouse = frappe.get_value( + "Material Request Item", source_doc.material_request_item, "warehouse" + ) target_doc.t_warehouse = warehouse target_doc.s_warehouse = source_doc.t_warehouse target_doc.qty = source_doc.qty - source_doc.transferred_qty - doclist = get_mapped_doc("Stock Entry", source_name, { - "Stock Entry": { - "doctype": "Stock Entry", - "field_map": { - "name": "outgoing_stock_entry" + doclist = get_mapped_doc( + "Stock Entry", + source_name, + { + "Stock Entry": { + "doctype": "Stock Entry", + "field_map": {"name": "outgoing_stock_entry"}, + "validation": {"docstatus": ["=", 1]}, }, - "validation": { - "docstatus": ["=", 1] - } - }, - "Stock Entry Detail": { - "doctype": "Stock Entry Detail", - "field_map": { - "name": "ste_detail", - "parent": "against_stock_entry", - "serial_no": "serial_no", - "batch_no": "batch_no" + "Stock Entry Detail": { + "doctype": "Stock Entry Detail", + "field_map": { + "name": "ste_detail", + "parent": "against_stock_entry", + "serial_no": "serial_no", + "batch_no": "batch_no", + }, + "postprocess": update_item, + "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01, }, - "postprocess": update_item, - "condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01 }, - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + @frappe.whitelist() def get_work_order_details(work_order, company): work_order = frappe.get_doc("Work Order", work_order) @@ -1926,9 +2294,10 @@ def get_work_order_details(work_order, company): "use_multi_level_bom": work_order.use_multi_level_bom, "wip_warehouse": work_order.wip_warehouse, "fg_warehouse": work_order.fg_warehouse, - "fg_completed_qty": pending_qty_to_produce + "fg_completed_qty": pending_qty_to_produce, } + def get_operating_cost_per_unit(work_order=None, bom_no=None): operating_cost_per_unit = 0 if work_order: @@ -1947,54 +2316,78 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if bom.quantity: operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) - if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings', - 'add_corrective_operation_cost_in_finished_good_valuation')): - operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) + if ( + work_order + and work_order.produced_qty + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" + ) + ) + ): + operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt( + work_order.produced_qty + ) return operating_cost_per_unit + def get_used_alternative_items(purchase_order=None, work_order=None): cond = "" if purchase_order: - cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(purchase_order) + cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format( + purchase_order + ) elif work_order: - cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(work_order) + cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format( + work_order + ) - if not cond: return {} + if not cond: + return {} used_alternative_items = {} - data = frappe.db.sql(""" select sted.original_item, sted.uom, sted.conversion_factor, + data = frappe.db.sql( + """ select sted.original_item, sted.uom, sted.conversion_factor, sted.item_code, sted.item_name, sted.conversion_factor,sted.stock_uom, sted.description from `tabStock Entry` ste, `tabStock Entry Detail` sted where sted.parent = ste.name and ste.docstatus = 1 and sted.original_item != sted.item_code - {0} """.format(cond), as_dict=1) + {0} """.format( + cond + ), + as_dict=1, + ) for d in data: used_alternative_items[d.original_item] = d return used_alternative_items + def get_valuation_rate_for_finished_good_entry(work_order): - work_order_qty = flt(frappe.get_cached_value("Work Order", - work_order, 'material_transferred_for_manufacturing')) + work_order_qty = flt( + frappe.get_cached_value("Work Order", work_order, "material_transferred_for_manufacturing") + ) field = "(SUM(total_outgoing_value) / %s) as valuation_rate" % (work_order_qty) - stock_data = frappe.get_all("Stock Entry", - fields = field, - filters = { + stock_data = frappe.get_all( + "Stock Entry", + fields=field, + filters={ "docstatus": 1, "purpose": "Material Transfer for Manufacture", - "work_order": work_order - } + "work_order": work_order, + }, ) if stock_data: return stock_data[0].valuation_rate + @frappe.whitelist() def get_uom_details(item_code, uom, qty): """Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}` @@ -2003,24 +2396,31 @@ def get_uom_details(item_code, uom, qty): conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor") if not conversion_factor: - frappe.msgprint(_("UOM coversion factor required for UOM: {0} in Item: {1}") - .format(uom, item_code)) - ret = {'uom' : ''} + frappe.msgprint( + _("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) + ) + ret = {"uom": ""} else: ret = { - 'conversion_factor' : flt(conversion_factor), - 'transfer_qty' : flt(qty) * flt(conversion_factor) + "conversion_factor": flt(conversion_factor), + "transfer_qty": flt(qty) * flt(conversion_factor), } return ret + @frappe.whitelist() def get_expired_batch_items(): - return frappe.db.sql("""select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\ + return frappe.db.sql( + """select b.item, sum(sle.actual_qty) as qty, sle.batch_no, sle.warehouse, sle.stock_uom\ from `tabBatch` b, `tabStock Ledger Entry` sle where b.expiry_date <= %s and b.expiry_date is not NULL and b.batch_id = sle.batch_no and sle.is_cancelled = 0 - group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1) + group by sle.warehouse, sle.item_code, sle.batch_no""", + (nowdate()), + as_dict=1, + ) + @frappe.whitelist() def get_warehouse_details(args): @@ -2031,51 +2431,73 @@ def get_warehouse_details(args): ret = {} if args.warehouse and args.item_code: - args.update({ - "posting_date": args.posting_date, - "posting_time": args.posting_time, - }) + args.update( + { + "posting_date": args.posting_date, + "posting_time": args.posting_time, + } + ) ret = { - "actual_qty" : get_previous_sle(args).get("qty_after_transaction") or 0, - "basic_rate" : get_incoming_rate(args) + "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, + "basic_rate": get_incoming_rate(args), } return ret + @frappe.whitelist() -def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None): +def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None): if cint(qty) < cint(sample_quantity): - frappe.throw(_("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty)) - retention_warehouse = frappe.db.get_single_value('Stock Settings', 'sample_retention_warehouse') + frappe.throw( + _("Sample quantity {0} cannot be more than received quantity {1}").format(sample_quantity, qty) + ) + retention_warehouse = frappe.db.get_single_value("Stock Settings", "sample_retention_warehouse") retainted_qty = 0 if batch_no: retainted_qty = get_batch_qty(batch_no, retention_warehouse, item_code) - max_retain_qty = frappe.get_value('Item', item_code, 'sample_quantity') + max_retain_qty = frappe.get_value("Item", item_code, "sample_quantity") if retainted_qty >= max_retain_qty: - frappe.msgprint(_("Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}."). - format(retainted_qty, batch_no, item_code, batch_no), alert=True) + frappe.msgprint( + _( + "Maximum Samples - {0} have already been retained for Batch {1} and Item {2} in Batch {3}." + ).format(retainted_qty, batch_no, item_code, batch_no), + alert=True, + ) sample_quantity = 0 - qty_diff = max_retain_qty-retainted_qty + qty_diff = max_retain_qty - retainted_qty if cint(sample_quantity) > cint(qty_diff): - frappe.msgprint(_("Maximum Samples - {0} can be retained for Batch {1} and Item {2}."). - format(max_retain_qty, batch_no, item_code), alert=True) + frappe.msgprint( + _("Maximum Samples - {0} can be retained for Batch {1} and Item {2}.").format( + max_retain_qty, batch_no, item_code + ), + alert=True, + ) sample_quantity = qty_diff return sample_quantity -def get_supplied_items(purchase_order): - fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`', - '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`'] - filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]] +def get_supplied_items(purchase_order): + fields = [ + "`tabStock Entry Detail`.`transfer_qty`", + "`tabStock Entry`.`is_return`", + "`tabStock Entry Detail`.`po_detail`", + "`tabStock Entry Detail`.`item_code`", + ] + + filters = [ + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purchase_order", "=", purchase_order], + ] supplied_item_details = {} - for row in frappe.get_all('Stock Entry', fields = fields, filters = filters): + for row in frappe.get_all("Stock Entry", fields=fields, filters=filters): if not row.po_detail: continue key = row.po_detail if key not in supplied_item_details: - supplied_item_details.setdefault(key, - frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0})) + supplied_item_details.setdefault( + key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}) + ) supplied_item = supplied_item_details[key] @@ -2084,6 +2506,8 @@ def get_supplied_items(purchase_order): else: supplied_item.supplied_qty += row.transfer_qty - supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty) + supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt( + supplied_item.returned_qty + ) return supplied_item_details diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index 5832fe49b2d..85ccc5b13fc 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -11,7 +11,7 @@ import erpnext @frappe.whitelist() def make_stock_entry(**args): - '''Helper function to make a Stock Entry + """Helper function to make a Stock Entry :item_code: Item to be moved :qty: Qty to be moved @@ -26,16 +26,16 @@ def make_stock_entry(**args): :purpose: Optional :do_not_save: Optional flag :do_not_submit: Optional flag - ''' + """ def process_serial_numbers(serial_nos_list): serial_nos_list = [ - '\n'.join(serial_num['serial_no'] for serial_num in serial_nos_list if serial_num.serial_no) + "\n".join(serial_num["serial_no"] for serial_num in serial_nos_list if serial_num.serial_no) ] - uniques = list(set(serial_nos_list[0].split('\n'))) + uniques = list(set(serial_nos_list[0].split("\n"))) - return '\n'.join(uniques) + return "\n".join(uniques) s = frappe.new_doc("Stock Entry") args = frappe._dict(args) @@ -61,7 +61,7 @@ def make_stock_entry(**args): s.apply_putaway_rule = args.apply_putaway_rule if isinstance(args.qty, string_types): - if '.' in args.qty: + if "." in args.qty: args.qty = flt(args.qty) else: args.qty = cint(args.qty) @@ -80,16 +80,16 @@ def make_stock_entry(**args): # company if not args.company: if args.source: - args.company = frappe.db.get_value('Warehouse', args.source, 'company') + args.company = frappe.db.get_value("Warehouse", args.source, "company") elif args.target: - args.company = frappe.db.get_value('Warehouse', args.target, 'company') + args.company = frappe.db.get_value("Warehouse", args.target, "company") # set vales from test if frappe.flags.in_test: if not args.company: - args.company = '_Test Company' + args.company = "_Test Company" if not args.item: - args.item = '_Test Item' + args.item = "_Test Item" s.company = args.company or erpnext.get_default_company() s.purchase_receipt_no = args.purchase_receipt_no @@ -97,40 +97,40 @@ def make_stock_entry(**args): s.sales_invoice_no = args.sales_invoice_no s.is_opening = args.is_opening or "No" if not args.cost_center: - args.cost_center = frappe.get_value('Company', s.company, 'cost_center') + args.cost_center = frappe.get_value("Company", s.company, "cost_center") if not args.expense_account and s.is_opening == "No": - args.expense_account = frappe.get_value('Company', s.company, 'stock_adjustment_account') + args.expense_account = frappe.get_value("Company", s.company, "stock_adjustment_account") # We can find out the serial number using the batch source document serial_number = args.serial_no if not args.serial_no and args.qty and args.batch_no: serial_number_list = frappe.get_list( - doctype='Stock Ledger Entry', - fields=['serial_no'], - filters={ - 'batch_no': args.batch_no, - 'warehouse': args.from_warehouse - } + doctype="Stock Ledger Entry", + fields=["serial_no"], + filters={"batch_no": args.batch_no, "warehouse": args.from_warehouse}, ) serial_number = process_serial_numbers(serial_number_list) args.serial_no = serial_number - s.append("items", { - "item_code": args.item, - "s_warehouse": args.source, - "t_warehouse": args.target, - "qty": args.qty, - "basic_rate": args.rate or args.basic_rate, - "conversion_factor": args.conversion_factor or 1.0, - "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), - "serial_no": args.serial_no, - 'batch_no': args.batch_no, - 'cost_center': args.cost_center, - 'expense_account': args.expense_account - }) + s.append( + "items", + { + "item_code": args.item, + "s_warehouse": args.source, + "t_warehouse": args.target, + "qty": args.qty, + "basic_rate": args.rate or args.basic_rate, + "conversion_factor": args.conversion_factor or 1.0, + "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0), + "serial_no": args.serial_no, + "batch_no": args.batch_no, + "cost_center": args.cost_center, + "expense_account": args.expense_account, + }, + ) s.set_stock_entry_type() if not args.do_not_save: diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6a3b21d81c8..9992b77c00e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -39,9 +39,14 @@ def get_sle(**args): condition += "`{0}`=%s".format(key) values.append(value) - return frappe.db.sql("""select * from `tabStock Ledger Entry` %s - order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, - values, as_dict=1) + return frappe.db.sql( + """select * from `tabStock Ledger Entry` %s + order by timestamp(posting_date, posting_time) desc, creation desc limit 1""" + % condition, + values, + as_dict=1, + ) + class TestStockEntry(FrappeTestCase): def tearDown(self): @@ -54,36 +59,37 @@ class TestStockEntry(FrappeTestCase): item_code = "_Test Item 2" warehouse = "_Test Warehouse - _TC" - create_stock_reconciliation(item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", - qty=0, rate=100) + create_stock_reconciliation( + item_code="_Test Item 2", warehouse="_Test Warehouse - _TC", qty=0, rate=100 + ) make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=10) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[1, 10]], frappe.safe_eval(sle.stock_queue)) # negative qty make_stock_entry(item_code=item_code, source=warehouse, qty=2, basic_rate=10) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[-1, 10]], frappe.safe_eval(sle.stock_queue)) # further negative make_stock_entry(item_code=item_code, source=warehouse, qty=1) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[-2, 10]], frappe.safe_eval(sle.stock_queue)) # move stock to positive make_stock_entry(item_code=item_code, target=warehouse, qty=3, basic_rate=20) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] self.assertEqual([[1, 20]], frappe.safe_eval(sle.stock_queue)) # incoming entry with diff rate make_stock_entry(item_code=item_code, target=warehouse, qty=1, basic_rate=30) - sle = get_sle(item_code = item_code, warehouse = warehouse)[0] + sle = get_sle(item_code=item_code, warehouse=warehouse)[0] - self.assertEqual([[1, 20],[1, 30]], frappe.safe_eval(sle.stock_queue)) + self.assertEqual([[1, 20], [1, 30]], frappe.safe_eval(sle.stock_queue)) frappe.db.set_default("allow_negative_stock", 0) @@ -93,37 +99,48 @@ class TestStockEntry(FrappeTestCase): self._test_auto_material_request("_Test Item", material_request_type="Transfer") def test_auto_material_request_for_variant(self): - fields = [{'field_name': 'reorder_levels'}] + fields = [{"field_name": "reorder_levels"}] set_item_variant_settings(fields) make_item_variant() template = frappe.get_doc("Item", "_Test Variant Item") if not template.reorder_levels: - template.append('reorder_levels', { - "material_request_type": "Purchase", - "warehouse": "_Test Warehouse - _TC", - "warehouse_reorder_level": 20, - "warehouse_reorder_qty": 20 - }) + template.append( + "reorder_levels", + { + "material_request_type": "Purchase", + "warehouse": "_Test Warehouse - _TC", + "warehouse_reorder_level": 20, + "warehouse_reorder_qty": 20, + }, + ) template.save() self._test_auto_material_request("_Test Variant Item-S") def test_auto_material_request_for_warehouse_group(self): - self._test_auto_material_request("_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC") + self._test_auto_material_request( + "_Test Item Warehouse Group Wise Reorder", warehouse="_Test Warehouse Group-C1 - _TC" + ) - def _test_auto_material_request(self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC"): + def _test_auto_material_request( + self, item_code, material_request_type="Purchase", warehouse="_Test Warehouse - _TC" + ): variant = frappe.get_doc("Item", item_code) - projected_qty, actual_qty = frappe.db.get_value("Bin", {"item_code": item_code, - "warehouse": warehouse}, ["projected_qty", "actual_qty"]) or [0, 0] + projected_qty, actual_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, ["projected_qty", "actual_qty"] + ) or [0, 0] # stock entry reqd for auto-reorder - create_stock_reconciliation(item_code=item_code, warehouse=warehouse, - qty = actual_qty + abs(projected_qty) + 10, rate=100) + create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=actual_qty + abs(projected_qty) + 10, rate=100 + ) - projected_qty = frappe.db.get_value("Bin", {"item_code": item_code, - "warehouse": warehouse}, "projected_qty") or 0 + projected_qty = ( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty") + or 0 + ) frappe.db.set_value("Stock Settings", None, "auto_indent", 1) @@ -134,6 +151,7 @@ class TestStockEntry(FrappeTestCase): variant.save() from erpnext.stock.reorder_item import reorder_item + mr_list = reorder_item() frappe.db.set_value("Stock Settings", None, "auto_indent", 0) @@ -146,65 +164,113 @@ class TestStockEntry(FrappeTestCase): self.assertTrue(item_code in items) def test_material_receipt_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + mr = make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) stock_in_hand_account = get_inventory_account(mr.company, mr.get("items")[0].t_warehouse) - self.check_stock_ledger_entries("Stock Entry", mr.name, - [["_Test Item", "Stores - TCP1", 50.0]]) + self.check_stock_ledger_entries("Stock Entry", mr.name, [["_Test Item", "Stores - TCP1", 50.0]]) - self.check_gl_entries("Stock Entry", mr.name, - sorted([ - [stock_in_hand_account, 5000.0, 0.0], - ["Stock Adjustment - TCP1", 0.0, 5000.0] - ]) + self.check_gl_entries( + "Stock Entry", + mr.name, + sorted([[stock_in_hand_account, 5000.0, 0.0], ["Stock Adjustment - TCP1", 0.0, 5000.0]]), ) mr.cancel() - self.assertTrue(frappe.db.sql("""select * from `tabStock Ledger Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) + self.assertTrue( + frappe.db.sql( + """select * from `tabStock Ledger Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mr.name, + ) + ) - self.assertTrue(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mr.name)) + self.assertTrue( + frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mr.name, + ) + ) def test_material_issue_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") + make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) - mi = make_stock_entry(item_code="_Test Item", source="Stores - TCP1", company=company, - qty=40, expense_account="Stock Adjustment - TCP1") + mi = make_stock_entry( + item_code="_Test Item", + source="Stores - TCP1", + company=company, + qty=40, + expense_account="Stock Adjustment - TCP1", + ) - self.check_stock_ledger_entries("Stock Entry", mi.name, - [["_Test Item", "Stores - TCP1", -40.0]]) + self.check_stock_ledger_entries("Stock Entry", mi.name, [["_Test Item", "Stores - TCP1", -40.0]]) stock_in_hand_account = get_inventory_account(mi.company, "Stores - TCP1") - stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": mi.name}, "stock_value_difference")) + stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": mi.name}, + "stock_value_difference", + ) + ) - self.check_gl_entries("Stock Entry", mi.name, - sorted([ - [stock_in_hand_account, 0.0, stock_value_diff], - ["Stock Adjustment - TCP1", stock_value_diff, 0.0] - ]) + self.check_gl_entries( + "Stock Entry", + mi.name, + sorted( + [ + [stock_in_hand_account, 0.0, stock_value_diff], + ["Stock Adjustment - TCP1", stock_value_diff, 0.0], + ] + ), ) mi.cancel() def test_material_transfer_gl_entry(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - item_code = 'Hand Sanitizer - 001' - create_item(item_code =item_code, is_stock_item = 1, - is_purchase_item=1, opening_stock=1000, valuation_rate=10, company=company, warehouse="Stores - TCP1") + item_code = "Hand Sanitizer - 001" + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=1000, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) - mtn = make_stock_entry(item_code=item_code, source="Stores - TCP1", - target="Finished Goods - TCP1", qty=45, company=company) + mtn = make_stock_entry( + item_code=item_code, + source="Stores - TCP1", + target="Finished Goods - TCP1", + qty=45, + company=company, + ) - self.check_stock_ledger_entries("Stock Entry", mtn.name, - [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]]) + self.check_stock_ledger_entries( + "Stock Entry", + mtn.name, + [[item_code, "Stores - TCP1", -45.0], [item_code, "Finished Goods - TCP1", 45.0]], + ) source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) @@ -212,18 +278,33 @@ class TestStockEntry(FrappeTestCase): if source_warehouse_account == target_warehouse_account: # no gl entry as both source and target warehouse has linked to same account. - self.assertFalse(frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name, as_dict=1)) + self.assertFalse( + frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Stock Entry' and voucher_no=%s""", + mtn.name, + as_dict=1, + ) + ) else: - stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": mtn.name, "warehouse": "Stores - TCP1"}, "stock_value_difference")) + stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": mtn.name, "warehouse": "Stores - TCP1"}, + "stock_value_difference", + ) + ) - self.check_gl_entries("Stock Entry", mtn.name, - sorted([ - [source_warehouse_account, 0.0, stock_value_diff], - [target_warehouse_account, stock_value_diff, 0.0], - ]) + self.check_gl_entries( + "Stock Entry", + mtn.name, + sorted( + [ + [source_warehouse_account, 0.0, stock_value_diff], + [target_warehouse_account, stock_value_diff, 0.0], + ] + ), ) mtn.cancel() @@ -240,20 +321,23 @@ class TestStockEntry(FrappeTestCase): repack.items[0].transfer_qty = 100.0 repack.items[1].qty = 50.0 - repack.append("items", { - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - _TC", - "basic_rate": 150, - "item_code": "_Test Item 2", - "parentfield": "items", - "qty": 50.0, - "stock_uom": "_Test UOM", - "t_warehouse": "_Test Warehouse - _TC", - "transfer_qty": 50.0, - "uom": "_Test UOM" - }) + repack.append( + "items", + { + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - _TC", + "basic_rate": 150, + "item_code": "_Test Item 2", + "parentfield": "items", + "qty": 50.0, + "stock_uom": "_Test UOM", + "t_warehouse": "_Test Warehouse - _TC", + "transfer_qty": 50.0, + "uom": "_Test UOM", + }, + ) repack.set_stock_entry_type() repack.insert() @@ -266,12 +350,13 @@ class TestStockEntry(FrappeTestCase): # must raise error if 0 fg in repack entry self.assertRaises(FinishedGoodError, repack.validate_finished_goods) - repack.delete() # teardown + repack.delete() # teardown def test_repack_no_change_in_valuation(self): make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", - qty=50, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=50, basic_rate=100 + ) repack = frappe.copy_doc(test_records[3]) repack.posting_date = nowdate() @@ -280,76 +365,113 @@ class TestStockEntry(FrappeTestCase): repack.insert() repack.submit() - self.check_stock_ledger_entries("Stock Entry", repack.name, - [["_Test Item", "_Test Warehouse - _TC", -50.0], - ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1]]) + self.check_stock_ledger_entries( + "Stock Entry", + repack.name, + [ + ["_Test Item", "_Test Warehouse - _TC", -50.0], + ["_Test Item Home Desktop 100", "_Test Warehouse - _TC", 1], + ], + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Stock Entry' and voucher_no=%s - order by account desc""", repack.name, as_dict=1) + order by account desc""", + repack.name, + as_dict=1, + ) self.assertFalse(gl_entries) def test_repack_with_additional_costs(self): - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company= company, - qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1") + make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company=company, + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + ) - - repack = make_stock_entry(company = company, purpose="Repack", do_not_save=True) + repack = make_stock_entry(company=company, purpose="Repack", do_not_save=True) repack.posting_date = nowdate() repack.posting_time = nowtime() - expenses_included_in_valuation = frappe.get_value("Company", company, "expenses_included_in_valuation") + expenses_included_in_valuation = frappe.get_value( + "Company", company, "expenses_included_in_valuation" + ) items = get_multiple_items() repack.items = [] for item in items: repack.append("items", item) - repack.set("additional_costs", [ - { - "expense_account": expenses_included_in_valuation, - "description": "Actual Operating Cost", - "amount": 1000 - }, - { - "expense_account": expenses_included_in_valuation, - "description": "Additional Operating Cost", - "amount": 200 - }, - ]) + repack.set( + "additional_costs", + [ + { + "expense_account": expenses_included_in_valuation, + "description": "Actual Operating Cost", + "amount": 1000, + }, + { + "expense_account": expenses_included_in_valuation, + "description": "Additional Operating Cost", + "amount": 200, + }, + ], + ) repack.set_stock_entry_type() repack.insert() repack.submit() stock_in_hand_account = get_inventory_account(repack.company, repack.get("items")[1].t_warehouse) - rm_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": repack.name, "item_code": "_Test Item"}, "stock_value_difference")) + rm_stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Stock Entry", "voucher_no": repack.name, "item_code": "_Test Item"}, + "stock_value_difference", + ) + ) - fg_stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", - "voucher_no": repack.name, "item_code": "_Test Item Home Desktop 100"}, "stock_value_difference")) + fg_stock_value_diff = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + "item_code": "_Test Item Home Desktop 100", + }, + "stock_value_difference", + ) + ) stock_value_diff = flt(fg_stock_value_diff - rm_stock_value_diff, 2) self.assertEqual(stock_value_diff, 1200) - self.check_gl_entries("Stock Entry", repack.name, - sorted([ - [stock_in_hand_account, 1200, 0.0], - ["Expenses Included In Valuation - TCP1", 0.0, 1200.0] - ]) + self.check_gl_entries( + "Stock Entry", + repack.name, + sorted( + [[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]] + ), ) def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle): expected_sle.sort(key=lambda x: x[1]) # check stock ledger entries - sle = frappe.db.sql("""select item_code, warehouse, actual_qty + sle = frappe.db.sql( + """select item_code, warehouse, actual_qty from `tabStock Ledger Entry` where voucher_type = %s and voucher_no = %s order by item_code, warehouse, actual_qty""", - (voucher_type, voucher_no), as_list=1) + (voucher_type, voucher_no), + as_list=1, + ) self.assertTrue(sle) sle.sort(key=lambda x: x[1]) @@ -361,9 +483,13 @@ class TestStockEntry(FrappeTestCase): def check_gl_entries(self, voucher_type, voucher_no, expected_gl_entries): expected_gl_entries.sort(key=lambda x: x[0]) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account asc, debit asc""", (voucher_type, voucher_no), as_list=1) + order by account asc, debit asc""", + (voucher_type, voucher_no), + as_list=1, + ) self.assertTrue(gl_entries) gl_entries.sort(key=lambda x: x[0]) @@ -464,7 +590,7 @@ class TestStockEntry(FrappeTestCase): def test_serial_item_error(self): se, serial_nos = self.test_serial_by_series() - if not frappe.db.exists('Serial No', 'ABCD'): + if not frappe.db.exists("Serial No", "ABCD"): make_serialized_item(item_code="_Test Serialized Item", serial_no="ABCD\nEFGH") se = frappe.copy_doc(test_records[0]) @@ -494,10 +620,14 @@ class TestStockEntry(FrappeTestCase): se.set_stock_entry_type() se.insert() se.submit() - self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC") + self.assertTrue( + frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse 1 - _TC" + ) se.cancel() - self.assertTrue(frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC") + self.assertTrue( + frappe.db.get_value("Serial No", serial_no, "warehouse"), "_Test Warehouse - _TC" + ) def test_serial_warehouse_error(self): make_serialized_item(target_warehouse="_Test Warehouse 1 - _TC") @@ -525,14 +655,16 @@ class TestStockEntry(FrappeTestCase): self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) def test_warehouse_company_validation(self): - company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') - frappe.get_doc("User", "test2@example.com")\ - .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") + company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") + frappe.get_doc("User", "test2@example.com").add_roles( + "Sales User", "Sales Manager", "Stock User", "Stock Manager" + ) frappe.set_user("test2@example.com") from erpnext.stock.utils import InvalidWarehouseCompany + st1 = frappe.copy_doc(test_records[0]) - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" st1.set_stock_entry_type() st1.insert() self.assertRaises(InvalidWarehouseCompany, st1.submit) @@ -546,14 +678,15 @@ class TestStockEntry(FrappeTestCase): test_user.add_roles("Sales User", "Sales Manager", "Stock User") test_user.remove_roles("Stock Manager", "System Manager") - frappe.get_doc("User", "test2@example.com")\ - .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") + frappe.get_doc("User", "test2@example.com").add_roles( + "Sales User", "Sales Manager", "Stock User", "Stock Manager" + ) st1 = frappe.copy_doc(test_records[0]) st1.company = "_Test Company 1" frappe.set_user("test@example.com") - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" self.assertRaises(frappe.PermissionError, st1.insert) test_user.add_roles("System Manager") @@ -561,7 +694,7 @@ class TestStockEntry(FrappeTestCase): frappe.set_user("test2@example.com") st1 = frappe.copy_doc(test_records[0]) st1.company = "_Test Company 1" - st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" + st1.get("items")[0].t_warehouse = "_Test Warehouse 2 - _TC1" st1.get("items")[0].expense_account = "Stock Adjustment - _TC1" st1.get("items")[0].cost_center = "Main - _TC1" st1.set_stock_entry_type() @@ -575,14 +708,14 @@ class TestStockEntry(FrappeTestCase): remove_user_permission("Company", "_Test Company 1", "test2@example.com") def test_freeze_stocks(self): - frappe.db.set_value('Stock Settings', None,'stock_auth_role', '') + frappe.db.set_value("Stock Settings", None, "stock_auth_role", "") # test freeze_stocks_upto frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", add_days(nowdate(), 5)) se = frappe.copy_doc(test_records[0]).insert() self.assertRaises(StockFreezeError, se.submit) - frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", '') + frappe.db.set_value("Stock Settings", None, "stock_frozen_upto", "") # test freeze_stocks_upto_days frappe.db.set_value("Stock Settings", None, "stock_frozen_upto_days", -1) @@ -598,20 +731,24 @@ class TestStockEntry(FrappeTestCase): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) - bom_no, bom_operation_cost = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", - "is_default": 1, "docstatus": 1}, ["name", "operating_cost"]) + + bom_no, bom_operation_cost = frappe.db.get_value( + "BOM", {"item": "_Test FG Item 2", "is_default": 1, "docstatus": 1}, ["name", "operating_cost"] + ) work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test FG Item 2", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC", - "additional_operating_cost": 1000 - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item 2", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + "additional_operating_cost": 1000, + } + ) work_order.insert() work_order.submit() @@ -624,37 +761,41 @@ class TestStockEntry(FrappeTestCase): for d in stock_entry.get("items"): if d.item_code != "_Test FG Item 2": rm_cost += flt(d.amount) - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item 2", stock_entry.get("items")))[0].amount - self.assertEqual(fg_cost, - flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item 2", stock_entry.get("items")))[ + 0 + ].amount + self.assertEqual( + fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2) + ) def test_work_order_manufacture_with_material_consumption(self): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) + frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1") - bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", - "is_default": 1, "docstatus": 1}) + bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_default": 1, "docstatus": 1}) work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test FG Item", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC" - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test FG Item", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + } + ) work_order.insert() work_order.submit() - make_stock_entry(item_code="_Test Item", - target="Stores - _TC", qty=10, basic_rate=5000.0) - make_stock_entry(item_code="_Test Item Home Desktop 100", - target="Stores - _TC", qty=10, basic_rate=1000.0) - + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=5000.0) + make_stock_entry( + item_code="_Test Item Home Desktop 100", target="Stores - _TC", qty=10, basic_rate=1000.0 + ) s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1)) for d in s.get("items"): @@ -666,13 +807,12 @@ class TestStockEntry(FrappeTestCase): s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1)) s.save() rm_cost = 0 - for d in s.get('items'): + for d in s.get("items"): if d.s_warehouse: rm_cost += d.amount - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount - self.assertEqual(fg_cost, - flt(rm_cost - scrap_cost, 2)) + self.assertEqual(fg_cost, flt(rm_cost - scrap_cost, 2)) # When Stock Entry has only FG + Scrap s.items.pop(0) @@ -680,31 +820,34 @@ class TestStockEntry(FrappeTestCase): s.submit() rm_cost = 0 - for d in s.get('items'): + for d in s.get("items"): if d.s_warehouse: rm_cost += d.amount self.assertEqual(rm_cost, 0) expected_fg_cost = s.get_basic_rate_for_manufactured_item(1) - fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount + fg_cost = list(filter(lambda x: x.item_code == "_Test FG Item", s.get("items")))[0].amount self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2)) def test_variant_work_order(self): - bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", - "is_default": 1, "docstatus": 1}) + bom_no = frappe.db.get_value( + "BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1} + ) - make_item_variant() # make variant of _Test Variant Item if absent + make_item_variant() # make variant of _Test Variant Item if absent work_order = frappe.new_doc("Work Order") - work_order.update({ - "company": "_Test Company", - "fg_warehouse": "_Test Warehouse 1 - _TC", - "production_item": "_Test Variant Item-S", - "bom_no": bom_no, - "qty": 1.0, - "stock_uom": "_Test UOM", - "wip_warehouse": "_Test Warehouse - _TC", - "skip_transfer": 1 - }) + work_order.update( + { + "company": "_Test Company", + "fg_warehouse": "_Test Warehouse 1 - _TC", + "production_item": "_Test Variant Item-S", + "bom_no": bom_no, + "qty": 1.0, + "stock_uom": "_Test UOM", + "wip_warehouse": "_Test Warehouse - _TC", + "skip_transfer": 1, + } + ) work_order.insert() work_order.submit() @@ -718,19 +861,29 @@ class TestStockEntry(FrappeTestCase): s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC") serial_nos = s1.get("items")[0].serial_no - s2 = make_stock_entry(item_code="_Test Serialized Item With Series", source="_Test Warehouse - _TC", - qty=2, basic_rate=100, purpose="Repack", serial_no=serial_nos, do_not_save=True) + s2 = make_stock_entry( + item_code="_Test Serialized Item With Series", + source="_Test Warehouse - _TC", + qty=2, + basic_rate=100, + purpose="Repack", + serial_no=serial_nos, + do_not_save=True, + ) - s2.append("items", { - "item_code": "_Test Serialized Item", - "t_warehouse": "_Test Warehouse - _TC", - "qty": 2, - "basic_rate": 120, - "expense_account": "Stock Adjustment - _TC", - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "serial_no": serial_nos - }) + s2.append( + "items", + { + "item_code": "_Test Serialized Item", + "t_warehouse": "_Test Warehouse - _TC", + "qty": 2, + "basic_rate": 120, + "expense_account": "Stock Adjustment - _TC", + "conversion_factor": 1.0, + "cost_center": "_Test Cost Center - _TC", + "serial_no": serial_nos, + }, + ) s2.submit() s2.cancel() @@ -740,10 +893,15 @@ class TestStockEntry(FrappeTestCase): from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse create_warehouse("Test Warehouse for Sample Retention") - frappe.db.set_value("Stock Settings", None, "sample_retention_warehouse", "Test Warehouse for Sample Retention - _TC") + frappe.db.set_value( + "Stock Settings", + None, + "sample_retention_warehouse", + "Test Warehouse for Sample Retention - _TC", + ) test_item_code = "Retain Sample Item" - if not frappe.db.exists('Item', test_item_code): + if not frappe.db.exists("Item", test_item_code): item = frappe.new_doc("Item") item.item_code = test_item_code item.item_name = "Retain Sample Item" @@ -759,44 +917,58 @@ class TestStockEntry(FrappeTestCase): receipt_entry = frappe.new_doc("Stock Entry") receipt_entry.company = "_Test Company" receipt_entry.purpose = "Material Receipt" - receipt_entry.append("items", { - "item_code": test_item_code, - "t_warehouse": "_Test Warehouse - _TC", - "qty": 40, - "basic_rate": 12, - "cost_center": "_Test Cost Center - _TC", - "sample_quantity": 4 - }) + receipt_entry.append( + "items", + { + "item_code": test_item_code, + "t_warehouse": "_Test Warehouse - _TC", + "qty": 40, + "basic_rate": 12, + "cost_center": "_Test Cost Center - _TC", + "sample_quantity": 4, + }, + ) receipt_entry.set_stock_entry_type() receipt_entry.insert() receipt_entry.submit() - retention_data = move_sample_to_retention_warehouse(receipt_entry.company, receipt_entry.get("items")) + retention_data = move_sample_to_retention_warehouse( + receipt_entry.company, receipt_entry.get("items") + ) retention_entry = frappe.new_doc("Stock Entry") retention_entry.company = retention_data.company retention_entry.purpose = retention_data.purpose - retention_entry.append("items", { - "item_code": test_item_code, - "t_warehouse": "Test Warehouse for Sample Retention - _TC", - "s_warehouse": "_Test Warehouse - _TC", - "qty": 4, - "basic_rate": 12, - "cost_center": "_Test Cost Center - _TC", - "batch_no": receipt_entry.get("items")[0].batch_no - }) + retention_entry.append( + "items", + { + "item_code": test_item_code, + "t_warehouse": "Test Warehouse for Sample Retention - _TC", + "s_warehouse": "_Test Warehouse - _TC", + "qty": 4, + "basic_rate": 12, + "cost_center": "_Test Cost Center - _TC", + "batch_no": receipt_entry.get("items")[0].batch_no, + }, + ) retention_entry.set_stock_entry_type() retention_entry.insert() retention_entry.submit() - qty_in_usable_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item") - qty_in_retention_warehouse = get_batch_qty(receipt_entry.get("items")[0].batch_no, "Test Warehouse for Sample Retention - _TC", "_Test Item") + qty_in_usable_warehouse = get_batch_qty( + receipt_entry.get("items")[0].batch_no, "_Test Warehouse - _TC", "_Test Item" + ) + qty_in_retention_warehouse = get_batch_qty( + receipt_entry.get("items")[0].batch_no, + "Test Warehouse for Sample Retention - _TC", + "_Test Item", + ) self.assertEqual(qty_in_usable_warehouse, 36) self.assertEqual(qty_in_retention_warehouse, 4) def test_quality_check(self): item_code = "_Test Item For QC" - if not frappe.db.exists('Item', item_code): + if not frappe.db.exists("Item", item_code): create_item(item_code) repack = frappe.copy_doc(test_records[3]) @@ -850,44 +1022,63 @@ class TestStockEntry(FrappeTestCase): # self.assertEqual(item_quantity.get(d.item_code), d.qty) def test_customer_provided_parts_se(self): - create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) - se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', - qty=4, to_warehouse = "_Test Warehouse - _TC") + create_item( + "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) + se = make_stock_entry( + item_code="CUST-0987", purpose="Material Receipt", qty=4, to_warehouse="_Test Warehouse - _TC" + ) self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) def test_zero_incoming_rate(self): - """ Make sure incoming rate of 0 is allowed while consuming. + """Make sure incoming rate of 0 is allowed while consuming. - qty | rate | valuation rate - 1 | 100 | 100 - 1 | 0 | 50 - -1 | 100 | 0 - -1 | 0 <--- assert this + qty | rate | valuation rate + 1 | 100 | 100 + 1 | 0 | 50 + -1 | 100 | 0 + -1 | 0 <--- assert this """ item_code = "_TestZeroVal" warehouse = "_Test Warehouse - _TC" - create_item('_TestZeroVal') + create_item("_TestZeroVal") _receipt = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=100) - receipt2 = make_stock_entry(item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True) + receipt2 = make_stock_entry( + item_code=item_code, qty=1, to_warehouse=warehouse, rate=0, do_not_save=True + ) receipt2.items[0].allow_zero_valuation_rate = 1 receipt2.save() receipt2.submit() issue = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) - value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": issue.name, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) self.assertEqual(value_diff, -100) issue2 = make_stock_entry(item_code=item_code, qty=1, from_warehouse=warehouse) - value_diff = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, "stock_value_difference") + value_diff = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": issue2.name, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) self.assertEqual(value_diff, 0) - def test_gle_for_opening_stock_entry(self): - mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", - company="_Test Company with perpetual inventory", qty=50, basic_rate=100, - expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) + mr = make_stock_entry( + item_code="_Test Item", + target="Stores - TCP1", + company="_Test Company with perpetual inventory", + qty=50, + basic_rate=100, + expense_account="Stock Adjustment - TCP1", + is_opening="Yes", + do_not_save=True, + ) self.assertRaises(OpeningEntryAccountError, mr.save) @@ -896,52 +1087,61 @@ class TestStockEntry(FrappeTestCase): mr.save() mr.submit() - is_opening = frappe.db.get_value("GL Entry", - filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, fieldname="is_opening") + is_opening = frappe.db.get_value( + "GL Entry", + filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, + fieldname="is_opening", + ) self.assertEqual(is_opening, "Yes") def test_total_basic_amount_zero(self): - se = frappe.get_doc({"doctype":"Stock Entry", - "purpose":"Material Receipt", - "stock_entry_type":"Material Receipt", - "posting_date": nowdate(), - "company":"_Test Company with perpetual inventory", - "items":[ - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 1, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - { - "item_code":"_Test Item", - "description":"_Test Item", - "qty": 2, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1" - }, - ], - "additional_costs":[ - {"expense_account":"Miscellaneous Expenses - TCP1", - "amount":100, - "description": "miscellanous" - }] - }) + se = frappe.get_doc( + { + "doctype": "Stock Entry", + "purpose": "Material Receipt", + "stock_entry_type": "Material Receipt", + "posting_date": nowdate(), + "company": "_Test Company with perpetual inventory", + "items": [ + { + "item_code": "_Test Item", + "description": "_Test Item", + "qty": 1, + "basic_rate": 0, + "uom": "Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1", + }, + { + "item_code": "_Test Item", + "description": "_Test Item", + "qty": 2, + "basic_rate": 0, + "uom": "Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1", + }, + ], + "additional_costs": [ + { + "expense_account": "Miscellaneous Expenses - TCP1", + "amount": 100, + "description": "miscellanous", + } + ], + } + ) se.insert() se.submit() - self.check_gl_entries("Stock Entry", se.name, - sorted([ - ["Stock Adjustment - TCP1", 100.0, 0.0], - ["Miscellaneous Expenses - TCP1", 0.0, 100.0] - ]) + self.check_gl_entries( + "Stock Entry", + se.name, + sorted( + [["Stock Adjustment - TCP1", 100.0, 0.0], ["Miscellaneous Expenses - TCP1", 0.0, 100.0]] + ), ) def test_conversion_factor_change(self): @@ -972,15 +1172,15 @@ class TestStockEntry(FrappeTestCase): def test_additional_cost_distribution_manufacture(self): se = frappe.get_doc( - doctype="Stock Entry", - purpose="Manufacture", - additional_costs=[frappe._dict(base_amount=100)], - items=[ - frappe._dict(item_code="RM", basic_amount=10), - frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1), - frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X") - ], - ) + doctype="Stock Entry", + purpose="Manufacture", + additional_costs=[frappe._dict(base_amount=100)], + items=[ + frappe._dict(item_code="RM", basic_amount=10), + frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1), + frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X"), + ], + ) se.distribute_additional_costs() @@ -989,14 +1189,14 @@ class TestStockEntry(FrappeTestCase): def test_additional_cost_distribution_non_manufacture(self): se = frappe.get_doc( - doctype="Stock Entry", - purpose="Material Receipt", - additional_costs=[frappe._dict(base_amount=100)], - items=[ - frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"), - frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X") - ], - ) + doctype="Stock Entry", + purpose="Material Receipt", + additional_costs=[frappe._dict(base_amount=100)], + items=[ + frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"), + frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X"), + ], + ) se.distribute_additional_costs() @@ -1006,40 +1206,42 @@ class TestStockEntry(FrappeTestCase): @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' - ] + 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' + 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, + 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, + 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, + 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') + posting_date="2021-07-02", # Illegal SE + purpose="Material Transfer", + ), ] self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries) @@ -1049,36 +1251,38 @@ class TestStockEntry(FrappeTestCase): from erpnext.stock.doctype.batch.test_batch import TestBatch # Initialize item, batch, warehouse, opening qty - item_code = '_Test MultiBatch Item' + item_code = "_Test MultiBatch Item" TestBatch.make_batch_item(item_code) - batch_nos = [] # store generate batches - warehouse = '_Test Warehouse - _TC' + 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' - ) + 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' - ) + 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, + 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') + posting_date="2021-09-02", # backdated consumption of 2nd batch + purpose="Material Issue", + ) def test_independent_manufacture_entry(self): "Test FG items and incoming rate calculation in Maniufacture Entry without WO or BOM linked." @@ -1088,9 +1292,11 @@ class TestStockEntry(FrappeTestCase): stock_entry_type="Manufacture", company="_Test Company", items=[ - frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), - frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC") - ] + frappe._dict( + item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC" + ), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC"), + ], ) # SE must have atleast one FG self.assertRaises(FinishedGoodError, se.save) @@ -1105,10 +1311,11 @@ class TestStockEntry(FrappeTestCase): # Check if FG cost is calculated based on RM total cost # RM total cost = 200, FG rate = 200/4(FG qty) = 50 - self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4)) + self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate / 4)) self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.total_incoming_value, se.total_outgoing_value) + def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) @@ -1138,50 +1345,57 @@ def make_serialized_item(**args): se.submit() return se + def get_qty_after_transaction(**args): args = frappe._dict(args) - last_sle = get_previous_sle({ - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "posting_date": args.posting_date or nowdate(), - "posting_time": args.posting_time or nowtime() - }) + last_sle = get_previous_sle( + { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "posting_date": args.posting_date or nowdate(), + "posting_time": args.posting_time or nowtime(), + } + ) return flt(last_sle.get("qty_after_transaction")) + def get_multiple_items(): return [ - { - "conversion_factor": 1.0, - "cost_center": "Main - TCP1", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - TCP1", - "basic_rate": 100, - "item_code": "_Test Item", - "qty": 50.0, - "s_warehouse": "Stores - TCP1", - "stock_uom": "_Test UOM", - "transfer_qty": 50.0, - "uom": "_Test UOM" - }, - { - "conversion_factor": 1.0, - "cost_center": "Main - TCP1", - "doctype": "Stock Entry Detail", - "expense_account": "Stock Adjustment - TCP1", - "basic_rate": 5000, - "item_code": "_Test Item Home Desktop 100", - "qty": 1, - "stock_uom": "_Test UOM", - "t_warehouse": "Stores - TCP1", - "transfer_qty": 1, - "uom": "_Test UOM" - } - ] + { + "conversion_factor": 1.0, + "cost_center": "Main - TCP1", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - TCP1", + "basic_rate": 100, + "item_code": "_Test Item", + "qty": 50.0, + "s_warehouse": "Stores - TCP1", + "stock_uom": "_Test UOM", + "transfer_qty": 50.0, + "uom": "_Test UOM", + }, + { + "conversion_factor": 1.0, + "cost_center": "Main - TCP1", + "doctype": "Stock Entry Detail", + "expense_account": "Stock Adjustment - TCP1", + "basic_rate": 5000, + "item_code": "_Test Item Home Desktop 100", + "qty": 1, + "stock_uom": "_Test UOM", + "t_warehouse": "Stores - TCP1", + "transfer_qty": 1, + "uom": "_Test UOM", + }, + ] + + +test_records = frappe.get_test_records("Stock Entry") -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): + 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, @@ -1192,9 +1406,9 @@ def initialize_records_for_future_negative_sle_test( 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', + purpose="Opening Stock", posting_date=posting_date, - posting_time='20:00:20', + posting_time="20:00:20", item_code=item_code, warehouse=warehouse_names[0], valuation_rate=100, diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index efd97c04ac6..7258cfbe2c9 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -8,5 +8,5 @@ from frappe.model.document import Document class StockEntryType(Document): def validate(self): - if self.add_to_transit and self.purpose != 'Material Transfer': + if self.add_to_transit and self.purpose != "Material Transfer": self.add_to_transit = 0 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 29a0d078692..5c1da420e24 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -15,11 +14,17 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock -class StockFreezeError(frappe.ValidationError): pass -class BackDatedStockTransaction(frappe.ValidationError): pass +class StockFreezeError(frappe.ValidationError): + pass + + +class BackDatedStockTransaction(frappe.ValidationError): + pass + exclude_from_linked_with = True + class StockLedgerEntry(Document): def autoname(self): """ @@ -33,6 +38,7 @@ class StockLedgerEntry(Document): def validate(self): self.flags.ignore_submit_comment = True from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company + self.validate_mandatory() self.validate_item() self.validate_batch() @@ -43,24 +49,29 @@ class StockLedgerEntry(Document): self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() - def on_submit(self): self.check_stock_frozen_date() self.calculate_batch_qty() if not self.get("via_landed_cost_voucher"): from erpnext.stock.doctype.serial_no.serial_no import process_serial_no + process_serial_no(self) def calculate_batch_qty(self): if self.batch_no: - batch_qty = frappe.db.get_value("Stock Ledger Entry", - {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0}, - "sum(actual_qty)") or 0 + batch_qty = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0}, + "sum(actual_qty)", + ) + or 0 + ) frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) def validate_mandatory(self): - mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] + mandatory = ["warehouse", "posting_date", "voucher_type", "voucher_no", "company"] for k in mandatory: if not self.get(k): frappe.throw(_("{0} is required").format(self.meta.get_label(k))) @@ -69,9 +80,13 @@ class StockLedgerEntry(Document): frappe.throw(_("Actual Qty is mandatory")) def validate_item(self): - item_det = frappe.db.sql("""select name, item_name, has_batch_no, docstatus, + item_det = frappe.db.sql( + """select name, item_name, has_batch_no, docstatus, is_stock_item, has_variants, stock_uom, create_new_batch - from tabItem where name=%s""", self.item_code, as_dict=True) + from tabItem where name=%s""", + self.item_code, + as_dict=True, + ) if not item_det: frappe.throw(_("Item {0} not found").format(self.item_code)) @@ -83,39 +98,58 @@ class StockLedgerEntry(Document): # check if batch number is valid if item_det.has_batch_no == 1: - batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name + batch_item = ( + self.item_code + if self.item_code == item_det.item_name + else self.item_code + ":" + item_det.item_name + ) if not self.batch_no: frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) - elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): - frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) + elif not frappe.db.get_value("Batch", {"item": self.item_code, "name": self.batch_no}): + frappe.throw( + _("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item) + ) elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0: frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) if item_det.has_variants: - frappe.throw(_("Stock cannot exist for Item {0} since has variants").format(self.item_code), - ItemTemplateCannotHaveStock) + frappe.throw( + _("Stock cannot exist for Item {0} since has variants").format(self.item_code), + ItemTemplateCannotHaveStock, + ) self.stock_uom = item_det.stock_uom def check_stock_frozen_date(self): - stock_settings = frappe.get_cached_doc('Stock Settings') + stock_settings = frappe.get_cached_doc("Stock Settings") if stock_settings.stock_frozen_upto: - if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) - and stock_settings.stock_auth_role not in frappe.get_roles()): - frappe.throw(_("Stock transactions before {0} are frozen") - .format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError) + if ( + getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto) + and stock_settings.stock_auth_role not in frappe.get_roles() + ): + frappe.throw( + _("Stock transactions before {0} are frozen").format( + formatdate(stock_settings.stock_frozen_upto) + ), + StockFreezeError, + ) stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days) if stock_frozen_upto_days: - older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()) + older_than_x_days_ago = ( + add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today() + ) if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles(): - frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError) + frappe.throw( + _("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), + StockFreezeError, + ) def scrub_posting_time(self): - if not self.posting_time or self.posting_time == '00:0': - self.posting_time = '00:00' + if not self.posting_time or self.posting_time == "00:0": + self.posting_time = "00:00" def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": @@ -129,43 +163,61 @@ class StockLedgerEntry(Document): self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0] else: from erpnext.accounts.utils import validate_fiscal_year - validate_fiscal_year(self.posting_date, self.fiscal_year, self.company, - self.meta.get_label("posting_date"), self) + + validate_fiscal_year( + self.posting_date, self.fiscal_year, self.company, self.meta.get_label("posting_date"), self + ) def block_transactions_against_group_warehouse(self): from erpnext.stock.utils import is_group_warehouse + is_group_warehouse(self.warehouse) def validate_with_last_transaction_posting_time(self): - authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions") + authorized_role = frappe.db.get_single_value( + "Stock Settings", "role_allowed_to_create_edit_back_dated_transactions" + ) if authorized_role: authorized_users = get_users(authorized_role) if authorized_users and frappe.session.user not in authorized_users: - last_transaction_time = frappe.db.sql(""" + last_transaction_time = frappe.db.sql( + """ select MAX(timestamp(posting_date, posting_time)) as posting_time from `tabStock Ledger Entry` where docstatus = 1 and is_cancelled = 0 and item_code = %s - and warehouse = %s""", (self.item_code, self.warehouse))[0][0] + and warehouse = %s""", + (self.item_code, self.warehouse), + )[0][0] - cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") + cur_doc_posting_datetime = "%s %s" % ( + self.posting_date, + self.get("posting_time") or "00:00:00", + ) - if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time): - msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code), - frappe.bold(self.warehouse), frappe.bold(last_transaction_time)) + if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime( + last_transaction_time + ): + msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format( + frappe.bold(self.item_code), frappe.bold(self.warehouse), frappe.bold(last_transaction_time) + ) - msg += "

    " + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format( - frappe.bold(self.item_code), frappe.bold(self.warehouse)) + msg += "

    " + _( + "You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time." + ).format(frappe.bold(self.item_code), frappe.bold(self.warehouse)) msg += "

    " + _("Please contact any of the following users to {} this transaction.") msg += "
    " + "
    ".join(authorized_users) frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry")) + def on_doctype_update(): - if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'): + if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): frappe.db.commit() - frappe.db.add_index("Stock Ledger Entry", + frappe.db.add_index( + "Stock Ledger Entry", fields=["posting_date", "posting_time", "name"], - index_name="posting_sort_index") + index_name="posting_sort_index", + ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 1c3e0bfc229..eee6a3fb9ec 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -27,21 +27,30 @@ from erpnext.stock.stock_ledger import get_previous_sle class TestStockLedgerEntry(FrappeTestCase): def setUp(self): items = create_items() - reset('Stock Entry') + reset("Stock Entry") # delete SLE and BINs for all items - frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) - frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items) - + frappe.db.sql( + "delete from `tabStock Ledger Entry` where item_code in (%s)" + % (", ".join(["%s"] * len(items))), + items, + ) + frappe.db.sql( + "delete from `tabBin` where item_code in (%s)" % (", ".join(["%s"] * len(items))), items + ) def assertSLEs(self, doc, expected_sles, sle_filters=None): - """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + """Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} if sle_filters: filters.update(sle_filters) - sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, - order_by="timestamp(posting_date, posting_time), creation") + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["*"], + filters=filters, + order_by="timestamp(posting_date, posting_time), creation", + ) for exp_sle, act_sle in zip(expected_sles, sles): for k, v in exp_sle.items(): @@ -64,9 +73,11 @@ class TestStockLedgerEntry(FrappeTestCase): qty=50, rate=100, company=company, - expense_account = "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-10', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-10", + posting_time="14:00", ) # _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200 @@ -76,9 +87,11 @@ class TestStockLedgerEntry(FrappeTestCase): qty=10, rate=200, company=company, - expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-20', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-20", + posting_time="14:00", ) # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020 @@ -88,28 +101,40 @@ class TestStockLedgerEntry(FrappeTestCase): target="Finished Goods - _TC", company=company, qty=10, - expense_account="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-30', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-30", + posting_time="14:00", + ) + target_wh_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": se.name, + }, + ["valuation_rate"], + as_dict=1, ) - target_wh_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": se.name - }, ["valuation_rate"], as_dict=1) self.assertEqual(target_wh_sle.get("valuation_rate"), 150) # Repack entry on 5-5-2020 - repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00') + repack = create_repack_entry(company=company, posting_date="2020-05-05", posting_time="14:00") - finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Finished Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": repack.name - }, ["incoming_rate", "valuation_rate"], as_dict=1) + finished_item_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + }, + ["incoming_rate", "valuation_rate"], + as_dict=1, + ) self.assertEqual(finished_item_sle.get("incoming_rate"), 540) self.assertEqual(finished_item_sle.get("valuation_rate"), 540) @@ -120,29 +145,37 @@ class TestStockLedgerEntry(FrappeTestCase): qty=50, rate=150, company=company, - expense_account ="Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC", - posting_date='2020-04-12', - posting_time='14:00' + expense_account="Stock Adjustment - _TC" + if frappe.get_all("Stock Ledger Entry") + else "Temporary Opening - _TC", + posting_date="2020-04-12", + posting_time="14:00", ) - # Check valuation rate of finished goods warehouse after back-dated entry at Stores - target_wh_sle = get_previous_sle({ - "item_code": "_Test Item for Reposting", - "warehouse": "Finished Goods - _TC", - "posting_date": '2020-04-30', - "posting_time": '14:00' - }) + target_wh_sle = get_previous_sle( + { + "item_code": "_Test Item for Reposting", + "warehouse": "Finished Goods - _TC", + "posting_date": "2020-04-30", + "posting_time": "14:00", + } + ) self.assertEqual(target_wh_sle.get("incoming_rate"), 150) self.assertEqual(target_wh_sle.get("valuation_rate"), 175) # Check valuation rate of repacked item after back-dated entry at Stores - finished_item_sle = frappe.db.get_value('Stock Ledger Entry', { - "item_code": "_Test Finished Item for Reposting", - "warehouse": "Finished Goods - _TC", - "voucher_type": "Stock Entry", - "voucher_no": repack.name - }, ["incoming_rate", "valuation_rate"], as_dict=1) + finished_item_sle = frappe.db.get_value( + "Stock Ledger Entry", + { + "item_code": "_Test Finished Item for Reposting", + "warehouse": "Finished Goods - _TC", + "voucher_type": "Stock Entry", + "voucher_no": repack.name, + }, + ["incoming_rate", "valuation_rate"], + as_dict=1, + ) self.assertEqual(finished_item_sle.get("incoming_rate"), 790) self.assertEqual(finished_item_sle.get("valuation_rate"), 790) @@ -152,76 +185,133 @@ class TestStockLedgerEntry(FrappeTestCase): self.assertEqual(repack.items[1].get("basic_rate"), 750) def test_purchase_return_valuation_reposting(self): - pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10', - warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100) + pr = make_purchase_receipt( + company="_Test Company", + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code="_Test Item for Reposting", + qty=5, + rate=100, + ) - return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15', - warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2) + return_pr = make_purchase_receipt( + company="_Test Company", + posting_date="2020-04-15", + warehouse="Stores - _TC", + item_code="_Test Item for Reposting", + is_return=1, + return_against=pr.name, + qty=-2, + ) # check sle - outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"]) + outgoing_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, + ["outgoing_rate", "stock_value_difference"], + ) self.assertEqual(outgoing_rate, 100) self.assertEqual(stock_value_difference, -200) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"]) + outgoing_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": return_pr.name}, + ["outgoing_rate", "stock_value_difference"], + ) self.assertEqual(outgoing_rate, 110) self.assertEqual(stock_value_difference, -220) def test_sales_return_valuation_reposting(self): company = "_Test Company" - item_code="_Test Item for Reposting" + item_code = "_Test Item for Reposting" # Purchase Return: Qty = 5, Rate = 100 - pr = make_purchase_receipt(company=company, posting_date='2020-04-10', - warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100) + pr = make_purchase_receipt( + company=company, + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code=item_code, + qty=5, + rate=100, + ) - #Delivery Note: Qty = 5, Rate = 150 - dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC", - company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + # Delivery Note: Qty = 5, Rate = 150 + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=150, + warehouse="Stores - _TC", + company=company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check outgoing_rate for DN - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 5) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 5 + ) self.assertEqual(dn.items[0].incoming_rate, 100) self.assertEqual(outgoing_rate, 100) # Return Entry: Qty = -2, Rate = 150 - return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150, - company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + return_dn = create_delivery_note( + is_return=1, + return_against=dn.name, + item_code=item_code, + qty=-2, + rate=150, + company=company, + warehouse="Stores - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check incoming rate for Return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(return_dn.items[0].incoming_rate, 100) self.assertEqual(incoming_rate, 100) self.assertEqual(stock_value_difference, 200) - #------------------------------- + # ------------------------------- # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50 lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) # check outgoing_rate for DN after reposting - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 5) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 5 + ) self.assertEqual(outgoing_rate, 110) dn.reload() self.assertEqual(dn.items[0].incoming_rate, 110) # check incoming rate for Return entry after reposting - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 110) self.assertEqual(stock_value_difference, 220) @@ -237,55 +327,93 @@ class TestStockLedgerEntry(FrappeTestCase): def test_reposting_of_sales_return_for_packed_item(self): company = "_Test Company" - packed_item_code="_Test Item for Reposting" + packed_item_code = "_Test Item for Reposting" bundled_item = "_Test Bundled Item for Reposting" create_product_bundle_item(bundled_item, [[packed_item_code, 4]]) # Purchase Return: Qty = 50, Rate = 100 - pr = make_purchase_receipt(company=company, posting_date='2020-04-10', - warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100) + pr = make_purchase_receipt( + company=company, + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code=packed_item_code, + qty=50, + rate=100, + ) - #Delivery Note: Qty = 5, Rate = 150 - dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC", - company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + # Delivery Note: Qty = 5, Rate = 150 + dn = create_delivery_note( + item_code=bundled_item, + qty=5, + rate=150, + warehouse="Stores - _TC", + company=company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check outgoing_rate for DN - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 20) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 20 + ) self.assertEqual(dn.packed_items[0].incoming_rate, 100) self.assertEqual(outgoing_rate, 100) # Return Entry: Qty = -2, Rate = 150 - return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150, - company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC") + return_dn = create_delivery_note( + is_return=1, + return_against=dn.name, + item_code=bundled_item, + qty=-2, + rate=150, + company=company, + warehouse="Stores - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + ) # check incoming rate for Return entry - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(return_dn.packed_items[0].incoming_rate, 100) self.assertEqual(incoming_rate, 100) self.assertEqual(stock_value_difference, 800) - #------------------------------- + # ------------------------------- # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50 lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) # check outgoing_rate for DN after reposting - outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note", - "voucher_no": dn.name}, "stock_value_difference") / 20) + outgoing_rate = abs( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Delivery Note", "voucher_no": dn.name}, + "stock_value_difference", + ) + / 20 + ) self.assertEqual(outgoing_rate, 101) dn.reload() self.assertEqual(dn.packed_items[0].incoming_rate, 101) # check incoming rate for Return entry after reposting - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": return_dn.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(incoming_rate, 101) self.assertEqual(stock_value_difference, 808) @@ -303,20 +431,35 @@ class TestStockLedgerEntry(FrappeTestCase): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom company = "_Test Company" - rm_item_code="_Test Item for Reposting" + rm_item_code = "_Test Item for Reposting" subcontracted_item = "_Test Subcontracted Item for Reposting" - frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") - make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR") + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" + ) + make_bom(item=subcontracted_item, raw_materials=[rm_item_code], currency="INR") # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100 - pr = make_purchase_receipt(company=company, posting_date='2020-04-10', - warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100) + pr = make_purchase_receipt( + company=company, + posting_date="2020-04-10", + warehouse="Stores - _TC", + item_code=rm_item_code, + qty=10, + rate=100, + ) # Purchase Receipt for subcontracted item - pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20', - warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC", - item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes") + pr1 = make_purchase_receipt( + company=company, + posting_date="2020-04-20", + warehouse="Finished Goods - _TC", + supplier_warehouse="Stores - _TC", + item_code=subcontracted_item, + qty=10, + rate=20, + is_subcontracted="Yes", + ) self.assertEqual(pr1.items[0].valuation_rate, 120) @@ -327,8 +470,11 @@ class TestStockLedgerEntry(FrappeTestCase): self.assertEqual(pr1.items[0].valuation_rate, 125) # check outgoing_rate for DN after reposting - incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", - "voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate") + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "item_code": subcontracted_item}, + "incoming_rate", + ) self.assertEqual(incoming_rate, 125) # cleanup data @@ -338,8 +484,9 @@ class TestStockLedgerEntry(FrappeTestCase): def test_back_dated_entry_not_allowed(self): # Back dated stock transactions are only allowed to stock managers - frappe.db.set_value("Stock Settings", None, - "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager") + frappe.db.set_value( + "Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager" + ) # Set User with Stock User role but not Stock Manager try: @@ -350,8 +497,13 @@ class TestStockLedgerEntry(FrappeTestCase): frappe.set_user(user.name) stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) - back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, - posting_date=add_days(today(), -1), do_not_submit=True) + back_dated_se_1 = make_stock_entry( + target="_Test Warehouse - _TC", + qty=10, + basic_rate=100, + posting_date=add_days(today(), -1), + do_not_submit=True, + ) # Block back-dated entry self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit) @@ -361,14 +513,17 @@ class TestStockLedgerEntry(FrappeTestCase): frappe.set_user(user.name) # Back dated entry allowed to Stock Manager - back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, - posting_date=add_days(today(), -1)) + back_dated_se_2 = make_stock_entry( + target="_Test Warehouse - _TC", qty=10, basic_rate=100, posting_date=add_days(today(), -1) + ) back_dated_se_2.cancel() stock_entry_on_today.cancel() finally: - frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None) + frappe.db.set_value( + "Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None + ) frappe.set_user("Administrator") user.remove_roles("Stock Manager") @@ -390,12 +545,12 @@ class TestStockLedgerEntry(FrappeTestCase): expected_queues = [] for idx, rate in enumerate(rates, start=1): - expected_queues.append( - {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]} - ) + expected_queues.append({"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}) self.assertSLEs(receipt, expected_queues) - transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10) + transfer = make_stock_entry( + item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10 + ) for rate in rates[1:]: row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) transfer.append("items", row) @@ -413,7 +568,9 @@ class TestStockLedgerEntry(FrappeTestCase): rates = [10 * i for i in range(1, 5)] - receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10) + receipt = make_stock_entry( + item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10 + ) for rate in rates[1:]: row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) row.basic_rate = rate @@ -422,26 +579,30 @@ class TestStockLedgerEntry(FrappeTestCase): receipt.save() receipt.submit() - repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10, - do_not_save=True, rate=10, purpose="Repack") + repack = make_stock_entry( + item_code=rm.name, source=warehouse, qty=10, do_not_save=True, rate=10, purpose="Repack" + ) for rate in rates[1:]: row = frappe.copy_doc(repack.items[0], ignore_no_copy=False) repack.append("items", row) - repack.append("items", { - "item_code": packed.name, - "t_warehouse": warehouse, - "qty": 1, - "transfer_qty": 1, - }) + repack.append( + "items", + { + "item_code": packed.name, + "t_warehouse": warehouse, + "qty": 1, + "transfer_qty": 1, + }, + ) repack.save() repack.submit() # same exact queue should be transferred - self.assertSLEs(repack, [ - {"incoming_rate": sum(rates) * 10} - ], sle_filters={"item_code": packed.name}) + self.assertSLEs( + repack, [{"incoming_rate": sum(rates) * 10}], sle_filters={"item_code": packed.name} + ) @change_settings("Stock Settings", {"allow_negative_stock": 1}) def test_negative_fifo_valuation(self): @@ -455,19 +616,14 @@ class TestStockLedgerEntry(FrappeTestCase): receipt = make_stock_entry(item_code=item, target=warehouse, qty=10, rate=10) consume1 = make_stock_entry(item_code=item, source=warehouse, qty=15) - self.assertSLEs(consume1, [ - {"stock_value": -5 * 10, "stock_queue": [[-5, 10]]} - ]) + self.assertSLEs(consume1, [{"stock_value": -5 * 10, "stock_queue": [[-5, 10]]}]) consume2 = make_stock_entry(item_code=item, source=warehouse, qty=5) - self.assertSLEs(consume2, [ - {"stock_value": -10 * 10, "stock_queue": [[-10, 10]]} - ]) + self.assertSLEs(consume2, [{"stock_value": -10 * 10, "stock_queue": [[-10, 10]]}]) receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15) - self.assertSLEs(receipt2, [ - {"stock_queue": [[5, 15]], "stock_value_difference": 175} - ]) + self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}]) + def create_repack_entry(**args): args = frappe._dict(args) @@ -476,51 +632,63 @@ def create_repack_entry(**args): repack.company = args.company or "_Test Company" repack.posting_date = args.posting_date repack.set_posting_time = 1 - repack.append("items", { - "item_code": "_Test Item for Reposting", - "s_warehouse": "Stores - _TC", - "qty": 5, - "conversion_factor": 1, - "expense_account": "Stock Adjustment - _TC", - "cost_center": "Main - _TC" - }) + repack.append( + "items", + { + "item_code": "_Test Item for Reposting", + "s_warehouse": "Stores - _TC", + "qty": 5, + "conversion_factor": 1, + "expense_account": "Stock Adjustment - _TC", + "cost_center": "Main - _TC", + }, + ) - repack.append("items", { - "item_code": "_Test Finished Item for Reposting", - "t_warehouse": "Finished Goods - _TC", - "qty": 1, - "conversion_factor": 1, - "expense_account": "Stock Adjustment - _TC", - "cost_center": "Main - _TC" - }) + repack.append( + "items", + { + "item_code": "_Test Finished Item for Reposting", + "t_warehouse": "Finished Goods - _TC", + "qty": 1, + "conversion_factor": 1, + "expense_account": "Stock Adjustment - _TC", + "cost_center": "Main - _TC", + }, + ) - repack.append("additional_costs", { - "expense_account": "Freight and Forwarding Charges - _TC", - "description": "transport cost", - "amount": 40 - }) + repack.append( + "additional_costs", + { + "expense_account": "Freight and Forwarding Charges - _TC", + "description": "transport cost", + "amount": 40, + }, + ) repack.save() repack.submit() return repack + def create_product_bundle_item(new_item_code, packed_items): if not frappe.db.exists("Product Bundle", new_item_code): item = frappe.new_doc("Product Bundle") item.new_item_code = new_item_code for d in packed_items: - item.append("items", { - "item_code": d[0], - "qty": d[1] - }) + item.append("items", {"item_code": d[0], "qty": d[1]}) item.save() + def create_items(): - items = ["_Test Item for Reposting", "_Test Finished Item for Reposting", - "_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"] + items = [ + "_Test Item for Reposting", + "_Test Finished Item for Reposting", + "_Test Subcontracted Item for Reposting", + "_Test Bundled Item for Reposting", + ] for d in items: properties = {"valuation_method": "FIFO"} if d == "_Test Bundled Item for Reposting": @@ -534,7 +702,6 @@ def create_items(): class TestDeferredNaming(FrappeTestCase): - @classmethod def setUpClass(cls) -> None: super().setUpClass() @@ -547,10 +714,22 @@ class TestDeferredNaming(FrappeTestCase): self.company = "_Test Company with perpetual inventory" def tearDown(self) -> None: - make_property_setter(doctype="GL Entry", for_doctype=True, - property="autoname", value=self.gle_autoname, property_type="Data", fieldname=None) - make_property_setter(doctype="Stock Ledger Entry", for_doctype=True, - property="autoname", value=self.sle_autoname, property_type="Data", fieldname=None) + make_property_setter( + doctype="GL Entry", + for_doctype=True, + property="autoname", + value=self.gle_autoname, + property_type="Data", + fieldname=None, + ) + make_property_setter( + doctype="Stock Ledger Entry", + for_doctype=True, + property="autoname", + value=self.sle_autoname, + property_type="Data", + fieldname=None, + ) # since deferred naming autocommits, commit all changes to avoid flake frappe.db.commit() # nosemgrep @@ -563,12 +742,13 @@ class TestDeferredNaming(FrappeTestCase): return gle, sle def test_deferred_naming(self): - se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse, - qty=10, rate=100, company=self.company) + se = make_stock_entry( + item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company + ) gle, sle = self.get_gle_sles(se) rename_gle_sle_docs() - renamed_gle, renamed_sle = self.get_gle_sles(se) + renamed_gle, renamed_sle = self.get_gle_sles(se) self.assertFalse(gle & renamed_gle, msg="GLEs not renamed") self.assertFalse(sle & renamed_sle, msg="SLEs not renamed") @@ -577,15 +757,22 @@ class TestDeferredNaming(FrappeTestCase): def test_hash_naming(self): # disable naming series for doctype in ("GL Entry", "Stock Ledger Entry"): - make_property_setter(doctype=doctype, for_doctype=True, - property="autoname", value="hash", property_type="Data", fieldname=None) + make_property_setter( + doctype=doctype, + for_doctype=True, + property="autoname", + value="hash", + property_type="Data", + fieldname=None, + ) - se = make_stock_entry(item_code=self.item, to_warehouse=self.warehouse, - qty=10, rate=100, company=self.company) + se = make_stock_entry( + item_code=self.item, to_warehouse=self.warehouse, qty=10, rate=100, company=self.company + ) gle, sle = self.get_gle_sles(se) rename_gle_sle_docs() - renamed_gle, renamed_sle = self.get_gle_sles(se) + renamed_gle, renamed_sle = self.get_gle_sles(se) self.assertEqual(gle, renamed_gle, msg="GLEs are renamed while using hash naming") self.assertEqual(sle, renamed_sle, msg="SLEs are renamed while using hash naming") diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 8f65287c4e8..16ca88bd810 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -14,8 +14,13 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance -class OpeningEntryAccountError(frappe.ValidationError): pass -class EmptyStockReconciliationItemsError(frappe.ValidationError): pass +class OpeningEntryAccountError(frappe.ValidationError): + pass + + +class EmptyStockReconciliationItemsError(frappe.ValidationError): + pass + class StockReconciliation(StockController): def __init__(self, *args, **kwargs): @@ -24,9 +29,11 @@ class StockReconciliation(StockController): def validate(self): if not self.expense_account: - self.expense_account = frappe.get_cached_value('Company', self.company, "stock_adjustment_account") + self.expense_account = frappe.get_cached_value( + "Company", self.company, "stock_adjustment_account" + ) if not self.cost_center: - self.cost_center = frappe.get_cached_value('Company', self.company, "cost_center") + self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") self.validate_posting_time() self.remove_items_with_no_change() self.validate_data() @@ -37,8 +44,8 @@ class StockReconciliation(StockController): self.set_total_qty_and_amount() self.validate_putaway_capacity() - if self._action=="submit": - self.make_batches('warehouse') + if self._action == "submit": + self.make_batches("warehouse") def on_submit(self): self.update_stock_ledger() @@ -46,10 +53,11 @@ class StockReconciliation(StockController): self.repost_future_sle_and_gle() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.make_sle_on_cancel() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() @@ -57,13 +65,17 @@ class StockReconciliation(StockController): def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 - def _changed(item): - item_dict = get_stock_balance_for(item.item_code, item.warehouse, - self.posting_date, self.posting_time, batch_no=item.batch_no) - if ((item.qty is None or item.qty==item_dict.get("qty")) and - (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and - (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")) )): + def _changed(item): + item_dict = get_stock_balance_for( + item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no + ) + + if ( + (item.qty is None or item.qty == item_dict.get("qty")) + and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")) + and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos"))) + ): return False else: # set default as current rates @@ -80,16 +92,20 @@ class StockReconciliation(StockController): item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") - self.difference_amount += (flt(item.qty, item.precision("qty")) * \ - flt(item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate")) \ - - flt(item_dict.get("qty"), item.precision("qty")) * flt(item_dict.get("rate"), item.precision("valuation_rate"))) + self.difference_amount += flt(item.qty, item.precision("qty")) * flt( + item.valuation_rate or item_dict.get("rate"), item.precision("valuation_rate") + ) - flt(item_dict.get("qty"), item.precision("qty")) * flt( + item_dict.get("rate"), item.precision("valuation_rate") + ) return True items = list(filter(lambda d: _changed(d), self.items)) if not items: - frappe.throw(_("None of the items have any change in quantity or value."), - EmptyStockReconciliationItemsError) + frappe.throw( + _("None of the items have any change in quantity or value."), + EmptyStockReconciliationItemsError, + ) elif len(items) != len(self.items): self.items = items @@ -99,7 +115,7 @@ class StockReconciliation(StockController): def validate_data(self): def _get_msg(row_num, msg): - return _("Row # {0}:").format(row_num+1) + " " + msg + return _("Row # {0}:").format(row_num + 1) + " " + msg self.validation_messages = [] item_warehouse_combinations = [] @@ -109,7 +125,7 @@ class StockReconciliation(StockController): for row_num, row in enumerate(self.items): # find duplicates key = [row.item_code, row.warehouse] - for field in ['serial_no', 'batch_no']: + for field in ["serial_no", "batch_no"]: if row.get(field): key.append(row.get(field)) @@ -126,32 +142,35 @@ class StockReconciliation(StockController): # if both not specified if row.qty in ["", None] and row.valuation_rate in ["", None]: - self.validation_messages.append(_get_msg(row_num, - _("Please specify either Quantity or Valuation Rate or both"))) + self.validation_messages.append( + _get_msg(row_num, _("Please specify either Quantity or Valuation Rate or both")) + ) # do not allow negative quantity if flt(row.qty) < 0: - self.validation_messages.append(_get_msg(row_num, - _("Negative Quantity is not allowed"))) + self.validation_messages.append(_get_msg(row_num, _("Negative Quantity is not allowed"))) # do not allow negative valuation if flt(row.valuation_rate) < 0: - self.validation_messages.append(_get_msg(row_num, - _("Negative Valuation Rate is not allowed"))) + self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed"))) if row.qty and row.valuation_rate in ["", None]: - row.valuation_rate = get_stock_balance(row.item_code, row.warehouse, - self.posting_date, self.posting_time, with_valuation_rate=True)[1] + row.valuation_rate = get_stock_balance( + row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True + )[1] if not row.valuation_rate: # try if there is a buying price list in default currency - buying_rate = frappe.db.get_value("Item Price", {"item_code": row.item_code, - "buying": 1, "currency": default_currency}, "price_list_rate") + buying_rate = frappe.db.get_value( + "Item Price", + {"item_code": row.item_code, "buying": 1, "currency": default_currency}, + "price_list_rate", + ) if buying_rate: row.valuation_rate = buying_rate else: # get valuation rate from Item - row.valuation_rate = frappe.get_value('Item', row.item_code, 'valuation_rate') + row.valuation_rate = frappe.get_value("Item", row.item_code, "valuation_rate") # throw all validation messages if self.validation_messages: @@ -178,7 +197,9 @@ class StockReconciliation(StockController): # item should not be serialized if item.has_serial_no and not row.serial_no and not item.serial_no_series: - raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code)) + raise frappe.ValidationError( + _("Serial no(s) required for serialized item {0}").format(item_code) + ) # item managed batch-wise not allowed if item.has_batch_no and not row.batch_no and not item.create_new_batch: @@ -191,8 +212,8 @@ class StockReconciliation(StockController): self.validation_messages.append(_("Row #") + " " + ("%d: " % (row.idx)) + cstr(e)) def update_stock_ledger(self): - """ find difference between current and expected entries - and create stock ledger entries based on the difference""" + """find difference between current and expected entries + and create stock ledger entries based on the difference""" from erpnext.stock.stock_ledger import get_previous_sle sl_entries = [] @@ -208,15 +229,20 @@ class StockReconciliation(StockController): self.get_sle_for_serialized_items(row, sl_entries) else: if row.serial_no or row.batch_no: - frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ - .format(row.idx, frappe.bold(row.item_code))) + frappe.throw( + _( + "Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it." + ).format(row.idx, frappe.bold(row.item_code)) + ) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time - }) + previous_sle = get_previous_sle( + { + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) if previous_sle: if row.qty in ("", None): @@ -226,12 +252,16 @@ class StockReconciliation(StockController): row.valuation_rate = previous_sle.get("valuation_rate", 0) if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate: - frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) + frappe.throw( + _("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx) + ) - if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") - and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0)) - or (not previous_sle and not row.qty)): - continue + if ( + previous_sle + and row.qty == previous_sle.get("qty_after_transaction") + and (row.valuation_rate == previous_sle.get("valuation_rate") or row.qty == 0) + ) or (not previous_sle and not row.qty): + continue sl_entries.append(self.get_sle_for_items(row)) @@ -253,21 +283,24 @@ class StockReconciliation(StockController): serial_nos = get_serial_nos(row.serial_no) - # To issue existing serial nos if row.current_qty and (row.current_serial_no or row.batch_no): args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': -1 * row.current_qty, - 'serial_no': row.current_serial_no, - 'batch_no': row.batch_no, - 'valuation_rate': row.current_valuation_rate - }) + args.update( + { + "actual_qty": -1 * row.current_qty, + "serial_no": row.current_serial_no, + "batch_no": row.batch_no, + "valuation_rate": row.current_valuation_rate, + } + ) if row.current_serial_no: - args.update({ - 'qty_after_transaction': 0, - }) + args.update( + { + "qty_after_transaction": 0, + } + ) sl_entries.append(args) @@ -275,42 +308,49 @@ class StockReconciliation(StockController): for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) - previous_sle = get_previous_sle({ - "item_code": row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "serial_no": serial_no - }) + previous_sle = get_previous_sle( + { + "item_code": row.item_code, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "serial_no": serial_no, + } + ) if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse - warehouse = previous_sle.get("warehouse", '') or row.warehouse + warehouse = previous_sle.get("warehouse", "") or row.warehouse if not qty_after_transaction: - qty_after_transaction = get_stock_balance(row.item_code, - warehouse, self.posting_date, self.posting_time) + qty_after_transaction = get_stock_balance( + row.item_code, warehouse, self.posting_date, self.posting_time + ) qty_after_transaction -= 1 new_args = args.copy() - new_args.update({ - 'actual_qty': -1, - 'qty_after_transaction': qty_after_transaction, - 'warehouse': warehouse, - 'valuation_rate': previous_sle.get("valuation_rate") - }) + new_args.update( + { + "actual_qty": -1, + "qty_after_transaction": qty_after_transaction, + "warehouse": warehouse, + "valuation_rate": previous_sle.get("valuation_rate"), + } + ) sl_entries.append(new_args) if row.qty: args = self.get_sle_for_items(row) - args.update({ - 'actual_qty': row.qty, - 'incoming_rate': row.valuation_rate, - 'valuation_rate': row.valuation_rate - }) + args.update( + { + "actual_qty": row.qty, + "incoming_rate": row.valuation_rate, + "valuation_rate": row.valuation_rate, + } + ) sl_entries.append(args) @@ -320,7 +360,8 @@ class StockReconciliation(StockController): def update_valuation_rate_for_serial_no(self): for d in self.items: - if not d.serial_no: continue + if not d.serial_no: + continue serial_nos = get_serial_nos(d.serial_no) self.update_valuation_rate_for_serial_nos(d, serial_nos) @@ -331,7 +372,7 @@ class StockReconciliation(StockController): return for d in serial_nos: - frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate) + frappe.db.set_value("Serial No", d, "purchase_rate", valuation_rate) def get_sle_for_items(self, row, serial_nos=None): """Insert Stock Ledger Entries""" @@ -339,22 +380,24 @@ class StockReconciliation(StockController): if not serial_nos and row.serial_no: serial_nos = get_serial_nos(row.serial_no) - data = frappe._dict({ - "doctype": "Stock Ledger Entry", - "item_code": row.item_code, - "warehouse": row.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "voucher_type": self.doctype, - "voucher_no": self.name, - "voucher_detail_no": row.name, - "company": self.company, - "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), - "is_cancelled": 1 if self.docstatus == 2 else 0, - "serial_no": '\n'.join(serial_nos) if serial_nos else '', - "batch_no": row.batch_no, - "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")) - }) + data = frappe._dict( + { + "doctype": "Stock Ledger Entry", + "item_code": row.item_code, + "warehouse": row.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": row.name, + "company": self.company, + "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), + "is_cancelled": 1 if self.docstatus == 2 else 0, + "serial_no": "\n".join(serial_nos) if serial_nos else "", + "batch_no": row.batch_no, + "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")), + } + ) if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) @@ -382,7 +425,7 @@ class StockReconciliation(StockController): for row in self.items: if row.serial_no or row.batch_no or row.current_serial_no: has_serial_no = True - serial_nos = '' + serial_nos = "" if row.current_serial_no: serial_nos = get_serial_nos(row.current_serial_no) @@ -395,10 +438,11 @@ class StockReconciliation(StockController): sl_entries = self.merge_similar_item_serial_nos(sl_entries) sl_entries.reverse() - allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + allow_negative_stock = cint( + frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + ) self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) - def merge_similar_item_serial_nos(self, sl_entries): # If user has put the same item in multiple row with different serial no new_sl_entries = [] @@ -411,16 +455,16 @@ class StockReconciliation(StockController): key = (d.item_code, d.warehouse) if key not in merge_similar_entries: - d.total_amount = (d.actual_qty * d.valuation_rate) + d.total_amount = d.actual_qty * d.valuation_rate merge_similar_entries[key] = d elif d.serial_no: data = merge_similar_entries[key] data.actual_qty += d.actual_qty data.qty_after_transaction += d.qty_after_transaction - data.total_amount += (d.actual_qty * d.valuation_rate) + data.total_amount += d.actual_qty * d.valuation_rate data.valuation_rate = (data.total_amount) / data.actual_qty - data.serial_no += '\n' + d.serial_no + data.serial_no += "\n" + d.serial_no data.incoming_rate = (data.total_amount) / data.actual_qty @@ -433,8 +477,9 @@ class StockReconciliation(StockController): if not self.cost_center: msgprint(_("Please enter Cost Center"), raise_exception=1) - return super(StockReconciliation, self).get_gl_entries(warehouse_account, - self.expense_account, self.cost_center) + return super(StockReconciliation, self).get_gl_entries( + warehouse_account, self.expense_account, self.cost_center + ) def validate_expense_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): @@ -442,29 +487,39 @@ class StockReconciliation(StockController): if not self.expense_account: frappe.throw(_("Please enter Expense Account")) - elif self.purpose == "Opening Stock" or not frappe.db.sql("""select name from `tabStock Ledger Entry` limit 1"""): + elif self.purpose == "Opening Stock" or not frappe.db.sql( + """select name from `tabStock Ledger Entry` limit 1""" + ): if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss": - frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError) + frappe.throw( + _( + "Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry" + ), + OpeningEntryAccountError, + ) def set_zero_value_for_customer_provided_items(self): changed_any_values = False - for d in self.get('items'): - is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item') + for d in self.get("items"): + is_customer_item = frappe.db.get_value("Item", d.item_code, "is_customer_provided_item") if is_customer_item and d.valuation_rate: d.valuation_rate = 0.0 changed_any_values = True if changed_any_values: - msgprint(_("Valuation rate for customer provided items has been set to zero."), - title=_("Note"), indicator="blue") - + msgprint( + _("Valuation rate for customer provided items has been set to zero."), + title=_("Note"), + indicator="blue", + ) def set_total_qty_and_amount(self): for d in self.get("items"): d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate")) - d.current_amount = (flt(d.current_qty, - d.precision("current_qty")) * flt(d.current_valuation_rate, d.precision("current_valuation_rate"))) + d.current_amount = flt(d.current_qty, d.precision("current_qty")) * flt( + d.current_valuation_rate, d.precision("current_valuation_rate") + ) d.quantity_difference = flt(d.qty) - flt(d.current_qty) d.amount_difference = flt(d.amount) - flt(d.current_amount) @@ -476,25 +531,33 @@ class StockReconciliation(StockController): def submit(self): if len(self.items) > 100: - msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage")) - self.queue_action('submit', timeout=2000) + msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" + ) + ) + self.queue_action("submit", timeout=2000) else: self._submit() def cancel(self): if len(self.items) > 100: - msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage")) - self.queue_action('cancel', timeout=2000) + msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" + ) + ) + self.queue_action("cancel", timeout=2000) else: self._cancel() + @frappe.whitelist() -def get_items(warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False): +def get_items( + warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False +): ignore_empty_stock = cint(ignore_empty_stock) - items = [frappe._dict({ - 'item_code': item_code, - 'warehouse': warehouse - })] + items = [frappe._dict({"item_code": item_code, "warehouse": warehouse})] if not item_code: items = get_items_for_stock_reco(warehouse, company) @@ -504,8 +567,9 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig for d in items: if d.item_code in itemwise_batch_data: - valuation_rate = get_stock_balance(d.item_code, d.warehouse, - posting_date, posting_time, with_valuation_rate=True)[1] + valuation_rate = get_stock_balance( + d.item_code, d.warehouse, posting_date, posting_time, with_valuation_rate=True + )[1] for row in itemwise_batch_data.get(d.item_code): if ignore_empty_stock and not row.qty: @@ -514,12 +578,22 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig args = get_item_data(row, row.qty, valuation_rate) res.append(args) else: - stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time, - with_valuation_rate=True , with_serial_no=cint(d.has_serial_no)) - qty, valuation_rate, serial_no = stock_bal[0], stock_bal[1], stock_bal[2] if cint(d.has_serial_no) else '' + stock_bal = get_stock_balance( + d.item_code, + d.warehouse, + posting_date, + posting_time, + with_valuation_rate=True, + with_serial_no=cint(d.has_serial_no), + ) + qty, valuation_rate, serial_no = ( + stock_bal[0], + stock_bal[1], + stock_bal[2] if cint(d.has_serial_no) else "", + ) if ignore_empty_stock and not stock_bal[0]: - continue + continue args = get_item_data(d, qty, valuation_rate, serial_no) @@ -527,9 +601,11 @@ def get_items(warehouse, posting_date, posting_time, company, item_code=None, ig return res + def get_items_for_stock_reco(warehouse, company): lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - items = frappe.db.sql(f""" + items = frappe.db.sql( + f""" select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no from @@ -542,9 +618,12 @@ def get_items_for_stock_reco(warehouse, company): and exists( select name from `tabWarehouse` where lft >= {lft} and rgt <= {rgt} and name = bin.warehouse ) - """, as_dict=1) + """, + as_dict=1, + ) - items += frappe.db.sql(""" + items += frappe.db.sql( + """ select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no from @@ -559,40 +638,50 @@ def get_items_for_stock_reco(warehouse, company): and IFNULL(i.disabled, 0) = 0 and id.company = %s group by i.name - """, (lft, rgt, company), as_dict=1) + """, + (lft, rgt, company), + as_dict=1, + ) # remove duplicates # check if item-warehouse key extracted from each entry exists in set iw_keys # and update iw_keys iw_keys = set() - items = [item for item in items if [(item.item_code, item.warehouse) not in iw_keys, iw_keys.add((item.item_code, item.warehouse))][0]] + items = [ + item + for item in items + if [ + (item.item_code, item.warehouse) not in iw_keys, + iw_keys.add((item.item_code, item.warehouse)), + ][0] + ] return items + def get_item_data(row, qty, valuation_rate, serial_no=None): return { - 'item_code': row.item_code, - 'warehouse': row.warehouse, - 'qty': qty, - 'item_name': row.item_name, - 'valuation_rate': valuation_rate, - 'current_qty': qty, - 'current_valuation_rate': valuation_rate, - 'current_serial_no': serial_no, - 'serial_no': serial_no, - 'batch_no': row.get('batch_no') + "item_code": row.item_code, + "warehouse": row.warehouse, + "qty": qty, + "item_name": row.item_name, + "valuation_rate": valuation_rate, + "current_qty": qty, + "current_valuation_rate": valuation_rate, + "current_serial_no": serial_no, + "serial_no": serial_no, + "batch_no": row.get("batch_no"), } + def get_itemwise_batch(warehouse, posting_date, company, item_code=None): from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute + itemwise_batch_data = {} - filters = frappe._dict({ - 'warehouse': warehouse, - 'from_date': posting_date, - 'to_date': posting_date, - 'company': company - }) + filters = frappe._dict( + {"warehouse": warehouse, "from_date": posting_date, "to_date": posting_date, "company": company} + ) if item_code: filters.item_code = item_code @@ -600,23 +689,28 @@ def get_itemwise_batch(warehouse, posting_date, company, item_code=None): columns, data = execute(filters) for row in data: - itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({ - 'item_code': row[0], - 'warehouse': warehouse, - 'qty': row[8], - 'item_name': row[1], - 'batch_no': row[4] - })) + itemwise_batch_data.setdefault(row[0], []).append( + frappe._dict( + { + "item_code": row[0], + "warehouse": warehouse, + "qty": row[8], + "item_name": row[1], + "batch_no": row[4], + } + ) + ) return itemwise_batch_data -@frappe.whitelist() -def get_stock_balance_for(item_code, warehouse, - posting_date, posting_time, batch_no=None, with_valuation_rate= True): - frappe.has_permission("Stock Reconciliation", "write", throw = True) - item_dict = frappe.db.get_value("Item", item_code, - ["has_serial_no", "has_batch_no"], as_dict=1) +@frappe.whitelist() +def get_stock_balance_for( + item_code, warehouse, posting_date, posting_time, batch_no=None, with_valuation_rate=True +): + frappe.has_permission("Stock Reconciliation", "write", throw=True) + + item_dict = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1) if not item_dict: # In cases of data upload to Items table @@ -625,8 +719,14 @@ def get_stock_balance_for(item_code, warehouse, serial_nos = "" with_serial_no = True if item_dict.get("has_serial_no") else False - data = get_stock_balance(item_code, warehouse, posting_date, posting_time, - with_valuation_rate=with_valuation_rate, with_serial_no=with_serial_no) + data = get_stock_balance( + item_code, + warehouse, + posting_date, + posting_time, + with_valuation_rate=with_valuation_rate, + with_serial_no=with_serial_no, + ) if with_serial_no: qty, rate, serial_nos = data @@ -634,20 +734,20 @@ def get_stock_balance_for(item_code, warehouse, qty, rate = data if item_dict.get("has_batch_no"): - qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + qty = ( + get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + ) + + return {"qty": qty, "rate": rate, "serial_nos": serial_nos} - return { - 'qty': qty, - 'rate': rate, - 'serial_nos': serial_nos - } @frappe.whitelist() def get_difference_account(purpose, company): - if purpose == 'Stock Reconciliation': + if purpose == "Stock Reconciliation": account = get_company_default(company, "stock_adjustment_account") else: - account = frappe.db.get_value('Account', {'is_group': 0, - 'company': company, 'account_type': 'Temporary'}, 'name') + account = frappe.db.get_value( + "Account", {"is_group": 0, "company": company, "account_type": "Temporary"}, "name" + ) return account diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index d3e63713847..f06771888f2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -32,7 +32,6 @@ class TestStockReconciliation(FrappeTestCase): def tearDown(self): frappe.flags.dont_execute_stock_reposts = None - def test_reco_for_fifo(self): self._test_reco_sle_gle("FIFO") @@ -40,55 +39,72 @@ class TestStockReconciliation(FrappeTestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1') - company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') + se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1") + company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") # [[qty, valuation_rate, posting_date, - # posting_time, expected_stock_value, bin_qty, bin_valuation]] + # posting_time, expected_stock_value, bin_qty, bin_valuation]] input_data = [ [50, 1000, "2012-12-26", "12:00"], [25, 900, "2012-12-26", "12:00"], ["", 1000, "2012-12-20", "12:05"], [20, "", "2012-12-26", "12:05"], - [0, "", "2012-12-31", "12:10"] + [0, "", "2012-12-31", "12:10"], ] for d in input_data: set_valuation_method("_Test Item", valuation_method) - last_sle = get_previous_sle({ - "item_code": "_Test Item", - "warehouse": "Stores - TCP1", - "posting_date": d[2], - "posting_time": d[3] - }) + last_sle = get_previous_sle( + { + "item_code": "_Test Item", + "warehouse": "Stores - TCP1", + "posting_date": d[2], + "posting_time": d[3], + } + ) # submit stock reconciliation - stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1], - posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1", - company=company, expense_account = "Stock Adjustment - TCP1") + stock_reco = create_stock_reconciliation( + qty=d[0], + rate=d[1], + posting_date=d[2], + posting_time=d[3], + warehouse="Stores - TCP1", + company=company, + expense_account="Stock Adjustment - TCP1", + ) # check stock value - sle = frappe.db.sql("""select * from `tabStock Ledger Entry` - where voucher_type='Stock Reconciliation' and voucher_no=%s""", stock_reco.name, as_dict=1) + sle = frappe.db.sql( + """select * from `tabStock Ledger Entry` + where voucher_type='Stock Reconciliation' and voucher_no=%s""", + stock_reco.name, + as_dict=1, + ) qty_after_transaction = flt(d[0]) if d[0] != "" else flt(last_sle.get("qty_after_transaction")) valuation_rate = flt(d[1]) if d[1] != "" else flt(last_sle.get("valuation_rate")) - if qty_after_transaction == last_sle.get("qty_after_transaction") \ - and valuation_rate == last_sle.get("valuation_rate"): - self.assertFalse(sle) + if qty_after_transaction == last_sle.get( + "qty_after_transaction" + ) and valuation_rate == last_sle.get("valuation_rate"): + self.assertFalse(sle) else: self.assertEqual(flt(sle[0].qty_after_transaction, 1), flt(qty_after_transaction, 1)) self.assertEqual(flt(sle[0].stock_value, 1), flt(qty_after_transaction * valuation_rate, 1)) # no gl entries - self.assertTrue(frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name})) + self.assertTrue( + frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Stock Reconciliation", "voucher_no": stock_reco.name} + ) + ) - acc_bal, stock_bal, wh_list = get_stock_and_account_balance("Stock In Hand - TCP1", - stock_reco.posting_date, stock_reco.company) + acc_bal, stock_bal, wh_list = get_stock_and_account_balance( + "Stock In Hand - TCP1", stock_reco.posting_date, stock_reco.company + ) self.assertEqual(flt(acc_bal, 1), flt(stock_bal, 1)) stock_reco.cancel() @@ -98,18 +114,33 @@ class TestStockReconciliation(FrappeTestCase): se1.cancel() def test_get_items(self): - create_warehouse("_Test Warehouse Group 1", - {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"}) - create_warehouse("_Test Warehouse Ledger 1", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"}) + create_warehouse( + "_Test Warehouse Group 1", + {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"}, + ) + create_warehouse( + "_Test Warehouse Ledger 1", + { + "is_group": 0, + "parent_warehouse": "_Test Warehouse Group 1 - _TC", + "company": "_Test Company", + }, + ) - create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100, - warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100) + create_item( + "_Test Stock Reco Item", + is_stock_item=1, + valuation_rate=100, + warehouse="_Test Warehouse Ledger 1 - _TC", + opening_stock=100, + ) items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime(), "_Test Company") - self.assertEqual(["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], - [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]]) + self.assertEqual( + ["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100], + [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]], + ) def test_stock_reco_for_serialized_item(self): to_delete_records = [] @@ -119,8 +150,9 @@ class TestStockReconciliation(FrappeTestCase): serial_item_code = "Stock-Reco-Serial-Item-1" serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" - sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=200) + sr = create_stock_reconciliation( + item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=200 + ) serial_nos = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos), 5) @@ -130,7 +162,7 @@ class TestStockReconciliation(FrappeTestCase): "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), - "serial_no": sr.items[0].serial_no + "serial_no": sr.items[0].serial_no, } valuation_rate = get_incoming_rate(args) @@ -138,8 +170,9 @@ class TestStockReconciliation(FrappeTestCase): to_delete_records.append(sr.name) - sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=300) + sr = create_stock_reconciliation( + item_code=serial_item_code, warehouse=serial_warehouse, qty=5, rate=300 + ) serial_nos1 = get_serial_nos(sr.items[0].serial_no) self.assertEqual(len(serial_nos1), 5) @@ -149,7 +182,7 @@ class TestStockReconciliation(FrappeTestCase): "warehouse": serial_warehouse, "posting_date": nowdate(), "posting_time": nowtime(), - "serial_no": sr.items[0].serial_no + "serial_no": sr.items[0].serial_no, } valuation_rate = get_incoming_rate(args) @@ -162,7 +195,6 @@ class TestStockReconciliation(FrappeTestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() - def test_stock_reco_for_merge_serialized_item(self): to_delete_records = [] @@ -170,23 +202,34 @@ class TestStockReconciliation(FrappeTestCase): serial_item_code = "Stock-Reco-Serial-Item-2" serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC" - sr = create_stock_reconciliation(item_code=serial_item_code, serial_no=random_string(6), - warehouse = serial_warehouse, qty=1, rate=100, do_not_submit=True, purpose='Opening Stock') + sr = create_stock_reconciliation( + item_code=serial_item_code, + serial_no=random_string(6), + warehouse=serial_warehouse, + qty=1, + rate=100, + do_not_submit=True, + purpose="Opening Stock", + ) for i in range(3): - sr.append('items', { - 'item_code': serial_item_code, - 'warehouse': serial_warehouse, - 'qty': 1, - 'valuation_rate': 100, - 'serial_no': random_string(6) - }) + sr.append( + "items", + { + "item_code": serial_item_code, + "warehouse": serial_warehouse, + "qty": 1, + "valuation_rate": 100, + "serial_no": random_string(6), + }, + ) sr.save() sr.submit() - sle_entries = frappe.get_all('Stock Ledger Entry', filters= {'voucher_no': sr.name}, - fields = ['name', 'incoming_rate']) + sle_entries = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": sr.name}, fields=["name", "incoming_rate"] + ) self.assertEqual(len(sle_entries), 1) self.assertEqual(sle_entries[0].incoming_rate, 100) @@ -206,16 +249,18 @@ class TestStockReconciliation(FrappeTestCase): item_code = "Stock-Reco-batch-Item-1" warehouse = "_Test Warehouse for Stock Reco2 - _TC" - sr = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=5, rate=200, do_not_submit=1) + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1 + ) sr.save(ignore_permissions=True) sr.submit() self.assertTrue(sr.items[0].batch_no) to_delete_records.append(sr.name) - sr1 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no) + sr1 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=6, rate=300, batch_no=sr.items[0].batch_no + ) args = { "item_code": item_code, @@ -228,9 +273,9 @@ class TestStockReconciliation(FrappeTestCase): self.assertEqual(valuation_rate, 300) to_delete_records.append(sr1.name) - - sr2 = create_stock_reconciliation(item_code=item_code, - warehouse = warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no) + sr2 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=0, rate=0, batch_no=sr.items[0].batch_no + ) stock_value = get_stock_value_on(warehouse, nowdate(), item_code) self.assertEqual(stock_value, 0) @@ -242,11 +287,12 @@ class TestStockReconciliation(FrappeTestCase): stock_doc.cancel() def test_customer_provided_items(self): - item_code = 'Stock-Reco-customer-Item-100' - create_item(item_code, is_customer_provided_item = 1, - customer = '_Test Customer', is_purchase_item = 0) + item_code = "Stock-Reco-customer-Item-100" + create_item( + item_code, is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 + ) - sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420) + sr = create_stock_reconciliation(item_code=item_code, qty=10, rate=420) self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(sr.get("items")[0].valuation_rate, 0) @@ -254,65 +300,79 @@ class TestStockReconciliation(FrappeTestCase): def test_backdated_stock_reco_qty_reposting(self): """ - Test if a backdated stock reco recalculates future qty until next reco. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] - PR1 | PR | 10 | 18 (posting date: today-3) - PR2 | PR | 1 | 19 (posting date: today-2) - SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] - PR3 | PR | 1 | 7 (posting date: today) # can't post future PR + Test if a backdated stock reco recalculates future qty until next reco. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR5 | Reco | 0 | 8 (posting date: today-4) [backdated] + PR1 | PR | 10 | 18 (posting date: today-3) + PR2 | PR | 1 | 19 (posting date: today-2) + SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] + PR3 | PR | 1 | 7 (posting date: today) # can't post future PR """ item_code = "Backdated-Reco-Item" warehouse = "_Test Warehouse - _TC" create_item(item_code) - pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), -3)) - pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=add_days(nowdate(), -2)) - pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=nowdate()) + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) + ) + pr2 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=add_days(nowdate(), -2) + ) + pr3 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() + ) - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr3_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(pr3_balance, 12) # post backdated stock reco in between - sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100, - posting_date=add_days(nowdate(), -1)) - pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, - "qty_after_transaction") + sr4 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1) + ) + pr3_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr3_balance, 7) # post backdated stock reco at the start - sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100, - posting_date=add_days(nowdate(), -4)) - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, - "qty_after_transaction") - sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, - "qty_after_transaction") + sr5 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4) + ) + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" + ) + sr4_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 18) self.assertEqual(pr2_balance, 19) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected # cancel backdated stock reco and check future impact sr5.cancel() - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, - "qty_after_transaction") - sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + pr2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" + ) + sr4_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(pr2_balance, 11) - self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected + self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected # teardown sr4.cancel() @@ -323,13 +383,13 @@ class TestStockReconciliation(FrappeTestCase): @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_future_negative_stock(self): """ - Test if a backdated stock reco causes future negative stock and is blocked. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - PR1 | PR | 10 | 10 (posting date: today-2) - SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] - DN2 | DN | -2 | 8(-1) (posting date: today) + Test if a backdated stock reco causes future negative stock and is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + PR1 | PR | 10 | 10 (posting date: today-2) + SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked] + DN2 | DN | -2 | 8(-1) (posting date: today) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError @@ -338,22 +398,31 @@ class TestStockReconciliation(FrappeTestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) + pr1 = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2) + ) + dn2 = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=2, rate=120, posting_date=nowdate() + ) - pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), -2)) - dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120, - posting_date=nowdate()) - - pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, - "qty_after_transaction") - dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, - "qty_after_transaction") + pr1_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" + ) + dn2_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(pr1_balance, 10) self.assertEqual(dn2_balance, 8) # check if stock reco is blocked - sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100, - posting_date=add_days(nowdate(), -1), do_not_submit=True) + sr3 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=1, + rate=100, + posting_date=add_days(nowdate(), -1), + do_not_submit=True, + ) self.assertRaises(NegativeStockError, sr3.submit) # teardown @@ -361,16 +430,15 @@ class TestStockReconciliation(FrappeTestCase): dn2.cancel() pr1.cancel() - @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_backdated_stock_reco_cancellation_future_negative_stock(self): """ - Test if a backdated stock reco cancellation that causes future negative stock is blocked. - ------------------------------------------- - Var | Doc | Qty | Balance - ------------------------------------------- - SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) - DN | DN | 100 | 0 (posting date: today) + Test if a backdated stock reco cancellation that causes future negative stock is blocked. + ------------------------------------------- + Var | Doc | Qty | Balance + ------------------------------------------- + SR | Reco | 100 | 100 (posting date: today-1) (shouldn't be cancelled after DN) + DN | DN | 100 | 0 (posting date: today) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError @@ -379,15 +447,21 @@ class TestStockReconciliation(FrappeTestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) + sr = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=100, + rate=100, + posting_date=add_days(nowdate(), -1), + ) - sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=100, rate=100, - posting_date=add_days(nowdate(), -1)) + dn = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=100, rate=120, posting_date=nowdate() + ) - dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=100, rate=120, - posting_date=nowdate()) - - dn_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, - "qty_after_transaction") + dn_balance = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": dn.name, "is_cancelled": 0}, "qty_after_transaction" + ) self.assertEqual(dn_balance, 0) # check if cancellation of stock reco is blocked @@ -399,12 +473,12 @@ class TestStockReconciliation(FrappeTestCase): def test_intermediate_sr_bin_update(self): """Bin should show correct qty even for backdated entries. - ------------------------------------------- - | creation | Var | Doc | Qty | balance qty - ------------------------------------------- - | 1 | SR | Reco | 10 | 10 (posting date: today+10) - | 3 | SR2 | Reco | 11 | 11 (posting date: today+11) - | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12) + ------------------------------------------- + | creation | Var | Doc | Qty | balance qty + ------------------------------------------- + | 1 | SR | Reco | 10 | 10 (posting date: today+10) + | 3 | SR2 | Reco | 11 | 11 (posting date: today+11) + | 2 | DN | DN | 5 | 6 <-- assert in BIN (posting date: today+12) """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note @@ -416,26 +490,33 @@ class TestStockReconciliation(FrappeTestCase): warehouse = "_Test Warehouse - _TC" create_item(item_code) - sr = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=10, rate=100, - posting_date=add_days(nowdate(), 10)) + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10) + ) - dn = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=5, rate=120, - posting_date=add_days(nowdate(), 12)) - old_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") + dn = create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=5, rate=120, posting_date=add_days(nowdate(), 12) + ) + old_bin_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty" + ) - sr2 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=11, rate=100, - posting_date=add_days(nowdate(), 11)) - new_bin_qty = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") + sr2 = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=11, rate=100, posting_date=add_days(nowdate(), 11) + ) + new_bin_qty = frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty" + ) self.assertEqual(old_bin_qty + 1, new_bin_qty) frappe.db.rollback() - def test_valid_batch(self): create_batch_item_with_batch("Testing Batch Item 1", "001") create_batch_item_with_batch("Testing Batch Item 2", "002") - sr = create_stock_reconciliation(item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002" - , do_not_submit=True) + sr = create_stock_reconciliation( + item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_submit=True + ) self.assertRaises(frappe.ValidationError, sr.submit) def test_serial_no_cancellation(self): @@ -457,15 +538,17 @@ class TestStockReconciliation(FrappeTestCase): serial_nos.pop() new_serial_nos = "\n".join(serial_nos) - sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9) + sr = create_stock_reconciliation( + item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9 + ) sr.cancel() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 10) - def test_serial_no_creation_and_inactivation(self): item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1) if not item.has_serial_no: @@ -475,19 +558,27 @@ class TestStockReconciliation(FrappeTestCase): item_code = item.name warehouse = "_Test Warehouse - _TC" - sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, - serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100) + sr = create_stock_reconciliation( + item_code=item.name, + warehouse=warehouse, + serial_no="SR-CREATED-SR-NO", + qty=1, + do_not_submit=True, + rate=100, + ) sr.save() self.assertEqual(cstr(sr.items[0].current_serial_no), "") sr.submit() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 1) sr.cancel() - active_sr_no = frappe.get_all("Serial No", - filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) + active_sr_no = frappe.get_all( + "Serial No", filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"} + ) self.assertEqual(len(active_sr_no), 0) @@ -498,32 +589,51 @@ def create_batch_item_with_batch(item_name, batch_id): batch_item_doc.create_new_batch = 1 batch_item_doc.save(ignore_permissions=True) - if not frappe.db.exists('Batch', batch_id): - b = frappe.new_doc('Batch') + if not frappe.db.exists("Batch", batch_id): + b = frappe.new_doc("Batch") b.item = item_name b.batch_id = batch_id b.save() + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item", - target=warehouse, qty=10, basic_rate=700) + se1 = make_stock_entry( + posting_date="2012-12-15", + posting_time="02:00", + item_code="_Test Item", + target=warehouse, + qty=10, + basic_rate=700, + ) - se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", - source=warehouse, qty=15) + se2 = make_stock_entry( + posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15 + ) - se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item", - target=warehouse, qty=15, basic_rate=1200) + se3 = make_stock_entry( + posting_date="2013-01-05", + posting_time="07:00", + item_code="_Test Item", + target=warehouse, + qty=15, + basic_rate=1200, + ) return se1, se2, se3 -def create_batch_or_serial_no_items(): - create_warehouse("_Test Warehouse for Stock Reco1", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) - create_warehouse("_Test Warehouse for Stock Reco2", - {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) +def create_batch_or_serial_no_items(): + create_warehouse( + "_Test Warehouse for Stock Reco1", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}, + ) + + create_warehouse( + "_Test Warehouse for Stock Reco2", + {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}, + ) serial_item_doc = create_item("Stock-Reco-Serial-Item-1", is_stock_item=1) if not serial_item_doc.has_serial_no: @@ -544,6 +654,7 @@ def create_batch_or_serial_no_items(): serial_item_doc.batch_number_series = "BASR.#####" batch_item_doc.save(ignore_permissions=True) + def create_stock_reconciliation(**args): args = frappe._dict(args) sr = frappe.new_doc("Stock Reconciliation") @@ -552,20 +663,26 @@ def create_stock_reconciliation(**args): sr.posting_time = args.posting_time or nowtime() sr.set_posting_time = 1 sr.company = args.company or "_Test Company" - sr.expense_account = args.expense_account or \ - ("Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC") - sr.cost_center = args.cost_center \ - or frappe.get_cached_value("Company", sr.company, "cost_center") \ + sr.expense_account = args.expense_account or ( + "Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC" + ) + sr.cost_center = ( + args.cost_center + or frappe.get_cached_value("Company", sr.company, "cost_center") or "_Test Cost Center - _TC" + ) - sr.append("items", { - "item_code": args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty, - "valuation_rate": args.rate, - "serial_no": args.serial_no, - "batch_no": args.batch_no - }) + sr.append( + "items", + { + "item_code": args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty, + "valuation_rate": args.rate, + "serial_no": args.serial_no, + "batch_no": args.batch_no, + }, + ) try: if not args.do_not_submit: @@ -574,6 +691,7 @@ def create_stock_reconciliation(**args): pass return sr + def set_valuation_method(item_code, valuation_method): existing_valuation_method = get_valuation_method(item_code) if valuation_method == existing_valuation_method: @@ -581,11 +699,13 @@ def set_valuation_method(item_code, valuation_method): frappe.db.set_value("Item", item_code, "valuation_method", valuation_method) - for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]): + for warehouse in frappe.get_all( + "Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"] + ): if not warehouse.is_group: - update_entries_after({ - "item_code": item_code, - "warehouse": warehouse.name - }, allow_negative_stock=1) + update_entries_after( + {"item_code": item_code, "warehouse": warehouse.name}, allow_negative_stock=1 + ) + test_dependencies = ["Item", "Warehouse"] diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py index bab521d69fc..e0c8ed12e7d 100644 --- a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py +++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py @@ -6,8 +6,6 @@ from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_h class StockRepostingSettings(Document): - - def validate(self): self.set_minimum_reposting_time_slot() diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index c1293cbf0fa..e592a4be3c6 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -16,24 +16,40 @@ from erpnext.stock.utils import check_pending_reposting class StockSettings(Document): def validate(self): - for key in ["item_naming_by", "item_group", "stock_uom", - "allow_negative_stock", "default_warehouse", "set_qty_in_transactions_based_on_serial_no_input"]: - frappe.db.set_default(key, self.get(key, "")) + for key in [ + "item_naming_by", + "item_group", + "stock_uom", + "allow_negative_stock", + "default_warehouse", + "set_qty_in_transactions_based_on_serial_no_input", + ]: + frappe.db.set_default(key, self.get(key, "")) from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Item", "item_code", - self.get("item_naming_by")=="Naming Series", hide_name_field=True, make_mandatory=0) + + set_by_naming_series( + "Item", + "item_code", + self.get("item_naming_by") == "Naming Series", + hide_name_field=True, + make_mandatory=0, + ) stock_frozen_limit = 356 submitted_stock_frozen = self.stock_frozen_upto_days or 0 if submitted_stock_frozen > stock_frozen_limit: self.stock_frozen_upto_days = stock_frozen_limit - frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit) + frappe.msgprint( + _("`Freeze Stocks Older Than` should be smaller than %d days.") % stock_frozen_limit + ) # show/hide barcode field for name in ["barcode", "barcodes", "scan_barcode"]: - frappe.make_property_setter({'fieldname': name, 'property': 'hidden', - 'value': 0 if self.show_barcode_field else 1}, validate_fields_for_doctype=False) + frappe.make_property_setter( + {"fieldname": name, "property": "hidden", "value": 0 if self.show_barcode_field else 1}, + validate_fields_for_doctype=False, + ) self.validate_warehouses() self.cant_change_valuation_method() @@ -44,8 +60,12 @@ class StockSettings(Document): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] for field in warehouse_fields: if frappe.db.get_value("Warehouse", self.get(field), "is_group"): - frappe.throw(_("Group Warehouses cannot be used in transactions. Please change the value of {0}") \ - .format(frappe.bold(self.meta.get_field(field).label)), title =_("Incorrect Warehouse")) + frappe.throw( + _("Group Warehouses cannot be used in transactions. Please change the value of {0}").format( + frappe.bold(self.meta.get_field(field).label) + ), + title=_("Incorrect Warehouse"), + ) def cant_change_valuation_method(self): db_valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") @@ -53,38 +73,73 @@ class StockSettings(Document): if db_valuation_method and db_valuation_method != self.valuation_method: # check if there are any stock ledger entries against items # which does not have it's own valuation method - sle = frappe.db.sql("""select name from `tabStock Ledger Entry` sle + sle = frappe.db.sql( + """select name from `tabStock Ledger Entry` sle where exists(select name from tabItem where name=sle.item_code and (valuation_method is null or valuation_method='')) limit 1 - """) + """ + ) if sle: - frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method")) + frappe.throw( + _( + "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" + ) + ) def validate_clean_description_html(self): - if int(self.clean_description_html or 0) \ - and not int(self.db_get('clean_description_html') or 0): + if int(self.clean_description_html or 0) and not int(self.db_get("clean_description_html") or 0): # changed to text - frappe.enqueue('erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions', now=frappe.flags.in_test) + 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() def toggle_warehouse_field_for_inter_warehouse_transfer(self): - make_property_setter("Sales Invoice Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) - make_property_setter("Delivery Note Item", "target_warehouse", "hidden", 1 - cint(self.allow_from_dn), "Check", validate_fields_for_doctype=False) - make_property_setter("Purchase Invoice Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) - make_property_setter("Purchase Receipt Item", "from_warehouse", "hidden", 1 - cint(self.allow_from_pr), "Check", validate_fields_for_doctype=False) + make_property_setter( + "Sales Invoice Item", + "target_warehouse", + "hidden", + 1 - cint(self.allow_from_dn), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Delivery Note Item", + "target_warehouse", + "hidden", + 1 - cint(self.allow_from_dn), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Purchase Invoice Item", + "from_warehouse", + "hidden", + 1 - cint(self.allow_from_pr), + "Check", + validate_fields_for_doctype=False, + ) + make_property_setter( + "Purchase Receipt Item", + "from_warehouse", + "hidden", + 1 - cint(self.allow_from_pr), + "Check", + validate_fields_for_doctype=False, + ) def clean_all_descriptions(): - for item in frappe.get_all('Item', ['name', 'description']): + for item in frappe.get_all("Item", ["name", "description"]): if item.description: clean_description = clean_html(item.description) if item.description != clean_description: - frappe.db.set_value('Item', item.name, 'description', clean_description) + frappe.db.set_value("Item", item.name, "description", clean_description) diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py index 13496718ead..974e16339b7 100644 --- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py @@ -13,35 +13,45 @@ class TestStockSettings(FrappeTestCase): frappe.db.set_value("Stock Settings", None, "clean_description_html", 0) def test_settings(self): - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = 'Item for description test', - item_group = 'Products', - description = '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ' - )).insert() + item = frappe.get_doc( + dict( + doctype="Item", + item_code="Item for description test", + item_group="Products", + description='

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ', + ) + ).insert() - settings = frappe.get_single('Stock Settings') + settings = frappe.get_single("Stock Settings") settings.clean_description_html = 1 settings.save() item.reload() - self.assertEqual(item.description, '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ') + self.assertEqual( + item.description, + "

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ", + ) item.delete() def test_clean_html(self): - settings = frappe.get_single('Stock Settings') + settings = frappe.get_single("Stock Settings") settings.clean_description_html = 1 settings.save() - item = frappe.get_doc(dict( - doctype = 'Item', - item_code = 'Item for description test', - item_group = 'Products', - description = '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ' - )).insert() + item = frappe.get_doc( + dict( + doctype="Item", + item_code="Item for description test", + item_group="Products", + description='

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ', + ) + ).insert() - self.assertEqual(item.description, '

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ') + self.assertEqual( + item.description, + "

    Drawing No. 07-xxx-PO132
    1800 x 1685 x 750
    All parts made of Marine Ply
    Top w/ Corian dd
    CO, CS, VIP Day Cabin

    ", + ) item.delete() diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 08d7c993521..1e9d01aa4b6 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -11,13 +11,14 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.warehouse import convert_to_group_or_ledger, get_children -test_records = frappe.get_test_records('Warehouse') +test_records = frappe.get_test_records("Warehouse") + class TestWarehouse(FrappeTestCase): def setUp(self): super().setUp() - if not frappe.get_value('Item', '_Test Item'): - make_test_records('Item') + if not frappe.get_value("Item", "_Test Item"): + make_test_records("Item") def test_parent_warehouse(self): parent_warehouse = frappe.get_doc("Warehouse", "_Test Warehouse Group - _TC") @@ -26,8 +27,12 @@ class TestWarehouse(FrappeTestCase): def test_warehouse_hierarchy(self): p_warehouse = frappe.get_doc("Warehouse", "_Test Warehouse Group - _TC") - child_warehouses = frappe.db.sql("""select name, is_group, parent_warehouse from `tabWarehouse` wh - where wh.lft > %s and wh.rgt < %s""", (p_warehouse.lft, p_warehouse.rgt), as_dict=1) + child_warehouses = frappe.db.sql( + """select name, is_group, parent_warehouse from `tabWarehouse` wh + where wh.lft > %s and wh.rgt < %s""", + (p_warehouse.lft, p_warehouse.rgt), + as_dict=1, + ) for child_warehouse in child_warehouses: self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) @@ -36,13 +41,13 @@ class TestWarehouse(FrappeTestCase): def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" - warehouse_names = [f'_Test Warehouse {i} for Unlinking' for i in range(2)] + warehouse_names = [f"_Test Warehouse {i} for Unlinking" for i in range(2)] warehouse_ids = [] for warehouse in warehouse_names: warehouse_id = create_warehouse(warehouse, company=company) warehouse_ids.append(warehouse_id) - item_names = [f'_Test Item {i} for Unlinking' for i in range(2)] + item_names = [f"_Test Item {i} for Unlinking" for i in range(2)] for item, warehouse in zip(item_names, warehouse_ids): create_item(item, warehouse=warehouse, company=company) @@ -52,17 +57,14 @@ class TestWarehouse(FrappeTestCase): # Check Item existance for item in item_names: - self.assertTrue( - bool(frappe.db.exists("Item", item)), - f"{item} doesn't exist" - ) + self.assertTrue(bool(frappe.db.exists("Item", item)), f"{item} doesn't exist") item_doc = frappe.get_doc("Item", item) for item_default in item_doc.item_defaults: self.assertNotIn( item_default.default_warehouse, warehouse_ids, - f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}." + f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}.", ) def test_group_non_group_conversion(self): @@ -90,7 +92,7 @@ class TestWarehouse(FrappeTestCase): company = "_Test Company" children = get_children("Warehouse", parent=company, company=company, is_root=True) - self.assertTrue(any(wh['value'] == "_Test Warehouse - _TC" for wh in children)) + self.assertTrue(any(wh["value"] == "_Test Warehouse - _TC" for wh in children)) def create_warehouse(warehouse_name, properties=None, company=None): @@ -111,40 +113,46 @@ def create_warehouse(warehouse_name, properties=None, company=None): else: return warehouse_id + def get_warehouse(**args): args = frappe._dict(args) - if(frappe.db.exists("Warehouse", args.warehouse_name + " - " + args.abbr)): + if frappe.db.exists("Warehouse", args.warehouse_name + " - " + args.abbr): return frappe.get_doc("Warehouse", args.warehouse_name + " - " + args.abbr) else: - w = frappe.get_doc({ - "company": args.company or "_Test Company", - "doctype": "Warehouse", - "warehouse_name": args.warehouse_name, - "is_group": 0, - "account": get_warehouse_account(args.warehouse_name, args.company, args.abbr) - }) + w = frappe.get_doc( + { + "company": args.company or "_Test Company", + "doctype": "Warehouse", + "warehouse_name": args.warehouse_name, + "is_group": 0, + "account": get_warehouse_account(args.warehouse_name, args.company, args.abbr), + } + ) w.insert() return w + def get_warehouse_account(warehouse_name, company, company_abbr=None): if not company_abbr: - company_abbr = frappe.get_cached_value("Company", company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") if not frappe.db.exists("Account", warehouse_name + " - " + company_abbr): return create_account( account_name=warehouse_name, parent_account=get_group_stock_account(company, company_abbr), - account_type='Stock', - company=company) + account_type="Stock", + company=company, + ) else: return warehouse_name + " - " + company_abbr def get_group_stock_account(company, company_abbr=None): - group_stock_account = frappe.db.get_value("Account", - filters={'account_type': 'Stock', 'is_group': 1, 'company': company}, fieldname='name') + group_stock_account = frappe.db.get_value( + "Account", filters={"account_type": "Stock", "is_group": 1, "company": company}, fieldname="name" + ) if not group_stock_account: if not company_abbr: - company_abbr = frappe.get_cached_value("Company", company, 'abbr') + company_abbr = frappe.get_cached_value("Company", company, "abbr") group_stock_account = "Current Assets - " + company_abbr return group_stock_account diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 4c7f41dcb5e..c892ba3ddce 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -14,23 +14,25 @@ from erpnext.stock import get_warehouse_account class Warehouse(NestedSet): - nsm_parent_field = 'parent_warehouse' + nsm_parent_field = "parent_warehouse" def autoname(self): if self.company: - suffix = " - " + frappe.get_cached_value('Company', self.company, "abbr") + suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr") if not self.warehouse_name.endswith(suffix): self.name = self.warehouse_name + suffix else: self.name = self.warehouse_name def onload(self): - '''load account name for General Ledger Report''' - if self.company and cint(frappe.db.get_value("Company", self.company, "enable_perpetual_inventory")): + """load account name for General Ledger Report""" + if self.company and cint( + frappe.db.get_value("Company", self.company, "enable_perpetual_inventory") + ): account = self.account or get_warehouse_account(self) if account: - self.set_onload('account', account) + self.set_onload("account", account) load_address_and_contact(self) def on_update(self): @@ -43,9 +45,19 @@ class Warehouse(NestedSet): # delete bin bins = frappe.get_all("Bin", fields="*", filters={"warehouse": self.name}) for d in bins: - if d['actual_qty'] or d['reserved_qty'] or d['ordered_qty'] or \ - d['indented_qty'] or d['projected_qty'] or d['planned_qty']: - throw(_("Warehouse {0} can not be deleted as quantity exists for Item {1}").format(self.name, d['item_code'])) + if ( + d["actual_qty"] + or d["reserved_qty"] + or d["ordered_qty"] + or d["indented_qty"] + or d["projected_qty"] + or d["planned_qty"] + ): + throw( + _("Warehouse {0} can not be deleted as quantity exists for Item {1}").format( + self.name, d["item_code"] + ) + ) if self.check_if_sle_exists(): throw(_("Warehouse can not be deleted as stock ledger entry exists for this warehouse.")) @@ -90,23 +102,24 @@ class Warehouse(NestedSet): def unlink_from_items(self): frappe.db.set_value("Item Default", {"default_warehouse": self.name}, "default_warehouse", None) + @frappe.whitelist() def get_children(doctype, parent=None, company=None, is_root=False): if is_root: parent = "" - fields = ['name as value', 'is_group as expandable'] + fields = ["name as value", "is_group as expandable"] filters = [ - ['docstatus', '<', '2'], - ['ifnull(`parent_warehouse`, "")', '=', parent], - ['company', 'in', (company, None,'')] + ["docstatus", "<", "2"], + ['ifnull(`parent_warehouse`, "")', "=", parent], + ["company", "in", (company, None, "")], ] - warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by='name') + warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by="name") - company_currency = '' + company_currency = "" if company: - company_currency = frappe.get_cached_value('Company', company, 'default_currency') + company_currency = frappe.get_cached_value("Company", company, "default_currency") warehouse_wise_value = get_warehouse_wise_stock_value(company) @@ -117,14 +130,20 @@ def get_children(doctype, parent=None, company=None, is_root=False): wh["company_currency"] = company_currency return warehouses -def get_warehouse_wise_stock_value(company): - warehouses = frappe.get_all('Warehouse', - fields = ['name', 'parent_warehouse'], filters = {'company': company}) - parent_warehouse = {d.name : d.parent_warehouse for d in warehouses} - filters = {'warehouse': ('in', [data.name for data in warehouses])} - bin_data = frappe.get_all('Bin', fields = ['sum(stock_value) as stock_value', 'warehouse'], - filters = filters, group_by = 'warehouse') +def get_warehouse_wise_stock_value(company): + warehouses = frappe.get_all( + "Warehouse", fields=["name", "parent_warehouse"], filters={"company": company} + ) + parent_warehouse = {d.name: d.parent_warehouse for d in warehouses} + + filters = {"warehouse": ("in", [data.name for data in warehouses])} + bin_data = frappe.get_all( + "Bin", + fields=["sum(stock_value) as stock_value", "warehouse"], + filters=filters, + group_by="warehouse", + ) warehouse_wise_stock_value = defaultdict(float) for row in bin_data: @@ -132,23 +151,30 @@ def get_warehouse_wise_stock_value(company): continue warehouse_wise_stock_value[row.warehouse] = row.stock_value - update_value_in_parent_warehouse(warehouse_wise_stock_value, - parent_warehouse, row.warehouse, row.stock_value) + update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse, row.warehouse, row.stock_value + ) return warehouse_wise_stock_value -def update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value): + +def update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value +): parent_warehouse = parent_warehouse_dict.get(warehouse) if not parent_warehouse: return warehouse_wise_stock_value[parent_warehouse] += flt(stock_value) - update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, - parent_warehouse, stock_value) + update_value_in_parent_warehouse( + warehouse_wise_stock_value, parent_warehouse_dict, parent_warehouse, stock_value + ) + @frappe.whitelist() def add_node(): from frappe.desk.treeview import make_tree_args + args = make_tree_args(**frappe.form_dict) if cint(args.is_root): @@ -156,33 +182,37 @@ def add_node(): frappe.get_doc(args).insert() + @frappe.whitelist() def convert_to_group_or_ledger(docname=None): if not docname: docname = frappe.form_dict.docname return frappe.get_doc("Warehouse", docname).convert_to_group_or_ledger() + def get_child_warehouses(warehouse): from frappe.utils.nestedset import get_descendants_of children = get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft") - return children + [warehouse] # append self for backward compatibility + return children + [warehouse] # append self for backward compatibility + def get_warehouses_based_on_account(account, company=None): warehouses = [] - for d in frappe.get_all("Warehouse", fields = ["name", "is_group"], - filters = {"account": account}): + for d in frappe.get_all("Warehouse", fields=["name", "is_group"], filters={"account": account}): if d.is_group: warehouses.extend(get_child_warehouses(d.name)) else: warehouses.append(d.name) - if (not warehouses and company and - frappe.get_cached_value("Company", company, "default_inventory_account") == account): - warehouses = [d.name for d in frappe.get_all("Warehouse", filters={'is_group': 0})] + if ( + not warehouses + and company + and frappe.get_cached_value("Company", company, "default_inventory_account") == account + ): + warehouses = [d.name for d in frappe.get_all("Warehouse", filters={"is_group": 0})] if not warehouses: - frappe.throw(_("Warehouse not found against the account {0}") - .format(account)) + frappe.throw(_("Warehouse not found against the account {0}").format(account)) return warehouses diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 59f02e36114..19680f6286e 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -23,31 +23,38 @@ from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_fact from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no from erpnext.stock.doctype.price_list.price_list import get_price_list_details -sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', 'POS Invoice'] -purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] +sales_doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"] +purchase_doctypes = [ + "Material Request", + "Supplier Quotation", + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", +] + @frappe.whitelist() def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): """ - args = { - "item_code": "", - "warehouse": None, - "customer": "", - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "is_subcontracted": "Yes" / "No", - "ignore_pricing_rule": 0/1 - "project": "" - "set_warehouse": "" - } + args = { + "item_code": "", + "warehouse": None, + "customer": "", + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "is_subcontracted": "Yes" / "No", + "ignore_pricing_rule": 0/1 + "project": "" + "set_warehouse": "" + } """ args = process_args(args) @@ -61,16 +68,21 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if isinstance(doc, string_types): doc = json.loads(doc) - if doc and doc.get('doctype') == 'Purchase Invoice': - args['bill_date'] = doc.get('bill_date') + if doc and doc.get("doctype") == "Purchase Invoice": + args["bill_date"] = doc.get("bill_date") if doc: - args['posting_date'] = doc.get('posting_date') - args['transaction_date'] = doc.get('transaction_date') + args["posting_date"] = doc.get("posting_date") + args["transaction_date"] = doc.get("transaction_date") get_item_tax_template(args, item, out) - out["item_tax_rate"] = get_item_tax_map(args.company, args.get("item_tax_template") if out.get("item_tax_template") is None \ - else out.get("item_tax_template"), as_json=True) + out["item_tax_rate"] = get_item_tax_map( + args.company, + args.get("item_tax_template") + if out.get("item_tax_template") is None + else out.get("item_tax_template"), + as_json=True, + ) get_party_item_code(args, item, out) @@ -83,12 +95,14 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) - if (args.get("doctype") == "Material Request" and - args.get("material_request_type") == "Material Transfer"): + if ( + args.get("doctype") == "Material Request" + and args.get("material_request_type") == "Material Transfer" + ): out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) elif out.get("warehouse"): - if doc and doc.get('doctype') == 'Purchase Order': + if doc and doc.get("doctype") == "Purchase Order": # calculate company_total_stock only for po bin_details = get_bin_details(args.item_code, out.warehouse, args.company) else: @@ -101,31 +115,35 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.get(key) is None: args[key] = value - data = get_pricing_rule_for_item(args, out.price_list_rate, - doc, for_validate=for_validate) + data = get_pricing_rule_for_item(args, out.price_list_rate, doc, for_validate=for_validate) out.update(data) update_stock(args, out) if args.transaction_date and item.lead_time_days: - out.schedule_date = out.lead_time_date = add_days(args.transaction_date, - item.lead_time_days) + out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days) if args.get("is_subcontracted") == "Yes": - out.bom = args.get('bom') or get_default_bom(args.item_code) + out.bom = args.get("bom") or get_default_bom(args.item_code) get_gross_profit(out) - if args.doctype == 'Material Request': + if args.doctype == "Material Request": out.rate = args.rate or out.price_list_rate out.amount = flt(args.qty) * flt(out.rate) return out + def update_stock(args, out): - if (args.get("doctype") == "Delivery Note" or - (args.get("doctype") == "Sales Invoice" and args.get('update_stock'))) \ - and out.warehouse and out.stock_qty > 0: + if ( + ( + args.get("doctype") == "Delivery Note" + or (args.get("doctype") == "Sales Invoice" and args.get("update_stock")) + ) + and out.warehouse + and out.stock_qty > 0 + ): if out.has_batch_no and not args.get("batch_no"): out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty) @@ -133,9 +151,9 @@ def update_stock(args, out): if actual_batch_qty: out.update(actual_batch_qty) - if out.has_serial_no and args.get('batch_no'): + if out.has_serial_no and args.get("batch_no"): reserved_so = get_so_reservation_for_item(args) - out.batch_no = args.get('batch_no') + out.batch_no = args.get("batch_no") out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so) elif out.has_serial_no: @@ -149,13 +167,14 @@ def set_valuation_rate(out, args): bundled_items = frappe.get_doc("Product Bundle", args.item_code) for bundle_item in bundled_items.items: - valuation_rate += \ - flt(get_valuation_rate(bundle_item.item_code, args.company, out.get("warehouse")).get("valuation_rate") \ - * bundle_item.qty) + valuation_rate += flt( + get_valuation_rate(bundle_item.item_code, args.company, out.get("warehouse")).get( + "valuation_rate" + ) + * bundle_item.qty + ) - out.update({ - "valuation_rate": valuation_rate - }) + out.update({"valuation_rate": valuation_rate}) else: out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse"))) @@ -178,11 +197,13 @@ def process_args(args): set_transaction_type(args) return args + def process_string_args(args): if isinstance(args, string_types): args = json.loads(args) return args + @frappe.whitelist() def get_item_code(barcode=None, serial_no=None): if barcode: @@ -202,6 +223,7 @@ def validate_item_details(args, item): throw(_("Please specify Company")) from erpnext.stock.doctype.item.item import validate_end_of_life + validate_end_of_life(item.name, item.end_of_life, item.disabled) if args.transaction_type == "selling" and cint(item.has_variants): @@ -215,37 +237,37 @@ def validate_item_details(args, item): def get_basic_details(args, item, overwrite_warehouse=True): """ :param args: { - "item_code": "", - "warehouse": None, - "customer": "", - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "price_list_uom_dependant": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "is_subcontracted": "Yes" / "No", - "ignore_pricing_rule": 0/1 - "project": "", - barcode: "", - serial_no: "", - currency: "", - update_stock: "", - price_list: "", - company: "", - order_type: "", - is_pos: "", - project: "", - qty: "", - stock_qty: "", - conversion_factor: "", - against_blanket_order: 0/1 - } + "item_code": "", + "warehouse": None, + "customer": "", + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "price_list_uom_dependant": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "is_subcontracted": "Yes" / "No", + "ignore_pricing_rule": 0/1 + "project": "", + barcode: "", + serial_no: "", + currency: "", + update_stock: "", + price_list: "", + company: "", + order_type: "", + is_pos: "", + project: "", + qty: "", + stock_qty: "", + conversion_factor: "", + against_blanket_order: 0/1 + } :param item: `item_code` of Item object :return: frappe._dict """ @@ -260,77 +282,98 @@ def get_basic_details(args, item, overwrite_warehouse=True): item_group_defaults = get_item_group_defaults(item.name, args.company) brand_defaults = get_brand_defaults(item.name, args.company) - defaults = frappe._dict({ - 'item_defaults': item_defaults, - 'item_group_defaults': item_group_defaults, - 'brand_defaults': brand_defaults - }) + defaults = frappe._dict( + { + "item_defaults": item_defaults, + "item_group_defaults": item_group_defaults, + "brand_defaults": brand_defaults, + } + ) warehouse = get_item_warehouse(item, args, overwrite_warehouse, defaults) - if args.get('doctype') == "Material Request" and not args.get('material_request_type'): - args['material_request_type'] = frappe.db.get_value('Material Request', - args.get('name'), 'material_request_type', cache=True) + if args.get("doctype") == "Material Request" and not args.get("material_request_type"): + args["material_request_type"] = frappe.db.get_value( + "Material Request", args.get("name"), "material_request_type", cache=True + ) expense_account = None - if args.get('doctype') == 'Purchase Invoice' and item.is_fixed_asset: + if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset: from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account - expense_account = get_asset_category_account(fieldname = "fixed_asset_account", item = args.item_code, company= args.company) - #Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master - if not args.get('uom'): - if args.get('doctype') in sales_doctypes: + expense_account = get_asset_category_account( + fieldname="fixed_asset_account", item=args.item_code, company=args.company + ) + + # Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master + if not args.get("uom"): + if args.get("doctype") in sales_doctypes: args.uom = item.sales_uom if item.sales_uom else item.stock_uom - elif (args.get('doctype') in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']) or \ - (args.get('doctype') == 'Material Request' and args.get('material_request_type') == 'Purchase'): + elif (args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]) or ( + args.get("doctype") == "Material Request" and args.get("material_request_type") == "Purchase" + ): args.uom = item.purchase_uom if item.purchase_uom else item.stock_uom else: args.uom = item.stock_uom - if (args.get("batch_no") and - item.name != frappe.get_cached_value('Batch', args.get("batch_no"), 'item')): - args['batch_no'] = '' + if args.get("batch_no") and item.name != frappe.get_cached_value( + "Batch", args.get("batch_no"), "item" + ): + args["batch_no"] = "" - out = frappe._dict({ - "item_code": item.name, - "item_name": item.item_name, - "description": cstr(item.description).strip(), - "image": cstr(item.image).strip(), - "warehouse": warehouse, - "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), - "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , - "discount_account": get_default_discount_account(args, item_defaults), - "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), - 'has_serial_no': item.has_serial_no, - 'has_batch_no': item.has_batch_no, - "batch_no": args.get("batch_no"), - "uom": args.uom, - "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", - "qty": flt(args.qty) or 1.0, - "stock_qty": flt(args.qty) or 1.0, - "price_list_rate": 0.0, - "base_price_list_rate": 0.0, - "rate": 0.0, - "base_rate": 0.0, - "amount": 0.0, - "base_amount": 0.0, - "net_rate": 0.0, - "net_amount": 0.0, - "discount_percentage": 0.0, - "discount_amount": 0.0, - "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), - "update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0, - "delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0, - "is_fixed_asset": item.is_fixed_asset, - "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, - "transaction_date": args.get("transaction_date"), - "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"), - "grant_commission": item.get("grant_commission") - }) + out = frappe._dict( + { + "item_code": item.name, + "item_name": item.item_name, + "description": cstr(item.description).strip(), + "image": cstr(item.image).strip(), + "warehouse": warehouse, + "income_account": get_default_income_account( + args, item_defaults, item_group_defaults, brand_defaults + ), + "expense_account": expense_account + or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), + "discount_account": get_default_discount_account(args, item_defaults), + "cost_center": get_default_cost_center( + args, item_defaults, item_group_defaults, brand_defaults + ), + "has_serial_no": item.has_serial_no, + "has_batch_no": item.has_batch_no, + "batch_no": args.get("batch_no"), + "uom": args.uom, + "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", + "qty": flt(args.qty) or 1.0, + "stock_qty": flt(args.qty) or 1.0, + "price_list_rate": 0.0, + "base_price_list_rate": 0.0, + "rate": 0.0, + "base_rate": 0.0, + "amount": 0.0, + "base_amount": 0.0, + "net_rate": 0.0, + "net_amount": 0.0, + "discount_percentage": 0.0, + "discount_amount": 0.0, + "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), + "update_stock": args.get("update_stock") + if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"] + else 0, + "delivered_by_supplier": item.delivered_by_supplier + if args.get("doctype") in ["Sales Order", "Sales Invoice"] + else 0, + "is_fixed_asset": item.is_fixed_asset, + "last_purchase_rate": item.last_purchase_rate + if args.get("doctype") in ["Purchase Order"] + else 0, + "transaction_date": args.get("transaction_date"), + "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"), + "grant_commission": item.get("grant_commission"), + } + ) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): out.update(calculate_service_end_date(args, item)) @@ -339,26 +382,31 @@ def get_basic_details(args, item, overwrite_warehouse=True): if item.stock_uom == args.uom: out.conversion_factor = 1.0 else: - out.conversion_factor = args.conversion_factor or \ - get_conversion_factor(item.name, args.uom).get("conversion_factor") + out.conversion_factor = args.conversion_factor or get_conversion_factor(item.name, args.uom).get( + "conversion_factor" + ) args.conversion_factor = out.conversion_factor out.stock_qty = out.qty * out.conversion_factor args.stock_qty = out.stock_qty # calculate last purchase rate - if args.get('doctype') in purchase_doctypes: + if args.get("doctype") in purchase_doctypes: from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate - out.last_purchase_rate = item_last_purchase_rate(args.name, args.conversion_rate, item.name, out.conversion_factor) + + out.last_purchase_rate = item_last_purchase_rate( + args.name, args.conversion_rate, item.name, out.conversion_factor + ) # if default specified in item is for another company, fetch from company for d in [ ["Account", "income_account", "default_income_account"], ["Account", "expense_account", "default_expense_account"], ["Cost Center", "cost_center", "cost_center"], - ["Warehouse", "warehouse", ""]]: - if not out[d[1]]: - out[d[1]] = frappe.get_cached_value('Company', args.company, d[2]) if d[2] else None + ["Warehouse", "warehouse", ""], + ]: + if not out[d[1]]: + out[d[1]] = frappe.get_cached_value("Company", args.company, d[2]) if d[2] else None for fieldname in ("item_name", "item_group", "brand", "stock_uom"): out[fieldname] = item.get(fieldname) @@ -371,53 +419,58 @@ def get_basic_details(args, item, overwrite_warehouse=True): out["manufacturer_part_no"] = None out["manufacturer"] = None else: - data = frappe.get_value("Item", item.name, - ["default_item_manufacturer", "default_manufacturer_part_no"] , as_dict=1) + data = frappe.get_value( + "Item", item.name, ["default_item_manufacturer", "default_manufacturer_part_no"], as_dict=1 + ) if data: - out.update({ - "manufacturer": data.default_item_manufacturer, - "manufacturer_part_no": data.default_manufacturer_part_no - }) + out.update( + { + "manufacturer": data.default_item_manufacturer, + "manufacturer_part_no": data.default_manufacturer_part_no, + } + ) - child_doctype = args.doctype + ' Item' + child_doctype = args.doctype + " Item" meta = frappe.get_meta(child_doctype) if meta.get_field("barcode"): update_barcode_value(out) if out.get("weight_per_unit"): - out['total_weight'] = out.weight_per_unit * out.stock_qty + out["total_weight"] = out.weight_per_unit * out.stock_qty return out + def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): if not defaults: - defaults = frappe._dict({ - 'item_defaults' : get_item_defaults(item.name, args.company), - 'item_group_defaults' : get_item_group_defaults(item.name, args.company), - 'brand_defaults' : get_brand_defaults(item.name, args.company) - }) + defaults = frappe._dict( + { + "item_defaults": get_item_defaults(item.name, args.company), + "item_group_defaults": get_item_group_defaults(item.name, args.company), + "brand_defaults": get_brand_defaults(item.name, args.company), + } + ) if overwrite_warehouse or not args.warehouse: warehouse = ( - args.get("set_warehouse") or - defaults.item_defaults.get("default_warehouse") or - defaults.item_group_defaults.get("default_warehouse") or - defaults.brand_defaults.get("default_warehouse") or - args.get('warehouse') + args.get("set_warehouse") + or defaults.item_defaults.get("default_warehouse") + or defaults.item_group_defaults.get("default_warehouse") + or defaults.brand_defaults.get("default_warehouse") + or args.get("warehouse") ) if not warehouse: defaults = frappe.defaults.get_defaults() or {} - warehouse_exists = frappe.db.exists("Warehouse", { - 'name': defaults.default_warehouse, - 'company': args.company - }) + warehouse_exists = frappe.db.exists( + "Warehouse", {"name": defaults.default_warehouse, "company": args.company} + ) if defaults.get("default_warehouse") and warehouse_exists: warehouse = defaults.default_warehouse else: - warehouse = args.get('warehouse') + warehouse = args.get("warehouse") if not warehouse: default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") @@ -426,12 +479,14 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults=None): return warehouse + def update_barcode_value(out): barcode_data = get_barcode_data([out]) # If item has one barcode then update the value of the barcode field if barcode_data and len(barcode_data.get(out.item_code)) == 1: - out['barcode'] = barcode_data.get(out.item_code)[0] + out["barcode"] = barcode_data.get(out.item_code)[0] + def get_barcode_data(items_list): # get itemwise batch no data @@ -440,9 +495,13 @@ def get_barcode_data(items_list): itemwise_barcode = {} for item in items_list: - barcodes = frappe.db.sql(""" + barcodes = frappe.db.sql( + """ select barcode from `tabItem Barcode` where parent = %s - """, item.item_code, as_dict=1) + """, + item.item_code, + as_dict=1, + ) for barcode in barcodes: if item.item_code not in itemwise_barcode: @@ -451,6 +510,7 @@ def get_barcode_data(items_list): return itemwise_barcode + @frappe.whitelist() def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_tax_templates=None): out = {} @@ -476,22 +536,29 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t out[item_code[1]] = {} item = frappe.get_cached_doc("Item", item_code[0]) - args = {"company": company, "tax_category": tax_category, "net_rate": item_rates.get(item_code[1])} + args = { + "company": company, + "tax_category": tax_category, + "net_rate": item_rates.get(item_code[1]), + } if item_tax_templates: args.update({"item_tax_template": item_tax_templates.get(item_code[1])}) get_item_tax_template(args, item, out[item_code[1]]) - out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True) + out[item_code[1]]["item_tax_rate"] = get_item_tax_map( + company, out[item_code[1]].get("item_tax_template"), as_json=True + ) return out + def get_item_tax_template(args, item, out): """ - args = { - "tax_category": None - "item_tax_template": None - } + args = { + "tax_category": None + "item_tax_template": None + } """ item_tax_template = None if item.taxes: @@ -504,6 +571,7 @@ def get_item_tax_template(args, item, out): item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_group = item_group_doc.parent_item_group + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: out = {} @@ -511,36 +579,43 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: - tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, 'company') - if tax_company == args['company']: - if (tax.valid_from or tax.maximum_net_rate): + tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company") + if tax_company == args["company"]: + if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date - validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date') + validation_date = ( + args.get("transaction_date") or args.get("bill_date") or args.get("posting_date") + ) - if getdate(tax.valid_from) <= getdate(validation_date) \ - and is_within_valid_range(args, tax): + if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(args, tax): taxes_with_validity.append(tax) else: taxes_with_no_validity.append(tax) if taxes_with_validity: - taxes = sorted(taxes_with_validity, key = lambda i: i.valid_from, reverse=True) + taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from, reverse=True) else: taxes = taxes_with_no_validity if for_validate: - return [tax.item_tax_template for tax in taxes if (cstr(tax.tax_category) == cstr(args.get('tax_category')) \ - and (tax.item_tax_template not in taxes))] + return [ + tax.item_tax_template + for tax in taxes + if ( + cstr(tax.tax_category) == cstr(args.get("tax_category")) + and (tax.item_tax_template not in taxes) + ) + ] # all templates have validity and no template is valid if not taxes_with_validity and (not taxes_with_no_validity): return None # do not change if already a valid template - if args.get('item_tax_template') in {t.item_tax_template for t in taxes}: - out["item_tax_template"] = args.get('item_tax_template') - return args.get('item_tax_template') + if args.get("item_tax_template") in {t.item_tax_template for t in taxes}: + out["item_tax_template"] = args.get("item_tax_template") + return args.get("item_tax_template") for tax in taxes: if cstr(tax.tax_category) == cstr(args.get("tax_category")): @@ -548,15 +623,17 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): return tax.item_tax_template return None + def is_within_valid_range(args, tax): if not flt(tax.maximum_net_rate): # No range specified, just ignore return True - elif flt(tax.minimum_net_rate) <= flt(args.get('net_rate')) <= flt(tax.maximum_net_rate): + elif flt(tax.minimum_net_rate) <= flt(args.get("net_rate")) <= flt(tax.maximum_net_rate): return True return False + @frappe.whitelist() def get_item_tax_map(company, item_tax_template, as_json=True): item_tax_map = {} @@ -568,6 +645,7 @@ def get_item_tax_map(company, item_tax_template, as_json=True): return json.dumps(item_tax_map) if as_json else item_tax_map + @frappe.whitelist() def calculate_service_end_date(args, item=None): args = process_args(args) @@ -586,53 +664,68 @@ def calculate_service_end_date(args, item=None): service_start_date = args.service_start_date if args.service_start_date else args.transaction_date service_end_date = add_months(service_start_date, item.get(no_of_months)) - deferred_detail = { - "service_start_date": service_start_date, - "service_end_date": service_end_date - } + deferred_detail = {"service_start_date": service_start_date, "service_end_date": service_end_date} deferred_detail[enable_deferred] = item.get(enable_deferred) deferred_detail[account] = get_default_deferred_account(args, item, fieldname=account) return deferred_detail + def get_default_income_account(args, item, item_group, brand): - return (item.get("income_account") + return ( + item.get("income_account") or item_group.get("income_account") or brand.get("income_account") - or args.income_account) + or args.income_account + ) + def get_default_expense_account(args, item, item_group, brand): - return (item.get("expense_account") + return ( + item.get("expense_account") or item_group.get("expense_account") or brand.get("expense_account") - or args.expense_account) + or args.expense_account + ) + def get_default_discount_account(args, item): - return (item.get("default_discount_account") - or args.discount_account) + return item.get("default_discount_account") or args.discount_account + def get_default_deferred_account(args, item, fieldname=None): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): - return (item.get(fieldname) + return ( + item.get(fieldname) or args.get(fieldname) - or frappe.get_cached_value('Company', args.company, "default_"+fieldname)) + or frappe.get_cached_value("Company", args.company, "default_" + fieldname) + ) else: return None + def get_default_cost_center(args, item=None, item_group=None, brand=None, company=None): cost_center = None if not company and args.get("company"): company = args.get("company") - if args.get('project'): + if args.get("project"): cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True) if not cost_center and (item and item_group and brand): - if args.get('customer'): - cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center') + if args.get("customer"): + cost_center = ( + item.get("selling_cost_center") + or item_group.get("selling_cost_center") + or brand.get("selling_cost_center") + ) else: - cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center') + cost_center = ( + item.get("buying_cost_center") + or item_group.get("buying_cost_center") + or brand.get("buying_cost_center") + ) elif not cost_center and args.get("item_code") and company: for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]: @@ -645,20 +738,26 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan if not cost_center and args.get("cost_center"): cost_center = args.get("cost_center") - if (company and cost_center - and frappe.get_cached_value("Cost Center", cost_center, "company") != company): + if ( + company + and cost_center + and frappe.get_cached_value("Cost Center", cost_center, "company") != company + ): return None if not cost_center and company: - cost_center = frappe.get_cached_value("Company", - company, "cost_center") + cost_center = frappe.get_cached_value("Company", company, "cost_center") return cost_center + def get_default_supplier(args, item, item_group, brand): - return (item.get("default_supplier") + return ( + item.get("default_supplier") or item_group.get("default_supplier") - or brand.get("default_supplier")) + or brand.get("default_supplier") + ) + def get_price_list_rate(args, item_doc, out=None): if out is None: @@ -666,7 +765,7 @@ def get_price_list_rate(args, item_doc, out=None): meta = frappe.get_meta(args.parenttype or args.doctype) - if meta.get_field("currency") or args.get('currency'): + if meta.get_field("currency") or args.get("currency"): if not args.get("price_list_currency") or not args.get("plc_conversion_rate"): # if currency and plc_conversion_rate exist then # `get_price_list_currency_and_exchange_rate` has already been called @@ -688,54 +787,72 @@ def get_price_list_rate(args, item_doc, out=None): insert_item_price(args) return out - out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \ - / flt(args.conversion_rate) + out.price_list_rate = ( + flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) + ) - if not out.price_list_rate and args.transaction_type=="buying": + if not out.price_list_rate and args.transaction_type == "buying": from erpnext.stock.doctype.item.item import get_last_purchase_details - out.update(get_last_purchase_details(item_doc.name, - args.name, args.conversion_rate)) + + out.update(get_last_purchase_details(item_doc.name, args.name, args.conversion_rate)) return out + def insert_item_price(args): """Insert Item Price if Price List and Price List Rate are specified and currency is the same""" - if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency \ - and cint(frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")): + if frappe.db.get_value( + "Price List", args.price_list, "currency", cache=True + ) == args.currency and cint( + frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") + ): if frappe.has_permission("Item Price", "write"): - price_list_rate = (args.rate / args.get('conversion_factor') - if args.get("conversion_factor") else args.rate) + price_list_rate = ( + args.rate / args.get("conversion_factor") if args.get("conversion_factor") else args.rate + ) - item_price = frappe.db.get_value('Item Price', - {'item_code': args.item_code, 'price_list': args.price_list, 'currency': args.currency}, - ['name', 'price_list_rate'], as_dict=1) + item_price = frappe.db.get_value( + "Item Price", + {"item_code": args.item_code, "price_list": args.price_list, "currency": args.currency}, + ["name", "price_list_rate"], + as_dict=1, + ) if item_price and item_price.name: - if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value('Stock Settings', 'update_existing_price_list_rate'): - frappe.db.set_value('Item Price', item_price.name, "price_list_rate", price_list_rate) - frappe.msgprint(_("Item Price updated for {0} in Price List {1}").format(args.item_code, - args.price_list), alert=True) + if item_price.price_list_rate != price_list_rate and frappe.db.get_single_value( + "Stock Settings", "update_existing_price_list_rate" + ): + frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate) + frappe.msgprint( + _("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) else: - item_price = frappe.get_doc({ - "doctype": "Item Price", - "price_list": args.price_list, - "item_code": args.item_code, - "currency": args.currency, - "price_list_rate": price_list_rate - }) + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": args.price_list, + "item_code": args.item_code, + "currency": args.currency, + "price_list_rate": price_list_rate, + } + ) item_price.insert() - frappe.msgprint(_("Item Price added for {0} in Price List {1}").format(args.item_code, - args.price_list), alert=True) + frappe.msgprint( + _("Item Price added for {0} in Price List {1}").format(args.item_code, args.price_list), + alert=True, + ) + def get_item_price(args, item_code, ignore_party=False): """ - Get name, price_list_rate from Item Price based on conditions - Check if the desired qty is within the increment of the packing list. - :param args: dict (or frappe._dict) with mandatory fields price_list, uom - optional fields transaction_date, customer, supplier - :param item_code: str, Item Doctype field item_code + Get name, price_list_rate from Item Price based on conditions + Check if the desired qty is within the increment of the packing list. + :param args: dict (or frappe._dict) with mandatory fields price_list, uom + optional fields transaction_date, customer, supplier + :param item_code: str, Item Doctype field item_code """ - args['item_code'] = item_code + args["item_code"] = item_code conditions = """where item_code=%(item_code)s and price_list=%(price_list)s @@ -751,36 +868,42 @@ def get_item_price(args, item_code, ignore_party=False): else: conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" - if args.get('transaction_date'): + if args.get("transaction_date"): conditions += """ and %(transaction_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - if args.get('posting_date'): + if args.get("posting_date"): conditions += """ and %(posting_date)s between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" - return frappe.db.sql(""" select name, price_list_rate, uom + return frappe.db.sql( + """ select name, price_list_rate, uom from `tabItem Price` {conditions} - order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args) + order by valid_from desc, batch_no desc, uom desc """.format( + conditions=conditions + ), + args, + ) + def get_price_list_rate_for(args, item_code): """ - :param customer: link to Customer DocType - :param supplier: link to Supplier DocType - :param price_list: str (Standard Buying or Standard Selling) - :param item_code: str, Item Doctype field item_code - :param qty: Desired Qty - :param transaction_date: Date of the price + :param customer: link to Customer DocType + :param supplier: link to Supplier DocType + :param price_list: str (Standard Buying or Standard Selling) + :param item_code: str, Item Doctype field item_code + :param qty: Desired Qty + :param transaction_date: Date of the price """ item_price_args = { - "item_code": item_code, - "price_list": args.get('price_list'), - "customer": args.get('customer'), - "supplier": args.get('supplier'), - "uom": args.get('uom'), - "transaction_date": args.get('transaction_date'), - "posting_date": args.get('posting_date'), - "batch_no": args.get('batch_no') + "item_code": item_code, + "price_list": args.get("price_list"), + "customer": args.get("customer"), + "supplier": args.get("supplier"), + "uom": args.get("uom"), + "transaction_date": args.get("transaction_date"), + "posting_date": args.get("posting_date"), + "batch_no": args.get("batch_no"), } item_price_data = 0 @@ -793,12 +916,15 @@ def get_price_list_rate_for(args, item_code): for field in ["customer", "supplier"]: del item_price_args[field] - general_price_list_rate = get_item_price(item_price_args, item_code, - ignore_party=args.get("ignore_party")) + general_price_list_rate = get_item_price( + item_price_args, item_code, ignore_party=args.get("ignore_party") + ) if not general_price_list_rate and args.get("uom") != args.get("stock_uom"): item_price_args["uom"] = args.get("stock_uom") - general_price_list_rate = get_item_price(item_price_args, item_code, ignore_party=args.get("ignore_party")) + general_price_list_rate = get_item_price( + item_price_args, item_code, ignore_party=args.get("ignore_party") + ) if general_price_list_rate: item_price_data = general_price_list_rate @@ -806,18 +932,19 @@ def get_price_list_rate_for(args, item_code): if item_price_data: if item_price_data[0][2] == args.get("uom"): return item_price_data[0][1] - elif not args.get('price_list_uom_dependant'): + elif not args.get("price_list_uom_dependant"): return flt(item_price_data[0][1] * flt(args.get("conversion_factor", 1))) else: return item_price_data[0][1] + def check_packing_list(price_list_rate_name, desired_qty, item_code): """ - Check if the desired qty is within the increment of the packing list. - :param price_list_rate_name: Name of Item Price - :param desired_qty: Desired Qt - :param item_code: str, Item Doctype field item_code - :param qty: Desired Qt + Check if the desired qty is within the increment of the packing list. + :param price_list_rate_name: Name of Item Price + :param desired_qty: Desired Qt + :param item_code: str, Item Doctype field item_code + :param qty: Desired Qt """ flag = True @@ -830,47 +957,62 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): return flag + def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate - company_currency = frappe.get_cached_value('Company', args.company, "default_currency") - if (not args.conversion_rate and args.currency==company_currency): + company_currency = frappe.get_cached_value("Company", args.company, "default_currency") + if not args.conversion_rate and args.currency == company_currency: args.conversion_rate = 1.0 - if (not args.ignore_conversion_rate and args.conversion_rate == 1 and args.currency!=company_currency): - args.conversion_rate = get_exchange_rate(args.currency, - company_currency, args.transaction_date, "for_buying") or 1.0 + if ( + not args.ignore_conversion_rate + and args.conversion_rate == 1 + and args.currency != company_currency + ): + args.conversion_rate = ( + get_exchange_rate(args.currency, company_currency, args.transaction_date, "for_buying") or 1.0 + ) # validate currency conversion rate - validate_conversion_rate(args.currency, args.conversion_rate, - meta.get_label("conversion_rate"), args.company) + validate_conversion_rate( + args.currency, args.conversion_rate, meta.get_label("conversion_rate"), args.company + ) - args.conversion_rate = flt(args.conversion_rate, - get_field_precision(meta.get_field("conversion_rate"), - frappe._dict({"fields": args}))) + args.conversion_rate = flt( + args.conversion_rate, + get_field_precision(meta.get_field("conversion_rate"), frappe._dict({"fields": args})), + ) if args.price_list: - if (not args.plc_conversion_rate - and args.price_list_currency==frappe.db.get_value("Price List", args.price_list, "currency", cache=True)): + if not args.plc_conversion_rate and args.price_list_currency == frappe.db.get_value( + "Price List", args.price_list, "currency", cache=True + ): args.plc_conversion_rate = 1.0 # validate price list currency conversion rate if not args.get("price_list_currency"): throw(_("Price List Currency not selected")) else: - validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate, - meta.get_label("plc_conversion_rate"), args.company) + validate_conversion_rate( + args.price_list_currency, + args.plc_conversion_rate, + meta.get_label("plc_conversion_rate"), + args.company, + ) if meta.get_field("plc_conversion_rate"): - args.plc_conversion_rate = flt(args.plc_conversion_rate, - get_field_precision(meta.get_field("plc_conversion_rate"), - frappe._dict({"fields": args}))) + args.plc_conversion_rate = flt( + args.plc_conversion_rate, + get_field_precision(meta.get_field("plc_conversion_rate"), frappe._dict({"fields": args})), + ) + def get_party_item_code(args, item_doc, out): - if args.transaction_type=="selling" and args.customer: + if args.transaction_type == "selling" and args.customer: out.customer_item_code = None - if args.quotation_to and args.quotation_to != 'Customer': + if args.quotation_to and args.quotation_to != "Customer": return customer_item_code = item_doc.get("customer_items", {"customer_name": args.customer}) @@ -883,15 +1025,16 @@ def get_party_item_code(args, item_doc, out): if customer_group_item_code and not customer_group_item_code[0].customer_name: out.customer_item_code = customer_group_item_code[0].ref_code - if args.transaction_type=="buying" and args.supplier: + if args.transaction_type == "buying" and args.supplier: item_supplier = item_doc.get("supplier_items", {"supplier": args.supplier}) out.supplier_part_no = item_supplier[0].supplier_part_no if item_supplier else None + def get_pos_profile_item_details(company, args, pos_profile=None, update_data=False): res = frappe._dict() if not frappe.flags.pos_profile and not pos_profile: - pos_profile = frappe.flags.pos_profile = get_pos_profile(company, args.get('pos_profile')) + pos_profile = frappe.flags.pos_profile = get_pos_profile(company, args.get("pos_profile")) if pos_profile: for fieldname in ("income_account", "cost_center", "warehouse", "expense_account"): @@ -903,70 +1046,89 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa return res + @frappe.whitelist() def get_pos_profile(company, pos_profile=None, user=None): - if pos_profile: return frappe.get_cached_doc('POS Profile', pos_profile) + if pos_profile: + return frappe.get_cached_doc("POS Profile", pos_profile) if not user: - user = frappe.session['user'] + user = frappe.session["user"] condition = "pfu.user = %(user)s AND pfu.default=1" if user and company: condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1" - pos_profile = frappe.db.sql("""SELECT pf.* + pos_profile = frappe.db.sql( + """SELECT pf.* FROM `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu ON pf.name = pfu.parent WHERE {cond} AND pf.disabled = 0 - """.format(cond = condition), { - 'user': user, - 'company': company - }, as_dict=1) + """.format( + cond=condition + ), + {"user": user, "company": company}, + as_dict=1, + ) if not pos_profile and company: - pos_profile = frappe.db.sql("""SELECT pf.* + pos_profile = frappe.db.sql( + """SELECT pf.* FROM `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu ON pf.name = pfu.parent WHERE pf.company = %(company)s AND pf.disabled = 0 - """, { - 'company': company - }, as_dict=1) + """, + {"company": company}, + as_dict=1, + ) return pos_profile and pos_profile[0] or None + def get_serial_nos_by_fifo(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join(frappe.db.sql_list("""select name from `tabSerial No` + return "\n".join( + frappe.db.sql_list( + """select name from `tabSerial No` where item_code=%(item_code)s and warehouse=%(warehouse)s and sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) order by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", - { - "item_code": args.item_code, - "warehouse": args.warehouse, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order - })) + { + "item_code": args.item_code, + "warehouse": args.warehouse, + "qty": abs(cint(args.stock_qty)), + "sales_order": sales_order, + }, + ) + ) + def get_serial_no_batchwise(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join(frappe.db.sql_list("""select name from `tabSerial No` + return "\n".join( + frappe.db.sql_list( + """select name from `tabSerial No` where item_code=%(item_code)s and warehouse=%(warehouse)s and sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order - by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", { - "item_code": args.item_code, - "warehouse": args.warehouse, - "batch_no": args.batch_no, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order - })) + by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", + { + "item_code": args.item_code, + "warehouse": args.warehouse, + "batch_no": args.batch_no, + "qty": abs(cint(args.stock_qty)), + "sales_order": sales_order, + }, + ) + ) + @frappe.whitelist() def get_conversion_factor(item_code, uom): @@ -974,69 +1136,94 @@ def get_conversion_factor(item_code, uom): filters = {"parent": item_code, "uom": uom} if variant_of: filters["parent"] = ("in", (item_code, variant_of)) - conversion_factor = frappe.db.get_value("UOM Conversion Detail", - filters, "conversion_factor") + conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") if not conversion_factor: stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") conversion_factor = get_uom_conv_factor(uom, stock_uom) return {"conversion_factor": conversion_factor or 1.0} + @frappe.whitelist() def get_projected_qty(item_code, warehouse): - return {"projected_qty": frappe.db.get_value("Bin", - {"item_code": item_code, "warehouse": warehouse}, "projected_qty")} + return { + "projected_qty": frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty" + ) + } + @frappe.whitelist() def get_bin_details(item_code, warehouse, company=None): - bin_details = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \ - or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} + bin_details = frappe.db.get_value( + "Bin", + {"item_code": item_code, "warehouse": warehouse}, + ["projected_qty", "actual_qty", "reserved_qty"], + as_dict=True, + cache=True, + ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} if company: - bin_details['company_total_stock'] = get_company_total_stock(item_code, company) + bin_details["company_total_stock"] = get_company_total_stock(item_code, company) return bin_details + def get_company_total_stock(item_code, company): - return frappe.db.sql("""SELECT sum(actual_qty) from + return frappe.db.sql( + """SELECT sum(actual_qty) from (`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name) WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""", - (company, item_code))[0][0] + (company, item_code), + )[0][0] + @frappe.whitelist() def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty, "serial_no":serial_no}) + args = frappe._dict( + {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no} + ) serial_no = get_serial_no(args) - return {'serial_no': serial_no} + return {"serial_no": serial_no} + @frappe.whitelist() -def get_bin_details_and_serial_nos(item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None): +def get_bin_details_and_serial_nos( + item_code, warehouse, has_batch_no=None, stock_qty=None, serial_no=None +): bin_details_and_serial_nos = {} bin_details_and_serial_nos.update(get_bin_details(item_code, warehouse)) if flt(stock_qty) > 0: if has_batch_no: - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty}) + args = frappe._dict({"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty}) serial_no = get_serial_no(args) - bin_details_and_serial_nos.update({'serial_no': serial_no}) + bin_details_and_serial_nos.update({"serial_no": serial_no}) return bin_details_and_serial_nos - bin_details_and_serial_nos.update(get_serial_no_details(item_code, warehouse, stock_qty, serial_no)) + bin_details_and_serial_nos.update( + get_serial_no_details(item_code, warehouse, stock_qty, serial_no) + ) return bin_details_and_serial_nos + @frappe.whitelist() def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_serial_no): batch_qty_and_serial_no = {} batch_qty_and_serial_no.update(get_batch_qty(batch_no, warehouse, item_code)) - if (flt(batch_qty_and_serial_no.get('actual_batch_qty')) >= flt(stock_qty)) and has_serial_no: - args = frappe._dict({"item_code":item_code, "warehouse":warehouse, "stock_qty":stock_qty, "batch_no":batch_no}) + if (flt(batch_qty_and_serial_no.get("actual_batch_qty")) >= flt(stock_qty)) and has_serial_no: + args = frappe._dict( + {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "batch_no": batch_no} + ) serial_no = get_serial_no(args) - batch_qty_and_serial_no.update({'serial_no': serial_no}) + batch_qty_and_serial_no.update({"serial_no": serial_no}) return batch_qty_and_serial_no + @frappe.whitelist() def get_batch_qty(batch_no, warehouse, item_code): from erpnext.stock.doctype.batch import batch + if batch_no: - return {'actual_batch_qty': batch.get_batch_qty(batch_no, warehouse)} + return {"actual_batch_qty": batch.get_batch_qty(batch_no, warehouse)} + @frappe.whitelist() def apply_price_list(args, as_doc=False): @@ -1046,23 +1233,23 @@ def apply_price_list(args, as_doc=False): :param args: See below :param as_doc: Updates value in the passed dict - args = { - "doctype": "", - "name": "", - "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], - "conversion_rate": 1.0, - "selling_price_list": None, - "price_list_currency": None, - "price_list_uom_dependant": None, - "plc_conversion_rate": 1.0, - "doctype": "", - "name": "", - "supplier": None, - "transaction_date": None, - "conversion_rate": 1.0, - "buying_price_list": None, - "ignore_pricing_rule": 0/1 - } + args = { + "doctype": "", + "name": "", + "items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...], + "conversion_rate": 1.0, + "selling_price_list": None, + "price_list_currency": None, + "price_list_uom_dependant": None, + "plc_conversion_rate": 1.0, + "doctype": "", + "name": "", + "supplier": None, + "transaction_date": None, + "conversion_rate": 1.0, + "buying_price_list": None, + "ignore_pricing_rule": 0/1 + } """ args = process_args(args) @@ -1082,10 +1269,10 @@ def apply_price_list(args, as_doc=False): children.append(item_details) if as_doc: - args.price_list_currency = parent.price_list_currency, + args.price_list_currency = (parent.price_list_currency,) args.plc_conversion_rate = parent.plc_conversion_rate - if args.get('items'): - for i, item in enumerate(args.get('items')): + if args.get("items"): + for i, item in enumerate(args.get("items")): for fieldname in children[i]: # if the field exists in the original doc # update the value @@ -1093,26 +1280,25 @@ def apply_price_list(args, as_doc=False): item[fieldname] = children[i][fieldname] return args else: - return { - "parent": parent, - "children": children - } + return {"parent": parent, "children": children} + def apply_price_list_on_item(args): - item_doc = frappe.db.get_value("Item", args.item_code, ['name', 'variant_of'], as_dict=1) + item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) item_details = get_price_list_rate(args, item_doc) item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate)) return item_details + def get_price_list_currency_and_exchange_rate(args): if not args.price_list: return {} - if args.doctype in ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']: + if args.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]: args.update({"exchange_rate": "for_selling"}) - elif args.doctype in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']: + elif args.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: args.update({"exchange_rate": "for_buying"}) price_list_details = get_price_list_details(args.price_list) @@ -1123,25 +1309,38 @@ def get_price_list_currency_and_exchange_rate(args): plc_conversion_rate = args.plc_conversion_rate company_currency = get_company_currency(args.company) - if (not plc_conversion_rate) or (price_list_currency and args.price_list_currency \ - and price_list_currency != args.price_list_currency): - # cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate - plc_conversion_rate = get_exchange_rate(price_list_currency, company_currency, - args.transaction_date, args.exchange_rate) or plc_conversion_rate + if (not plc_conversion_rate) or ( + price_list_currency + and args.price_list_currency + and price_list_currency != args.price_list_currency + ): + # cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate + plc_conversion_rate = ( + get_exchange_rate( + price_list_currency, company_currency, args.transaction_date, args.exchange_rate + ) + or plc_conversion_rate + ) + + return frappe._dict( + { + "price_list_currency": price_list_currency, + "price_list_uom_dependant": price_list_uom_dependant, + "plc_conversion_rate": plc_conversion_rate or 1, + } + ) - return frappe._dict({ - "price_list_currency": price_list_currency, - "price_list_uom_dependant": price_list_uom_dependant, - "plc_conversion_rate": plc_conversion_rate or 1 - }) @frappe.whitelist() def get_default_bom(item_code=None): if item_code: - bom = frappe.db.get_value("BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code}) + bom = frappe.db.get_value( + "BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} + ) if bom: return bom + @frappe.whitelist() def get_valuation_rate(item_code, company, warehouse=None): item = get_item_defaults(item_code, company) @@ -1150,43 +1349,57 @@ def get_valuation_rate(item_code, company, warehouse=None): # item = frappe.get_doc("Item", item_code) if item.get("is_stock_item"): if not warehouse: - warehouse = item.get("default_warehouse") or item_group.get("default_warehouse") or brand.get("default_warehouse") + warehouse = ( + item.get("default_warehouse") + or item_group.get("default_warehouse") + or brand.get("default_warehouse") + ) - return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, - ["valuation_rate"], as_dict=True) or {"valuation_rate": 0} + return frappe.db.get_value( + "Bin", {"item_code": item_code, "warehouse": warehouse}, ["valuation_rate"], as_dict=True + ) or {"valuation_rate": 0} elif not item.get("is_stock_item"): - valuation_rate =frappe.db.sql("""select sum(base_net_amount) / sum(qty*conversion_factor) + valuation_rate = frappe.db.sql( + """select sum(base_net_amount) / sum(qty*conversion_factor) from `tabPurchase Invoice Item` - where item_code = %s and docstatus=1""", item_code) + where item_code = %s and docstatus=1""", + item_code, + ) if valuation_rate: return {"valuation_rate": valuation_rate[0][0] or 0.0} else: return {"valuation_rate": 0.0} + def get_gross_profit(out): if out.valuation_rate: - out.update({ - "gross_profit": ((out.base_rate - out.valuation_rate) * out.stock_qty) - }) + out.update({"gross_profit": ((out.base_rate - out.valuation_rate) * out.stock_qty)}) return out + @frappe.whitelist() def get_serial_no(args, serial_nos=None, sales_order=None): serial_no = None if isinstance(args, string_types): args = json.loads(args) args = frappe._dict(args) - if args.get('doctype') == 'Sales Invoice' and not args.get('update_stock'): + if args.get("doctype") == "Sales Invoice" and not args.get("update_stock"): return "" - if args.get('warehouse') and args.get('stock_qty') and args.get('item_code'): - has_serial_no = frappe.get_value('Item', {'item_code': args.item_code}, "has_serial_no") - if args.get('batch_no') and has_serial_no == 1: + if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"): + has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no") + if args.get("batch_no") and has_serial_no == 1: return get_serial_no_batchwise(args, sales_order) elif has_serial_no == 1: - args = json.dumps({"item_code": args.get('item_code'),"warehouse": args.get('warehouse'),"stock_qty": args.get('stock_qty')}) + args = json.dumps( + { + "item_code": args.get("item_code"), + "warehouse": args.get("warehouse"), + "stock_qty": args.get("stock_qty"), + } + ) args = process_args(args) serial_no = get_serial_nos_by_fifo(args, sales_order) @@ -1203,53 +1416,68 @@ def update_party_blanket_order(args, out): if blanket_order_details: out.update(blanket_order_details) + @frappe.whitelist() def get_blanket_order_details(args): if isinstance(args, string_types): args = frappe._dict(json.loads(args)) blanket_order_details = None - condition = '' + condition = "" if args.item_code: if args.customer and args.doctype == "Sales Order": - condition = ' and bo.customer=%(customer)s' + condition = " and bo.customer=%(customer)s" elif args.supplier and args.doctype == "Purchase Order": - condition = ' and bo.supplier=%(supplier)s' + condition = " and bo.supplier=%(supplier)s" if args.blanket_order: - condition += ' and bo.name =%(blanket_order)s' + condition += " and bo.name =%(blanket_order)s" if args.transaction_date: - condition += ' and bo.to_date>=%(transaction_date)s' + condition += " and bo.to_date>=%(transaction_date)s" - blanket_order_details = frappe.db.sql(''' + blanket_order_details = frappe.db.sql( + """ select boi.rate as blanket_order_rate, bo.name as blanket_order from `tabBlanket Order` bo, `tabBlanket Order Item` boi where bo.company=%(company)s and boi.item_code=%(item_code)s and bo.docstatus=1 and bo.name = boi.parent {0} - '''.format(condition), args, as_dict=True) + """.format( + condition + ), + args, + as_dict=True, + ) - blanket_order_details = blanket_order_details[0] if blanket_order_details else '' + blanket_order_details = blanket_order_details[0] if blanket_order_details else "" return blanket_order_details + def get_so_reservation_for_item(args): reserved_so = None - if args.get('against_sales_order'): - if get_reserved_qty_for_so(args.get('against_sales_order'), args.get('item_code')): - reserved_so = args.get('against_sales_order') - elif args.get('against_sales_invoice'): - sales_order = frappe.db.sql("""select sales_order from `tabSales Invoice Item` where - parent=%s and item_code=%s""", (args.get('against_sales_invoice'), args.get('item_code'))) + if args.get("against_sales_order"): + if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")): + reserved_so = args.get("against_sales_order") + elif args.get("against_sales_invoice"): + sales_order = frappe.db.sql( + """select sales_order from `tabSales Invoice Item` where + parent=%s and item_code=%s""", + (args.get("against_sales_invoice"), args.get("item_code")), + ) if sales_order and sales_order[0]: - if get_reserved_qty_for_so(sales_order[0][0], args.get('item_code')): + if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")): reserved_so = sales_order[0] elif args.get("sales_order"): - if get_reserved_qty_for_so(args.get('sales_order'), args.get('item_code')): - reserved_so = args.get('sales_order') + if get_reserved_qty_for_so(args.get("sales_order"), args.get("item_code")): + reserved_so = args.get("sales_order") return reserved_so + def get_reserved_qty_for_so(sales_order, item_code): - reserved_qty = frappe.db.sql("""select sum(qty) from `tabSales Order Item` + reserved_qty = frappe.db.sql( + """select sum(qty) from `tabSales Order Item` where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1 - """, (sales_order, item_code)) + """, + (sales_order, item_code), + ) if reserved_qty and reserved_qty[0][0]: return reserved_qty[0][0] else: diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 21f2573a279..a96ffefd474 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -13,22 +13,29 @@ import erpnext def reorder_item(): - """ Reorder item if stock reaches reorder level""" + """Reorder item if stock reaches reorder level""" # if initial setup not completed, return if not (frappe.db.a_row_exists("Company") and frappe.db.a_row_exists("Fiscal Year")): return - if cint(frappe.db.get_value('Stock Settings', None, 'auto_indent')): + if cint(frappe.db.get_value("Stock Settings", None, "auto_indent")): return _reorder_item() + def _reorder_item(): material_requests = {"Purchase": {}, "Transfer": {}, "Material Issue": {}, "Manufacture": {}} - warehouse_company = frappe._dict(frappe.db.sql("""select name, company from `tabWarehouse` - where disabled=0""")) - default_company = (erpnext.get_default_company() or - frappe.db.sql("""select name from tabCompany limit 1""")[0][0]) + warehouse_company = frappe._dict( + frappe.db.sql( + """select name, company from `tabWarehouse` + where disabled=0""" + ) + ) + default_company = ( + erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] + ) - items_to_consider = frappe.db.sql_list("""select name from `tabItem` item + items_to_consider = frappe.db.sql_list( + """select name from `tabItem` item where is_stock_item=1 and has_variants=0 and disabled=0 and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) @@ -36,14 +43,17 @@ def _reorder_item(): or (variant_of is not null and variant_of != '' and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) )""", - {"today": nowdate()}) + {"today": nowdate()}, + ) if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request(item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None): + def add_to_material_request( + item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None + ): if warehouse not in warehouse_company: # a disabled warehouse return @@ -64,11 +74,9 @@ def _reorder_item(): company = warehouse_company.get(warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append({ - "item_code": item_code, - "warehouse": warehouse, - "reorder_qty": reorder_qty - }) + material_requests[material_request_type].setdefault(company, []).append( + {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + ) for item_code in items_to_consider: item = frappe.get_doc("Item", item_code) @@ -78,19 +86,30 @@ def _reorder_item(): if item.get("reorder_levels"): for d in item.get("reorder_levels"): - add_to_material_request(item_code, d.warehouse, d.warehouse_reorder_level, - d.warehouse_reorder_qty, d.material_request_type, warehouse_group=d.warehouse_group) + add_to_material_request( + item_code, + d.warehouse, + d.warehouse_reorder_level, + d.warehouse_reorder_qty, + d.material_request_type, + warehouse_group=d.warehouse_group, + ) if material_requests: return create_material_request(material_requests) + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} - for item_code, warehouse, projected_qty in frappe.db.sql("""select item_code, warehouse, projected_qty + for item_code, warehouse, projected_qty in frappe.db.sql( + """select item_code, warehouse, projected_qty from tabBin where item_code in ({0}) - and (warehouse != "" and warehouse is not null)"""\ - .format(", ".join(["%s"] * len(items_to_consider))), items_to_consider): + and (warehouse != "" and warehouse is not null)""".format( + ", ".join(["%s"] * len(items_to_consider)) + ), + items_to_consider, + ): if item_code not in item_warehouse_projected_qty: item_warehouse_projected_qty.setdefault(item_code, {}) @@ -102,15 +121,18 @@ def get_item_warehouse_projected_qty(items_to_consider): while warehouse_doc.parent_warehouse: if not item_warehouse_projected_qty.get(item_code, {}).get(warehouse_doc.parent_warehouse): - item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt(projected_qty) + item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt( + projected_qty + ) else: item_warehouse_projected_qty[item_code][warehouse_doc.parent_warehouse] += flt(projected_qty) warehouse_doc = frappe.get_doc("Warehouse", warehouse_doc.parent_warehouse) return item_warehouse_projected_qty + def create_material_request(material_requests): - """ Create indent on reaching reorder level """ + """Create indent on reaching reorder level""" mr_list = [] exceptions_list = [] @@ -131,11 +153,13 @@ def create_material_request(material_requests): continue mr = frappe.new_doc("Material Request") - mr.update({ - "company": company, - "transaction_date": nowdate(), - "material_request_type": "Material Transfer" if request_type=="Transfer" else request_type - }) + mr.update( + { + "company": company, + "transaction_date": nowdate(), + "material_request_type": "Material Transfer" if request_type == "Transfer" else request_type, + } + ) for d in items: d = frappe._dict(d) @@ -143,30 +167,37 @@ def create_material_request(material_requests): uom = item.stock_uom conversion_factor = 1.0 - if request_type == 'Purchase': + if request_type == "Purchase": uom = item.purchase_uom or item.stock_uom if uom != item.stock_uom: - conversion_factor = frappe.db.get_value("UOM Conversion Detail", - {'parent': item.name, 'uom': uom}, 'conversion_factor') or 1.0 + conversion_factor = ( + frappe.db.get_value( + "UOM Conversion Detail", {"parent": item.name, "uom": uom}, "conversion_factor" + ) + or 1.0 + ) must_be_whole_number = frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) qty = d.reorder_qty / conversion_factor if must_be_whole_number: qty = ceil(qty) - mr.append("items", { - "doctype": "Material Request Item", - "item_code": d.item_code, - "schedule_date": add_days(nowdate(),cint(item.lead_time_days)), - "qty": qty, - "uom": uom, - "stock_uom": item.stock_uom, - "warehouse": d.warehouse, - "item_name": item.item_name, - "description": item.description, - "item_group": item.item_group, - "brand": item.brand, - }) + mr.append( + "items", + { + "doctype": "Material Request Item", + "item_code": d.item_code, + "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), + "qty": qty, + "uom": uom, + "stock_uom": item.stock_uom, + "warehouse": d.warehouse, + "item_name": item.item_name, + "description": item.description, + "item_group": item.item_group, + "brand": item.brand, + }, + ) schedule_dates = [d.schedule_date for d in mr.items] mr.schedule_date = max(schedule_dates or [nowdate()]) @@ -180,10 +211,11 @@ def create_material_request(material_requests): if mr_list: if getattr(frappe.local, "reorder_email_notify", None) is None: - frappe.local.reorder_email_notify = cint(frappe.db.get_value('Stock Settings', None, - 'reorder_email_notify')) + frappe.local.reorder_email_notify = cint( + frappe.db.get_value("Stock Settings", None, "reorder_email_notify") + ) - if(frappe.local.reorder_email_notify): + if frappe.local.reorder_email_notify: send_email_notification(mr_list) if exceptions_list: @@ -191,33 +223,44 @@ def create_material_request(material_requests): return mr_list -def send_email_notification(mr_list): - """ Notify user about auto creation of indent""" - email_list = frappe.db.sql_list("""select distinct r.parent +def send_email_notification(mr_list): + """Notify user about auto creation of indent""" + + email_list = frappe.db.sql_list( + """select distinct r.parent from `tabHas Role` r, tabUser p where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 and r.role in ('Purchase Manager','Stock Manager') - and p.name not in ('Administrator', 'All', 'Guest')""") + and p.name not in ('Administrator', 'All', 'Guest')""" + ) - msg = frappe.render_template("templates/emails/reorder_item.html", { - "mr_list": mr_list - }) + msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + + frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) - frappe.sendmail(recipients=email_list, - subject=_('Auto Material Requests Generated'), message = msg) def notify_errors(exceptions_list): subject = _("[Important] [ERPNext] Auto Reorder Errors") - content = _("Dear System Manager,") + "
    " + _("An error occured for certain Items while creating Material Requests based on Re-order level. \ - Please rectify these issues :") + "
    " + content = ( + _("Dear System Manager,") + + "
    " + + _( + "An error occured for certain Items while creating Material Requests based on Re-order level. \ + Please rectify these issues :" + ) + + "
    " + ) for exception in exceptions_list: exception = json.loads(exception) - error_message = """
    {0}

    """.format(_(exception.get("message"))) + error_message = """
    {0}

    """.format( + _(exception.get("message")) + ) content += error_message content += _("Regards,") + "
    " + _("Administrator") from frappe.email import sendmail_to_system_managers + sendmail_to_system_managers(subject, content) diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 87097c72fa4..3d9b0461977 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -8,7 +8,8 @@ from frappe.utils import cint, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} float_precision = cint(frappe.db.get_default("float_precision")) or 3 @@ -22,22 +23,37 @@ def execute(filters=None): for batch in sorted(iwb_map[item][wh]): qty_dict = iwb_map[item][wh][batch] - data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, - frappe.db.get_value('Batch', batch, 'expiry_date'), qty_dict.expiry_status - ]) - + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["description"], + wh, + batch, + frappe.db.get_value("Batch", batch, "expiry_date"), + qty_dict.expiry_status, + ] + ) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ - [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Expires On") + ":Date:90"] + \ - [_("Expiry (In Days)") + ":Int:120"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::150"] + + [_("Description") + "::150"] + + [_("Warehouse") + ":Link/Warehouse:100"] + + [_("Batch") + ":Link/Batch:100"] + + [_("Expires On") + ":Date:90"] + + [_("Expiry (In Days)") + ":Int:120"] + ) return columns + def get_conditions(filters): conditions = "" if not filters.get("from_date"): @@ -50,14 +66,19 @@ def get_conditions(filters): return conditions + def get_stock_ledger_entries(filters): conditions = get_conditions(filters) - return frappe.db.sql("""select item_code, batch_no, warehouse, + return frappe.db.sql( + """select item_code, batch_no, warehouse, posting_date, actual_qty from `tabStock Ledger Entry` where is_cancelled = 0 - and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % - conditions, as_dict=1) + and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" + % conditions, + as_dict=1, + ) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) @@ -67,13 +88,13 @@ def get_item_warehouse_batch_map(filters, float_precision): to_date = getdate(filters["to_date"]) for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {})\ - .setdefault(d.batch_no, frappe._dict({ - "expires_on": None, "expiry_status": None})) + iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( + d.batch_no, frappe._dict({"expires_on": None, "expiry_status": None}) + ) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] - expiry_date_unicode = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') + expiry_date_unicode = frappe.db.get_value("Batch", d.batch_no, "expiry_date") qty_dict.expires_on = expiry_date_unicode exp_date = frappe.utils.data.getdate(expiry_date_unicode) @@ -88,6 +109,7 @@ def get_item_warehouse_batch_map(filters, float_precision): return iwb_map + def get_item_details(filters): item_map = {} for d in frappe.db.sql("select name, item_name, description from tabItem", as_dict=1): diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 9b21deabcd4..8a13300dc83 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -8,7 +8,8 @@ from frappe.utils import cint, flt, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) @@ -26,11 +27,20 @@ def execute(filters=None): for batch in sorted(iwb_map[item][wh]): qty_dict = iwb_map[item][wh][batch] if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty: - data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, - flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision), - flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision), - item_map[item]["stock_uom"] - ]) + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["description"], + wh, + batch, + flt(qty_dict.opening_qty, float_precision), + flt(qty_dict.in_qty, float_precision), + flt(qty_dict.out_qty, float_precision), + flt(qty_dict.bal_qty, float_precision), + item_map[item]["stock_uom"], + ] + ) return columns, data @@ -38,10 +48,18 @@ def execute(filters=None): def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ - [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \ - [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \ - [_("UOM") + "::90"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::150"] + + [_("Description") + "::150"] + + [_("Warehouse") + ":Link/Warehouse:100"] + + [_("Batch") + ":Link/Batch:100"] + + [_("Opening Qty") + ":Float:90"] + + [_("In Qty") + ":Float:80"] + + [_("Out Qty") + ":Float:80"] + + [_("Balance Qty") + ":Float:90"] + + [_("UOM") + "::90"] + ) return columns @@ -66,13 +84,16 @@ def get_conditions(filters): # get all details def get_stock_ledger_entries(filters): conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty from `tabStock Ledger Entry` where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s group by voucher_no, batch_no, item_code, warehouse - order by item_code, warehouse""" % - conditions, as_dict=1) + order by item_code, warehouse""" + % conditions, + as_dict=1, + ) def get_item_warehouse_batch_map(filters, float_precision): @@ -83,20 +104,21 @@ def get_item_warehouse_batch_map(filters, float_precision): to_date = getdate(filters["to_date"]) for d in sle: - iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {})\ - .setdefault(d.batch_no, frappe._dict({ - "opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0 - })) + iwb_map.setdefault(d.item_code, {}).setdefault(d.warehouse, {}).setdefault( + d.batch_no, frappe._dict({"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "bal_qty": 0.0}) + ) qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no] if d.posting_date < from_date: - qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) \ - + flt(d.actual_qty, float_precision) + qty_dict.opening_qty = flt(qty_dict.opening_qty, float_precision) + flt( + d.actual_qty, float_precision + ) elif d.posting_date >= from_date and d.posting_date <= to_date: if flt(d.actual_qty) > 0: qty_dict.in_qty = flt(qty_dict.in_qty, float_precision) + flt(d.actual_qty, float_precision) else: - qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) \ - + abs(flt(d.actual_qty, float_precision)) + qty_dict.out_qty = flt(qty_dict.out_qty, float_precision) + abs( + flt(d.actual_qty, float_precision) + ) qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision) diff --git a/erpnext/stock/report/bom_search/bom_search.py b/erpnext/stock/report/bom_search/bom_search.py index 960396d5345..ec5da6236d6 100644 --- a/erpnext/stock/report/bom_search/bom_search.py +++ b/erpnext/stock/report/bom_search/bom_search.py @@ -11,11 +11,13 @@ def execute(filters=None): parents = { "Product Bundle Item": "Product Bundle", "BOM Explosion Item": "BOM", - "BOM Item": "BOM" + "BOM Item": "BOM", } - for doctype in ("Product Bundle Item", - "BOM Explosion Item" if filters.search_sub_assemblies else "BOM Item"): + for doctype in ( + "Product Bundle Item", + "BOM Explosion Item" if filters.search_sub_assemblies else "BOM Item", + ): all_boms = {} for d in frappe.get_all(doctype, fields=["parent", "item_code"]): all_boms.setdefault(d.parent, []).append(d.item_code) @@ -30,16 +32,13 @@ def execute(filters=None): if valid: data.append((parent, parents[doctype])) - return [{ - "fieldname": "parent", - "label": "BOM", - "width": 200, - "fieldtype": "Dynamic Link", - "options": "doctype" - }, - { - "fieldname": "doctype", - "label": "Type", - "width": 200, - "fieldtype": "Data" - }], data + return [ + { + "fieldname": "parent", + "label": "BOM", + "width": 200, + "fieldtype": "Dynamic Link", + "options": "doctype", + }, + {"fieldname": "doctype", "label": "Type", "width": 200, "fieldtype": "Data"}, + ], data diff --git a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py index 058af77aa21..4642a535b63 100644 --- a/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py +++ b/erpnext/stock/report/cogs_by_item_group/cogs_by_item_group.py @@ -41,18 +41,8 @@ def validate_filters(filters: Filters) -> None: def get_columns() -> Columns: return [ - { - 'label': _('Item Group'), - 'fieldname': 'item_group', - 'fieldtype': 'Data', - 'width': '200' - }, - { - 'label': _('COGS Debit'), - 'fieldname': 'cogs_debit', - 'fieldtype': 'Currency', - 'width': '200' - } + {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Data", "width": "200"}, + {"label": _("COGS Debit"), "fieldname": "cogs_debit", "fieldtype": "Currency", "width": "200"}, ] @@ -67,11 +57,11 @@ def get_data(filters: Filters) -> Data: data = [] for item in leveled_dict.items(): i = item[1] - if i['agg_value'] == 0: + if i["agg_value"] == 0: continue - data.append(get_row(i['name'], i['agg_value'], i['is_group'], i['level'])) - if i['self_value'] < i['agg_value'] and i['self_value'] > 0: - data.append(get_row(i['name'], i['self_value'], 0, i['level'] + 1)) + data.append(get_row(i["name"], i["agg_value"], i["is_group"], i["level"])) + if i["self_value"] < i["agg_value"] and i["self_value"] > 0: + data.append(get_row(i["name"], i["self_value"], 0, i["level"] + 1)) return data @@ -79,8 +69,8 @@ def get_filtered_entries(filters: Filters) -> FilteredEntries: gl_entries = get_gl_entries(filters, []) filtered_entries = [] for entry in gl_entries: - posting_date = entry.get('posting_date') - from_date = filters.get('from_date') + posting_date = entry.get("posting_date") + from_date = filters.get("from_date") if date_diff(from_date, posting_date) > 0: continue filtered_entries.append(entry) @@ -88,10 +78,11 @@ def get_filtered_entries(filters: Filters) -> FilteredEntries: def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDList: - voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] + voucher_nos = [fe.get("voucher_no") for fe in filtered_entries] svd_list = frappe.get_list( - 'Stock Ledger Entry', fields=['item_code','stock_value_difference'], - filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)] + "Stock Ledger Entry", + fields=["item_code", "stock_value_difference"], + filters=[("voucher_no", "in", voucher_nos), ("is_cancelled", "=", 0)], ) assign_item_groups_to_svd_list(svd_list) return svd_list @@ -99,7 +90,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis def get_leveled_dict() -> OrderedDict: item_groups_dict = get_item_groups_dict() - lr_list = sorted(item_groups_dict, key=lambda x : int(x[0])) + lr_list = sorted(item_groups_dict, key=lambda x: int(x[0])) leveled_dict = OrderedDict() current_level = 0 nesting_r = [] @@ -108,10 +99,10 @@ def get_leveled_dict() -> OrderedDict: nesting_r.pop() current_level -= 1 - leveled_dict[(l,r)] = { - 'level' : current_level, - 'name' : item_groups_dict[(l,r)]['name'], - 'is_group' : item_groups_dict[(l,r)]['is_group'] + leveled_dict[(l, r)] = { + "level": current_level, + "name": item_groups_dict[(l, r)]["name"], + "is_group": item_groups_dict[(l, r)]["is_group"], } if int(r) - int(l) > 1: @@ -123,38 +114,38 @@ def get_leveled_dict() -> OrderedDict: def assign_self_values(leveled_dict: OrderedDict, svd_list: SVDList) -> None: - key_dict = {v['name']:k for k, v in leveled_dict.items()} + key_dict = {v["name"]: k for k, v in leveled_dict.items()} for item in svd_list: key = key_dict[item.get("item_group")] - leveled_dict[key]['self_value'] += -item.get("stock_value_difference") + leveled_dict[key]["self_value"] += -item.get("stock_value_difference") def assign_agg_values(leveled_dict: OrderedDict) -> None: keys = list(leveled_dict.keys())[::-1] - prev_level = leveled_dict[keys[-1]]['level'] + prev_level = leveled_dict[keys[-1]]["level"] accu = [0] for k in keys[:-1]: - curr_level = leveled_dict[k]['level'] + curr_level = leveled_dict[k]["level"] if curr_level == prev_level: - accu[-1] += leveled_dict[k]['self_value'] - leveled_dict[k]['agg_value'] = leveled_dict[k]['self_value'] + accu[-1] += leveled_dict[k]["self_value"] + leveled_dict[k]["agg_value"] = leveled_dict[k]["self_value"] elif curr_level > prev_level: - accu.append(leveled_dict[k]['self_value']) - leveled_dict[k]['agg_value'] = accu[-1] + accu.append(leveled_dict[k]["self_value"]) + leveled_dict[k]["agg_value"] = accu[-1] elif curr_level < prev_level: - accu[-1] += leveled_dict[k]['self_value'] - leveled_dict[k]['agg_value'] = accu[-1] + accu[-1] += leveled_dict[k]["self_value"] + leveled_dict[k]["agg_value"] = accu[-1] prev_level = curr_level # root node rk = keys[-1] - leveled_dict[rk]['agg_value'] = sum(accu) + leveled_dict[rk]['self_value'] + leveled_dict[rk]["agg_value"] = sum(accu) + leveled_dict[rk]["self_value"] -def get_row(name:str, value:float, is_bold:int, indent:int) -> Row: +def get_row(name: str, value: float, is_bold: int, indent: int) -> Row: item_group = name if is_bold: item_group = frappe.bold(item_group) @@ -168,20 +159,20 @@ def assign_item_groups_to_svd_list(svd_list: SVDList) -> None: def get_item_groups_map(svd_list: SVDList) -> Dict[str, str]: - item_codes = set(i['item_code'] for i in svd_list) + item_codes = set(i["item_code"] for i in svd_list) ig_list = frappe.get_list( - 'Item', fields=['item_code','item_group'], - filters=[('item_code', 'in', item_codes)] + "Item", fields=["item_code", "item_group"], filters=[("item_code", "in", item_codes)] ) - return {i['item_code']:i['item_group'] for i in ig_list} + return {i["item_code"]: i["item_group"] for i in ig_list} def get_item_groups_dict() -> ItemGroupsDict: item_groups_list = frappe.get_all("Item Group", fields=("name", "is_group", "lft", "rgt")) - return {(i['lft'],i['rgt']):{'name':i['name'], 'is_group':i['is_group']} - for i in item_groups_list} + return { + (i["lft"], i["rgt"]): {"name": i["name"], "is_group": i["is_group"]} for i in item_groups_list + } def update_leveled_dict(leveled_dict: OrderedDict) -> None: for k in leveled_dict: - leveled_dict[k].update({'self_value':0, 'agg_value':0}) + leveled_dict[k].update({"self_value": 0, "agg_value": 0}) diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 4ec36ea417f..9df24d65596 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -7,11 +7,12 @@ from frappe import _ from frappe.utils import date_diff -def execute(filters=None, consolidated = False): +def execute(filters=None, consolidated=False): data, columns = DelayedItemReport(filters).run() return data, columns + class DelayedItemReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -23,28 +24,38 @@ class DelayedItemReport(object): conditions = "" doctype = self.filters.get("based_on") - child_doc= "%s Item" % doctype + child_doc = "%s Item" % doctype if doctype == "Sales Invoice": conditions = " and `tabSales Invoice`.update_stock = 1 and `tabSales Invoice`.is_pos = 0" if self.filters.get("item_group"): - conditions += " and `tab%s`.item_group = %s" % (child_doc, - frappe.db.escape(self.filters.get("item_group"))) + conditions += " and `tab%s`.item_group = %s" % ( + child_doc, + frappe.db.escape(self.filters.get("item_group")), + ) for field in ["customer", "customer_group", "company"]: if self.filters.get(field): - conditions += " and `tab%s`.%s = %s" % (doctype, - field, frappe.db.escape(self.filters.get(field))) + conditions += " and `tab%s`.%s = %s" % ( + doctype, + field, + frappe.db.escape(self.filters.get(field)), + ) sales_order_field = "against_sales_order" if doctype == "Sales Invoice": sales_order_field = "sales_order" if self.filters.get("sales_order"): - conditions = " and `tab%s`.%s = '%s'" %(child_doc, sales_order_field, self.filters.get("sales_order")) + conditions = " and `tab%s`.%s = '%s'" % ( + child_doc, + sales_order_field, + self.filters.get("sales_order"), + ) - self.transactions = frappe.db.sql(""" SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, + self.transactions = frappe.db.sql( + """ SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, `tab{child_doc}`.item_group, `tab{child_doc}`.qty, `tab{child_doc}`.rate, `tab{child_doc}`.amount, `tab{child_doc}`.so_detail, `tab{child_doc}`.{so_field} as sales_order, `tab{doctype}`.shipping_address_name, `tab{doctype}`.po_no, `tab{doctype}`.customer, @@ -54,10 +65,12 @@ class DelayedItemReport(object): `tab{child_doc}`.parent = `tab{doctype}`.name and `tab{doctype}`.docstatus = 1 and `tab{doctype}`.posting_date between %(from_date)s and %(to_date)s and `tab{child_doc}`.{so_field} is not null and `tab{child_doc}`.{so_field} != '' {cond} - """.format(cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field), { - 'from_date': self.filters.get('from_date'), - 'to_date': self.filters.get('to_date') - }, as_dict=1) + """.format( + cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field + ), + {"from_date": self.filters.get("from_date"), "to_date": self.filters.get("to_date")}, + as_dict=1, + ) if self.transactions: self.filter_transactions_data(consolidated) @@ -67,112 +80,85 @@ class DelayedItemReport(object): def filter_transactions_data(self, consolidated=False): sales_orders = [d.sales_order for d in self.transactions] doctype = "Sales Order" - filters = {'name': ('in', sales_orders)} + filters = {"name": ("in", sales_orders)} if not consolidated: sales_order_items = [d.so_detail for d in self.transactions] doctype = "Sales Order Item" - filters = {'parent': ('in', sales_orders), 'name': ('in', sales_order_items)} + filters = {"parent": ("in", sales_orders), "name": ("in", sales_order_items)} so_data = {} - for d in frappe.get_all(doctype, filters = filters, - fields = ["delivery_date", "parent", "name"]): + for d in frappe.get_all(doctype, filters=filters, fields=["delivery_date", "parent", "name"]): key = d.name if consolidated else (d.parent, d.name) if key not in so_data: so_data.setdefault(key, d.delivery_date) for row in self.transactions: key = row.sales_order if consolidated else (row.sales_order, row.so_detail) - row.update({ - 'delivery_date': so_data.get(key), - 'delayed_days': date_diff(row.posting_date, so_data.get(key)) - }) + row.update( + { + "delivery_date": so_data.get(key), + "delayed_days": date_diff(row.posting_date, so_data.get(key)), + } + ) return self.transactions def get_columns(self): based_on = self.filters.get("based_on") - return [{ - "label": _(based_on), - "fieldname": "name", - "fieldtype": "Link", - "options": based_on, - "width": 100 - }, - { - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer", - "width": 200 - }, - { - "label": _("Shipping Address"), - "fieldname": "shipping_address_name", - "fieldtype": "Link", - "options": "Address", - "width": 120 - }, - { - "label": _("Expected Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Actual Delivery Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Quantity"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Rate"), - "fieldname": "rate", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "amount", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Delayed Days"), - "fieldname": "delayed_days", - "fieldtype": "Int", - "width": 100 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 100 - }, - { - "label": _("Customer PO"), - "fieldname": "po_no", - "fieldtype": "Data", - "width": 100 - }] + return [ + { + "label": _(based_on), + "fieldname": "name", + "fieldtype": "Link", + "options": based_on, + "width": 100, + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 200, + }, + { + "label": _("Shipping Address"), + "fieldname": "shipping_address_name", + "fieldtype": "Link", + "options": "Address", + "width": 120, + }, + { + "label": _("Expected Delivery Date"), + "fieldname": "delivery_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual Delivery Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100}, + {"label": _("Delayed Days"), "fieldname": "delayed_days", "fieldtype": "Int", "width": 100}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 100, + }, + {"label": _("Customer PO"), "fieldname": "po_no", "fieldtype": "Data", "width": 100}, + ] diff --git a/erpnext/stock/report/delayed_order_report/delayed_order_report.py b/erpnext/stock/report/delayed_order_report/delayed_order_report.py index 26090ab8ffb..197218d7ff4 100644 --- a/erpnext/stock/report/delayed_order_report/delayed_order_report.py +++ b/erpnext/stock/report/delayed_order_report/delayed_order_report.py @@ -14,6 +14,7 @@ def execute(filters=None): return columns, data + class DelayedOrderReport(DelayedItemReport): def run(self): return self.get_columns(), self.get_data(consolidated=True) or [] @@ -33,60 +34,48 @@ class DelayedOrderReport(DelayedItemReport): def get_columns(self): based_on = self.filters.get("based_on") - return [{ - "label": _(based_on), - "fieldname": "name", - "fieldtype": "Link", - "options": based_on, - "width": 100 - },{ - "label": _("Customer"), - "fieldname": "customer", - "fieldtype": "Link", - "options": "Customer", - "width": 200 - }, - { - "label": _("Shipping Address"), - "fieldname": "shipping_address_name", - "fieldtype": "Link", - "options": "Address", - "width": 140 - }, - { - "label": _("Expected Delivery Date"), - "fieldname": "delivery_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Actual Delivery Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Amount"), - "fieldname": "grand_total", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": _("Delayed Days"), - "fieldname": "delayed_days", - "fieldtype": "Int", - "width": 100 - }, - { - "label": _("Sales Order"), - "fieldname": "sales_order", - "fieldtype": "Link", - "options": "Sales Order", - "width": 150 - }, - { - "label": _("Customer PO"), - "fieldname": "po_no", - "fieldtype": "Data", - "width": 110 - }] + return [ + { + "label": _(based_on), + "fieldname": "name", + "fieldtype": "Link", + "options": based_on, + "width": 100, + }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 200, + }, + { + "label": _("Shipping Address"), + "fieldname": "shipping_address_name", + "fieldtype": "Link", + "options": "Address", + "width": 140, + }, + { + "label": _("Expected Delivery Date"), + "fieldname": "delivery_date", + "fieldtype": "Date", + "width": 100, + }, + { + "label": _("Actual Delivery Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + {"label": _("Amount"), "fieldname": "grand_total", "fieldtype": "Currency", "width": 100}, + {"label": _("Delayed Days"), "fieldname": "delayed_days", "fieldtype": "Int", "width": 100}, + { + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 150, + }, + {"label": _("Customer PO"), "fieldname": "po_no", "fieldtype": "Data", "width": 110}, + ] diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index b7ac7ff6a53..7a1b8c0cee9 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Delivery Note") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, filters): if not data: return [] @@ -27,7 +29,7 @@ def get_chart_data(data, filters): # consider only consolidated row data = [row for row in data if row[0]] - data = sorted(data, key = lambda i: i[-1],reverse=True) + data = sorted(data, key=lambda i: i[-1], reverse=True) if len(data) > 10: # get top 10 if data too long @@ -39,13 +41,8 @@ def get_chart_data(data, filters): return { "data": { - "labels" : labels, - "datasets" : [ - { - "name": _("Total Delivered Amount"), - "values": datapoints - } - ] + "labels": labels, + "datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}], }, - "type" : "bar" + "type": "bar", } diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py index 7d7e9644854..af91b248b8c 100644 --- a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py +++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py @@ -13,6 +13,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_data(filters): data = get_stock_ledger_entries(filters) itewise_balance_qty = {} @@ -24,6 +25,7 @@ def get_data(filters): res = validate_data(itewise_balance_qty) return res + def validate_data(itewise_balance_qty): res = [] for key, data in iteritems(itewise_balance_qty): @@ -34,6 +36,7 @@ def validate_data(itewise_balance_qty): return res + def get_incorrect_data(data): balance_qty = 0.0 for row in data: @@ -46,67 +49,84 @@ def get_incorrect_data(data): row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) return row + def get_stock_ledger_entries(report_filters): filters = {"is_cancelled": 0} - fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty', - 'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no'] + fields = [ + "name", + "voucher_type", + "voucher_no", + "item_code", + "actual_qty", + "posting_date", + "posting_time", + "company", + "warehouse", + "qty_after_transaction", + "batch_no", + ] - for field in ['warehouse', 'item_code', 'company']: + for field in ["warehouse", "item_code", "company"]: if report_filters.get(field): filters[field] = report_filters.get(field) - return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, - order_by = 'timestamp(posting_date, posting_time) asc, creation asc') + return frappe.get_all( + "Stock Ledger Entry", + fields=fields, + filters=filters, + order_by="timestamp(posting_date, posting_time) asc, creation asc", + ) + def get_columns(): - return [{ - 'label': _('Id'), - 'fieldtype': 'Link', - 'fieldname': 'name', - 'options': 'Stock Ledger Entry', - 'width': 120 - }, { - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date', - 'width': 110 - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 120 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 120 - }, { - 'label': _('Item Code'), - 'fieldtype': 'Link', - 'fieldname': 'item_code', - 'options': 'Item', - 'width': 120 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 120 - }, { - 'label': _('Expected Balance Qty'), - 'fieldtype': 'Float', - 'fieldname': 'expected_balance_qty', - 'width': 170 - }, { - 'label': _('Actual Balance Qty'), - 'fieldtype': 'Float', - 'fieldname': 'qty_after_transaction', - 'width': 150 - }, { - 'label': _('Difference'), - 'fieldtype': 'Float', - 'fieldname': 'differnce', - 'width': 110 - }] + return [ + { + "label": _("Id"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Stock Ledger Entry", + "width": 120, + }, + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 110}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 120, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 120, + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("Expected Balance Qty"), + "fieldtype": "Float", + "fieldname": "expected_balance_qty", + "width": 170, + }, + { + "label": _("Actual Balance Qty"), + "fieldtype": "Float", + "fieldname": "qty_after_transaction", + "width": 150, + }, + {"label": _("Difference"), "fieldtype": "Float", "fieldname": "differnce", "width": 110}, + ] diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py index e4ea9947166..26892407d99 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py @@ -16,6 +16,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_data(filters): data = get_stock_ledger_entries(filters) serial_nos_data = prepare_serial_nos(data) @@ -23,6 +24,7 @@ def get_data(filters): return data + def prepare_serial_nos(data): serial_no_wise_data = {} for row in data: @@ -38,13 +40,16 @@ def prepare_serial_nos(data): return serial_no_wise_data + def get_incorrect_serial_nos(serial_nos_data): result = [] - total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))}) + total_value = frappe._dict( + {"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Balance"))} + ) for serial_no, data in iteritems(serial_nos_data): - total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))}) + total_dict = frappe._dict({"qty": 0, "valuation_rate": 0, "serial_no": frappe.bold(_("Total"))}) if check_incorrect_serial_data(data, total_dict): result.extend(data) @@ -59,93 +64,111 @@ def get_incorrect_serial_nos(serial_nos_data): return result + def check_incorrect_serial_data(data, total_dict): incorrect_data = False for row in data: total_dict.qty += row.qty total_dict.valuation_rate += row.valuation_rate - if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0): + if (total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0: incorrect_data = True return incorrect_data + def get_stock_ledger_entries(report_filters): - fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty', - 'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate'] + fields = [ + "name", + "voucher_type", + "voucher_no", + "item_code", + "serial_no as serial_nos", + "actual_qty", + "posting_date", + "posting_time", + "company", + "warehouse", + "(stock_value_difference / actual_qty) as valuation_rate", + ] - filters = {'serial_no': ("is", "set"), "is_cancelled": 0} + filters = {"serial_no": ("is", "set"), "is_cancelled": 0} - if report_filters.get('item_code'): - filters['item_code'] = report_filters.get('item_code') + if report_filters.get("item_code"): + filters["item_code"] = report_filters.get("item_code") - if report_filters.get('from_date') and report_filters.get('to_date'): - filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')]) + if report_filters.get("from_date") and report_filters.get("to_date"): + filters["posting_date"] = ( + "between", + [report_filters.get("from_date"), report_filters.get("to_date")], + ) + + return frappe.get_all( + "Stock Ledger Entry", + fields=fields, + filters=filters, + order_by="timestamp(posting_date, posting_time) asc, creation asc", + ) - return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters, - order_by = 'timestamp(posting_date, posting_time) asc, creation asc') def get_columns(): - return [{ - 'label': _('Company'), - 'fieldtype': 'Link', - 'fieldname': 'company', - 'options': 'Company', - 'width': 120 - }, { - 'label': _('Id'), - 'fieldtype': 'Link', - 'fieldname': 'name', - 'options': 'Stock Ledger Entry', - 'width': 120 - }, { - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date', - 'width': 90 - }, { - 'label': _('Posting Time'), - 'fieldtype': 'Time', - 'fieldname': 'posting_time', - 'width': 90 - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 100 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 110 - }, { - 'label': _('Item Code'), - 'fieldtype': 'Link', - 'fieldname': 'item_code', - 'options': 'Item', - 'width': 120 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 120 - }, { - 'label': _('Serial No'), - 'fieldtype': 'Link', - 'fieldname': 'serial_no', - 'options': 'Serial No', - 'width': 100 - }, { - 'label': _('Qty'), - 'fieldtype': 'Float', - 'fieldname': 'qty', - 'width': 80 - }, { - 'label': _('Valuation Rate (In / Out)'), - 'fieldtype': 'Currency', - 'fieldname': 'valuation_rate', - 'width': 110 - }] + return [ + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 120, + }, + { + "label": _("Id"), + "fieldtype": "Link", + "fieldname": "name", + "options": "Stock Ledger Entry", + "width": 120, + }, + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 90}, + {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 100, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 110, + }, + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("Serial No"), + "fieldtype": "Link", + "fieldname": "serial_no", + "options": "Serial No", + "width": 100, + }, + {"label": _("Qty"), "fieldtype": "Float", "fieldname": "qty", "width": 80}, + { + "label": _("Valuation Rate (In / Out)"), + "fieldtype": "Currency", + "fieldname": "valuation_rate", + "width": 110, + }, + ] diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index 7cce4a7d22e..15f211127d7 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -14,14 +14,18 @@ from erpnext.stock.utils import get_stock_value_on def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): - frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") - .format(filters.company)) + frappe.throw( + _("Perpetual inventory required for the company {0} to view this report.").format( + filters.company + ) + ) data = get_data(filters) columns = get_columns(filters) return columns, data + def get_unsync_date(filters): date = filters.from_date if not date: @@ -32,14 +36,16 @@ def get_unsync_date(filters): return while getdate(date) < getdate(today()): - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date, - company=filters.company, account = filters.account) + account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( + posting_date=date, company=filters.company, account=filters.account + ) if abs(account_bal - stock_bal) > 0.1: return date date = add_days(date, 1) + def get_data(report_filters): from_date = get_unsync_date(report_filters) @@ -49,7 +55,8 @@ def get_data(report_filters): result = [] voucher_wise_dict = {} - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT name, posting_date, posting_time, voucher_type, voucher_no, stock_value_difference, stock_value, warehouse, item_code @@ -60,14 +67,19 @@ def get_data(report_filters): = %s and company = %s and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - ''', (from_date, report_filters.company), as_dict=1) + """, + (from_date, report_filters.company), + as_dict=1, + ) for d in data: voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) closing_date = add_days(from_date, -1) for key, stock_data in iteritems(voucher_wise_dict): - prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1]) + prev_stock_value = get_stock_value_on( + posting_date=closing_date, item_code=key[0], warehouse=key[1] + ) for data in stock_data: expected_stock_value = prev_stock_value + data.stock_value_difference if abs(data.stock_value - expected_stock_value) > 0.1: @@ -77,6 +89,7 @@ def get_data(report_filters): return result + def get_columns(filters): return [ { @@ -84,60 +97,43 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Stock Ledger Entry", - "width": "80" - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date" - }, - { - "label": _("Posting Time"), - "fieldname": "posting_time", - "fieldtype": "Time" - }, - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": "110" + "width": "80", }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"}, { "label": _("Voucher No"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": "110" + "width": "110", }, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": "110" + "width": "110", }, { "label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": "110" + "width": "110", }, { "label": _("Expected Stock Value"), "fieldname": "expected_stock_value", "fieldtype": "Currency", - "width": "150" - }, - { - "label": _("Stock Value"), - "fieldname": "stock_value", - "fieldtype": "Currency", - "width": "120" + "width": "150", }, + {"label": _("Stock Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": "120"}, { "label": _("Difference Value"), "fieldname": "difference_value", "fieldtype": "Currency", - "width": "150" - } + "width": "150", + }, ] diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 65af9f51cdf..15218e63a87 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -7,10 +7,11 @@ from frappe import _ def execute(filters=None): columns, data = [], [] - columns=get_columns() - data=get_data(filters,columns) + columns = get_columns() + data = get_data(filters, columns) return columns, data + def get_columns(): return [ { @@ -18,77 +19,64 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 120 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Data", - "width": 100 + "width": 120, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Data", "width": 100}, { "label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": 120 + "width": 120, }, { "label": _("Stock Available"), "fieldname": "stock_available", "fieldtype": "Float", - "width": 120 + "width": 120, }, { "label": _("Buying Price List"), "fieldname": "buying_price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 - }, - { - "label": _("Buying Rate"), - "fieldname": "buying_rate", - "fieldtype": "Currency", - "width": 120 + "width": 120, }, + {"label": _("Buying Rate"), "fieldname": "buying_rate", "fieldtype": "Currency", "width": 120}, { "label": _("Selling Price List"), "fieldname": "selling_price_list", "fieldtype": "Link", "options": "Price List", - "width": 120 + "width": 120, }, - { - "label": _("Selling Rate"), - "fieldname": "selling_rate", - "fieldtype": "Currency", - "width": 120 - } + {"label": _("Selling Rate"), "fieldname": "selling_rate", "fieldtype": "Currency", "width": 120}, ] + def get_data(filters, columns): item_price_qty_data = [] item_price_qty_data = get_item_price_qty_data(filters) return item_price_qty_data + def get_item_price_qty_data(filters): conditions = "" if filters.get("item_code"): conditions += "where a.item_code=%(item_code)s" - item_results = frappe.db.sql("""select a.item_code, a.item_name, a.name as price_list_name, + item_results = frappe.db.sql( + """select a.item_code, a.item_name, a.name as price_list_name, a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty from `tabItem Price` a left join `tabBin` b ON a.item_code = b.item_code - {conditions}""" - .format(conditions=conditions), filters, as_dict=1) + {conditions}""".format( + conditions=conditions + ), + filters, + as_dict=1, + ) price_list_names = list(set(item.price_list_name for item in item_results)) @@ -99,15 +87,15 @@ def get_item_price_qty_data(filters): if item_results: for item_dict in item_results: data = { - 'item_code': item_dict.item_code, - 'item_name': item_dict.item_name, - 'brand': item_dict.brand, - 'warehouse': item_dict.warehouse, - 'stock_available': item_dict.actual_qty or 0, - 'buying_price_list': "", - 'buying_rate': 0.0, - 'selling_price_list': "", - 'selling_rate': 0.0 + "item_code": item_dict.item_code, + "item_name": item_dict.item_name, + "brand": item_dict.brand, + "warehouse": item_dict.warehouse, + "stock_available": item_dict.actual_qty or 0, + "buying_price_list": "", + "buying_rate": 0.0, + "selling_price_list": "", + "selling_rate": 0.0, } price_list = item_dict["price_list_name"] @@ -122,6 +110,7 @@ def get_item_price_qty_data(filters): return result + def get_price_map(price_list_names, buying=0, selling=0): price_map = {} @@ -137,14 +126,12 @@ def get_price_map(price_list_names, buying=0, selling=0): else: filters["selling"] = 1 - pricing_details = frappe.get_all("Item Price", - fields = ["name", "price_list", "price_list_rate"], filters=filters) + pricing_details = frappe.get_all( + "Item Price", fields=["name", "price_list", "price_list_rate"], filters=filters + ) for d in pricing_details: name = d["name"] - price_map[name] = { - price_list_key :d["price_list"], - rate_key :d["price_list_rate"] - } + price_map[name] = {price_list_key: d["price_list"], rate_key: d["price_list_rate"]} return price_map diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index 0d0e8d22924..87f1a42e2b2 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -8,7 +8,8 @@ from frappe.utils import flt def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(filters) conditions = get_condition(filters) @@ -19,64 +20,95 @@ def execute(filters=None): val_rate_map = get_valuation_rate() from erpnext.accounts.utils import get_currency_precision + precision = get_currency_precision() or 2 data = [] for item in sorted(item_map): - data.append([item, item_map[item]["item_name"],item_map[item]["item_group"], - item_map[item]["brand"], item_map[item]["description"], item_map[item]["stock_uom"], - flt(last_purchase_rate.get(item, 0), precision), - flt(val_rate_map.get(item, 0), precision), - pl.get(item, {}).get("Selling"), - pl.get(item, {}).get("Buying"), - flt(bom_rate.get(item, 0), precision) - ]) + data.append( + [ + item, + item_map[item]["item_name"], + item_map[item]["item_group"], + item_map[item]["brand"], + item_map[item]["description"], + item_map[item]["stock_uom"], + flt(last_purchase_rate.get(item, 0), precision), + flt(val_rate_map.get(item, 0), precision), + pl.get(item, {}).get("Selling"), + pl.get(item, {}).get("Buying"), + flt(bom_rate.get(item, 0), precision), + ] + ) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100", _("Item Name") + "::150",_("Item Group") + ":Link/Item Group:125", - _("Brand") + "::100", _("Description") + "::150", _("UOM") + ":Link/UOM:80", - _("Last Purchase Rate") + ":Currency:90", _("Valuation Rate") + ":Currency:80", _("Sales Price List") + "::180", - _("Purchase Price List") + "::180", _("BOM Rate") + ":Currency:90"] + columns = [ + _("Item") + ":Link/Item:100", + _("Item Name") + "::150", + _("Item Group") + ":Link/Item Group:125", + _("Brand") + "::100", + _("Description") + "::150", + _("UOM") + ":Link/UOM:80", + _("Last Purchase Rate") + ":Currency:90", + _("Valuation Rate") + ":Currency:80", + _("Sales Price List") + "::180", + _("Purchase Price List") + "::180", + _("BOM Rate") + ":Currency:90", + ] return columns + def get_item_details(conditions): """returns all items details""" item_map = {} - for i in frappe.db.sql("""select name, item_group, item_name, description, + for i in frappe.db.sql( + """select name, item_group, item_name, description, brand, stock_uom from tabItem %s - order by item_code, item_group""" % (conditions), as_dict=1): - item_map.setdefault(i.name, i) + order by item_code, item_group""" + % (conditions), + as_dict=1, + ): + item_map.setdefault(i.name, i) return item_map + def get_price_list(): """Get selling & buying price list of every item""" rate = {} - price_list = frappe.db.sql("""select ip.item_code, ip.buying, ip.selling, + price_list = frappe.db.sql( + """select ip.item_code, ip.buying, ip.selling, concat(ifnull(cu.symbol,ip.currency), " ", round(ip.price_list_rate,2), " - ", ip.price_list) as price from `tabItem Price` ip, `tabPrice List` pl, `tabCurrency` cu - where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", as_dict=1) + where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", + as_dict=1, + ) for j in price_list: if j.price: - rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append(j.price) + rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append( + j.price + ) item_rate_map = {} for item in rate: for buying_or_selling in rate[item]: - item_rate_map.setdefault(item, {}).setdefault(buying_or_selling, - ", ".join(rate[item].get(buying_or_selling, []))) + item_rate_map.setdefault(item, {}).setdefault( + buying_or_selling, ", ".join(rate[item].get(buying_or_selling, [])) + ) return item_rate_map + def get_last_purchase_rate(): item_last_purchase_rate_map = {} @@ -108,29 +140,38 @@ def get_last_purchase_rate(): return item_last_purchase_rate_map + def get_item_bom_rate(): """Get BOM rate of an item from BOM""" item_bom_map = {} - for b in frappe.db.sql("""select item, (total_cost/quantity) as bom_rate - from `tabBOM` where is_active=1 and is_default=1""", as_dict=1): - item_bom_map.setdefault(b.item, flt(b.bom_rate)) + for b in frappe.db.sql( + """select item, (total_cost/quantity) as bom_rate + from `tabBOM` where is_active=1 and is_default=1""", + as_dict=1, + ): + item_bom_map.setdefault(b.item, flt(b.bom_rate)) return item_bom_map + def get_valuation_rate(): """Get an average valuation rate of an item from all warehouses""" item_val_rate_map = {} - for d in frappe.db.sql("""select item_code, + for d in frappe.db.sql( + """select item_code, sum(actual_qty*valuation_rate)/sum(actual_qty) as val_rate - from tabBin where actual_qty > 0 group by item_code""", as_dict=1): - item_val_rate_map.setdefault(d.item_code, d.val_rate) + from tabBin where actual_qty > 0 group by item_code""", + as_dict=1, + ): + item_val_rate_map.setdefault(d.item_code, d.val_rate) return item_val_rate_map + def get_condition(filters): """Get Filter Items""" diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 30c761421fd..03a3a6a0b83 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -18,6 +18,7 @@ def execute(filters=None): return columns, data, None, chart_data + def get_conditions(filters): conditions = "" @@ -28,8 +29,10 @@ def get_conditions(filters): return conditions + def get_data(conditions, filters): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT bin.warehouse, bin.item_code, @@ -51,10 +54,16 @@ def get_data(conditions, filters): AND warehouse.name = bin.warehouse AND bin.item_code=item.name {0} - ORDER BY bin.projected_qty;""".format(conditions), filters, as_dict=1) + ORDER BY bin.projected_qty;""".format( + conditions + ), + filters, + as_dict=1, + ) return data + def get_chart_data(data): labels, datapoints = [], [] @@ -67,18 +76,11 @@ def get_chart_data(data): datapoints = datapoints[:10] return { - "data": { - "labels": labels, - "datasets":[ - { - "name": _("Projected Qty"), - "values": datapoints - } - ] - }, - "type": "bar" + "data": {"labels": labels, "datasets": [{"name": _("Projected Qty"), "values": datapoints}]}, + "type": "bar", } + def get_columns(): columns = [ { @@ -86,76 +88,66 @@ def get_columns(): "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", - "width": 150 + "width": 150, }, { "label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 150 + "width": 150, }, { "label": _("Actual Quantity"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Ordered Quantity"), "fieldname": "ordered_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Planned Quantity"), "fieldname": "planned_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Reserved Quantity"), "fieldname": "reserved_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Reserved Quantity for Production"), "fieldname": "reserved_qty_for_production", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Projected Quantity"), "fieldname": "projected_qty", "fieldtype": "Float", "width": 120, - "convertible": "qty" + "convertible": "qty", }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 + "width": 120, }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 120 - } + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 120}, ] return columns diff --git a/erpnext/stock/report/item_variant_details/item_variant_details.py b/erpnext/stock/report/item_variant_details/item_variant_details.py index 10cef70215b..d1bf2203f17 100644 --- a/erpnext/stock/report/item_variant_details/item_variant_details.py +++ b/erpnext/stock/report/item_variant_details/item_variant_details.py @@ -11,25 +11,21 @@ def execute(filters=None): data = get_data(filters.item) return columns, data + def get_data(item): if not item: return [] item_dicts = [] variant_results = frappe.db.get_all( - "Item", - fields=["name"], - filters={ - "variant_of": ["=", item], - "disabled": 0 - } + "Item", fields=["name"], filters={"variant_of": ["=", item], "disabled": 0} ) if not variant_results: frappe.msgprint(_("There aren't any item variants for the selected item")) return [] else: - variant_list = [variant['name'] for variant in variant_results] + variant_list = [variant["name"] for variant in variant_results] order_count_map = get_open_sales_orders_count(variant_list) stock_details_map = get_stock_details_map(variant_list) @@ -40,15 +36,13 @@ def get_data(item): attributes = frappe.db.get_all( "Item Variant Attribute", fields=["attribute"], - filters={ - "parent": ["in", variant_list] - }, - group_by="attribute" + filters={"parent": ["in", variant_list]}, + group_by="attribute", ) attribute_list = [row.get("attribute") for row in attributes] # Prepare dicts - variant_dicts = [{"variant_name": d['name']} for d in variant_results] + variant_dicts = [{"variant_name": d["name"]} for d in variant_results] for item_dict in variant_dicts: name = item_dict.get("variant_name") @@ -72,73 +66,66 @@ def get_data(item): return item_dicts + def get_columns(item): - columns = [{ - "fieldname": "variant_name", - "label": "Variant", - "fieldtype": "Link", - "options": "Item", - "width": 200 - }] + columns = [ + { + "fieldname": "variant_name", + "label": "Variant", + "fieldtype": "Link", + "options": "Item", + "width": 200, + } + ] item_doc = frappe.get_doc("Item", item) for entry in item_doc.attributes: - columns.append({ - "fieldname": frappe.scrub(entry.attribute), - "label": entry.attribute, - "fieldtype": "Data", - "width": 100 - }) + columns.append( + { + "fieldname": frappe.scrub(entry.attribute), + "label": entry.attribute, + "fieldtype": "Data", + "width": 100, + } + ) additional_columns = [ { "fieldname": "avg_buying_price_list_rate", "label": _("Avg. Buying Price List Rate"), "fieldtype": "Currency", - "width": 150 + "width": 150, }, { "fieldname": "avg_selling_price_list_rate", "label": _("Avg. Selling Price List Rate"), "fieldtype": "Currency", - "width": 150 - }, - { - "fieldname": "current_stock", - "label": _("Current Stock"), - "fieldtype": "Float", - "width": 120 - }, - { - "fieldname": "in_production", - "label": _("In Production"), - "fieldtype": "Float", - "width": 150 + "width": 150, }, + {"fieldname": "current_stock", "label": _("Current Stock"), "fieldtype": "Float", "width": 120}, + {"fieldname": "in_production", "label": _("In Production"), "fieldtype": "Float", "width": 150}, { "fieldname": "open_orders", "label": _("Open Sales Orders"), "fieldtype": "Float", - "width": 150 - } + "width": 150, + }, ] columns.extend(additional_columns) return columns + def get_open_sales_orders_count(variants_list): open_sales_orders = frappe.db.get_list( "Sales Order", - fields=[ - "name", - "`tabSales Order Item`.item_code" - ], + fields=["name", "`tabSales Order Item`.item_code"], filters=[ ["Sales Order", "docstatus", "=", 1], - ["Sales Order Item", "item_code", "in", variants_list] + ["Sales Order Item", "item_code", "in", variants_list], ], - distinct=1 + distinct=1, ) order_count_map = {} @@ -151,6 +138,7 @@ def get_open_sales_orders_count(variants_list): return order_count_map + def get_stock_details_map(variant_list): stock_details = frappe.db.get_all( "Bin", @@ -160,10 +148,8 @@ def get_stock_details_map(variant_list): "sum(projected_qty) as projected_qty", "item_code", ], - filters={ - "item_code": ["in", variant_list] - }, - group_by="item_code" + filters={"item_code": ["in", variant_list]}, + group_by="item_code", ) stock_details_map = {} @@ -171,11 +157,12 @@ def get_stock_details_map(variant_list): name = row.get("item_code") stock_details_map[name] = { "Inventory": row.get("actual_qty"), - "In Production": row.get("planned_qty") + "In Production": row.get("planned_qty"), } return stock_details_map + def get_buying_price_map(variant_list): buying = frappe.db.get_all( "Item Price", @@ -183,11 +170,8 @@ def get_buying_price_map(variant_list): "avg(price_list_rate) as avg_rate", "item_code", ], - filters={ - "item_code": ["in", variant_list], - "buying": 1 - }, - group_by="item_code" + filters={"item_code": ["in", variant_list], "buying": 1}, + group_by="item_code", ) buying_price_map = {} @@ -196,6 +180,7 @@ def get_buying_price_map(variant_list): return buying_price_map + def get_selling_price_map(variant_list): selling = frappe.db.get_all( "Item Price", @@ -203,11 +188,8 @@ def get_selling_price_map(variant_list): "avg(price_list_rate) as avg_rate", "item_code", ], - filters={ - "item_code": ["in", variant_list], - "selling": 1 - }, - group_by="item_code" + filters={"item_code": ["in", variant_list], "selling": 1}, + group_by="item_code", ) selling_price_map = {} @@ -216,17 +198,12 @@ def get_selling_price_map(variant_list): return selling_price_map + def get_attribute_values_map(variant_list): attribute_list = frappe.db.get_all( "Item Variant Attribute", - fields=[ - "attribute", - "attribute_value", - "parent" - ], - filters={ - "parent": ["in", variant_list] - } + fields=["attribute", "attribute_value", "parent"], + filters={"parent": ["in", variant_list]}, ) attr_val_map = {} diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py index cfa1e474c7b..f308e9e41f1 100644 --- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py @@ -7,13 +7,14 @@ from frappe.utils import flt, getdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} float_precision = frappe.db.get_default("float_precision") condition = get_condition(filters) avg_daily_outgoing = 0 - diff = ((getdate(filters.get("to_date")) - getdate(filters.get("from_date"))).days)+1 + diff = ((getdate(filters.get("to_date")) - getdate(filters.get("from_date"))).days) + 1 if diff <= 0: frappe.throw(_("'From Date' must be after 'To Date'")) @@ -24,42 +25,72 @@ def execute(filters=None): data = [] for item in items: - total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name,0)) + total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt( + delivered_item_map.get(item.name, 0) + ) avg_daily_outgoing = flt(total_outgoing / diff, float_precision) reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock) - data.append([item.name, item.item_name, item.item_group, item.brand, item.description, - item.safety_stock, item.lead_time_days, consumed_item_map.get(item.name, 0), - delivered_item_map.get(item.name,0), total_outgoing, avg_daily_outgoing, reorder_level]) + data.append( + [ + item.name, + item.item_name, + item.item_group, + item.brand, + item.description, + item.safety_stock, + item.lead_time_days, + consumed_item_map.get(item.name, 0), + delivered_item_map.get(item.name, 0), + total_outgoing, + avg_daily_outgoing, + reorder_level, + ] + ) + + return columns, data - return columns , data def get_columns(): - return[ - _("Item") + ":Link/Item:120", _("Item Name") + ":Data:120", _("Item Group") + ":Link/Item Group:100", - _("Brand") + ":Link/Brand:100", _("Description") + "::160", - _("Safety Stock") + ":Float:160", _("Lead Time Days") + ":Float:120", _("Consumed") + ":Float:120", - _("Delivered") + ":Float:120", _("Total Outgoing") + ":Float:120", _("Avg Daily Outgoing") + ":Float:160", - _("Reorder Level") + ":Float:120" + return [ + _("Item") + ":Link/Item:120", + _("Item Name") + ":Data:120", + _("Item Group") + ":Link/Item Group:100", + _("Brand") + ":Link/Brand:100", + _("Description") + "::160", + _("Safety Stock") + ":Float:160", + _("Lead Time Days") + ":Float:120", + _("Consumed") + ":Float:120", + _("Delivered") + ":Float:120", + _("Total Outgoing") + ":Float:120", + _("Avg Daily Outgoing") + ":Float:160", + _("Reorder Level") + ":Float:120", ] + def get_item_info(filters): from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition + conditions = [get_item_group_condition(filters.get("item_group"))] if filters.get("brand"): conditions.append("item.brand=%(brand)s") conditions.append("is_stock_item = 1") - return frappe.db.sql("""select name, item_name, description, brand, item_group, - safety_stock, lead_time_days from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters, as_dict=1) + return frappe.db.sql( + """select name, item_name, description, brand, item_group, + safety_stock, lead_time_days from `tabItem` item where {}""".format( + " and ".join(conditions) + ), + filters, + as_dict=1, + ) def get_consumed_items(condition): purpose_to_exclude = [ "Material Transfer for Manufacture", "Material Transfer", - "Send to Subcontractor" + "Send to Subcontractor", ] condition += """ @@ -67,10 +98,13 @@ def get_consumed_items(condition): purpose is NULL or purpose not in ({}) ) - """.format(', '.join(f"'{p}'" for p in purpose_to_exclude)) + """.format( + ", ".join(f"'{p}'" for p in purpose_to_exclude) + ) condition = condition.replace("posting_date", "sle.posting_date") - consumed_items = frappe.db.sql(""" + consumed_items = frappe.db.sql( + """ select item_code, abs(sum(actual_qty)) as consumed_qty from `tabStock Ledger Entry` as sle left join `tabStock Entry` as se on sle.voucher_no = se.name @@ -79,22 +113,34 @@ def get_consumed_items(condition): and is_cancelled = 0 and voucher_type not in ('Delivery Note', 'Sales Invoice') %s - group by item_code""" % condition, as_dict=1) + group by item_code""" + % condition, + as_dict=1, + ) - consumed_items_map = {item.item_code : item.consumed_qty for item in consumed_items} + consumed_items_map = {item.item_code: item.consumed_qty for item in consumed_items} return consumed_items_map + def get_delivered_items(condition): - dn_items = frappe.db.sql("""select dn_item.item_code, sum(dn_item.stock_qty) as dn_qty + dn_items = frappe.db.sql( + """select dn_item.item_code, sum(dn_item.stock_qty) as dn_qty from `tabDelivery Note` dn, `tabDelivery Note Item` dn_item where dn.name = dn_item.parent and dn.docstatus = 1 %s - group by dn_item.item_code""" % (condition), as_dict=1) + group by dn_item.item_code""" + % (condition), + as_dict=1, + ) - si_items = frappe.db.sql("""select si_item.item_code, sum(si_item.stock_qty) as si_qty + si_items = frappe.db.sql( + """select si_item.item_code, sum(si_item.stock_qty) as si_qty from `tabSales Invoice` si, `tabSales Invoice Item` si_item where si.name = si_item.parent and si.docstatus = 1 and si.update_stock = 1 %s - group by si_item.item_code""" % (condition), as_dict=1) + group by si_item.item_code""" + % (condition), + as_dict=1, + ) dn_item_map = {} for item in dn_items: @@ -105,10 +151,14 @@ def get_delivered_items(condition): return dn_item_map + def get_condition(filters): conditions = "" if filters.get("from_date") and filters.get("to_date"): - conditions += " and posting_date between '%s' and '%s'" % (filters["from_date"],filters["to_date"]) + conditions += " and posting_date between '%s' and '%s'" % ( + filters["from_date"], + filters["to_date"], + ) else: frappe.throw(_("From and To dates required")) return conditions diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 3c4dbce73a6..88b29e60130 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -45,7 +45,9 @@ def execute(filters=None): child_rows = [] for child_item_detail in required_items: - child_item_balance = stock_balance.get(child_item_detail.item_code, frappe._dict()).get(warehouse, frappe._dict()) + child_item_balance = stock_balance.get(child_item_detail.item_code, frappe._dict()).get( + warehouse, frappe._dict() + ) child_row = { "indent": 1, "parent_item": parent_item, @@ -74,16 +76,46 @@ def execute(filters=None): def get_columns(): columns = [ - {"fieldname": "item_code", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 300}, - {"fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Link", "options": "Warehouse", "width": 100}, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", + "width": 300, + }, + { + "fieldname": "warehouse", + "label": _("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 70}, {"fieldname": "bundle_qty", "label": _("Bundle Qty"), "fieldtype": "Float", "width": 100}, {"fieldname": "actual_qty", "label": _("Actual Qty"), "fieldtype": "Float", "width": 100}, {"fieldname": "minimum_qty", "label": _("Minimum Qty"), "fieldtype": "Float", "width": 100}, - {"fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"fieldname": "brand", "label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 100}, + { + "fieldname": "item_group", + "label": _("Item Group"), + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "fieldname": "brand", + "label": _("Brand"), + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, {"fieldname": "description", "label": _("Description"), "width": 140}, - {"fieldname": "company", "label": _("Company"), "fieldtype": "Link", "options": "Company", "width": 100} + { + "fieldname": "company", + "label": _("Company"), + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, ] return columns @@ -93,12 +125,18 @@ def get_items(filters): item_details = frappe._dict() conditions = get_parent_item_conditions(filters) - parent_item_details = frappe.db.sql(""" + parent_item_details = frappe.db.sql( + """ select item.name as item_code, item.item_name, pb.description, item.item_group, item.brand, item.stock_uom from `tabItem` item inner join `tabProduct Bundle` pb on pb.new_item_code = item.name where ifnull(item.disabled, 0) = 0 {0} - """.format(conditions), filters, as_dict=1) # nosec + """.format( + conditions + ), + filters, + as_dict=1, + ) # nosec parent_items = [] for d in parent_item_details: @@ -106,7 +144,8 @@ def get_items(filters): item_details[d.item_code] = d if parent_items: - child_item_details = frappe.db.sql(""" + child_item_details = frappe.db.sql( + """ select pb.new_item_code as parent_item, pbi.item_code, item.item_name, pbi.description, item.item_group, item.brand, item.stock_uom, pbi.uom, pbi.qty @@ -114,7 +153,12 @@ def get_items(filters): inner join `tabProduct Bundle` pb on pb.name = pbi.parent inner join `tabItem` item on item.name = pbi.item_code where pb.new_item_code in ({0}) - """.format(", ".join(["%s"] * len(parent_items))), parent_items, as_dict=1) # nosec + """.format( + ", ".join(["%s"] * len(parent_items)) + ), + parent_items, + as_dict=1, + ) # nosec else: child_item_details = [] @@ -141,12 +185,14 @@ def get_stock_ledger_entries(filters, items): if not items: return [] - item_conditions_sql = ' and sle.item_code in ({})' \ - .format(', '.join(frappe.db.escape(i) for i in items)) + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i) for i in items) + ) conditions = get_sle_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company from @@ -154,7 +200,10 @@ def get_stock_ledger_entries(filters, items): left join `tabStock Ledger Entry` sle2 on sle.item_code = sle2.item_code and sle.warehouse = sle2.warehouse and (sle.posting_date, sle.posting_time, sle.name) < (sle2.posting_date, sle2.posting_time, sle2.name) - where sle2.name is null and sle.docstatus < 2 %s %s""" % (item_conditions_sql, conditions), as_dict=1) # nosec + where sle2.name is null and sle.docstatus < 2 %s %s""" + % (item_conditions_sql, conditions), + as_dict=1, + ) # nosec def get_parent_item_conditions(filters): @@ -180,9 +229,14 @@ def get_sle_conditions(filters): conditions += " and sle.posting_date <= %s" % frappe.db.escape(filters.get("date")) if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" % (warehouse_details.lft, warehouse_details.rgt) # nosec + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) # nosec return conditions diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index 97384427fa4..fe2d55a3913 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -8,7 +8,8 @@ from erpnext.controllers.trends import get_columns, get_data def execute(filters=None): - if not filters: filters ={} + if not filters: + filters = {} data = [] conditions = get_columns(filters, "Purchase Receipt") data = get_data(filters, conditions) @@ -17,6 +18,7 @@ def execute(filters=None): return conditions["columns"], data, None, chart_data + def get_chart_data(data, filters): if not data: return [] @@ -27,7 +29,7 @@ def get_chart_data(data, filters): # consider only consolidated row data = [row for row in data if row[0]] - data = sorted(data, key = lambda i: i[-1], reverse=True) + data = sorted(data, key=lambda i: i[-1], reverse=True) if len(data) > 10: # get top 10 if data too long @@ -39,14 +41,9 @@ def get_chart_data(data, filters): return { "data": { - "labels" : labels, - "datasets" : [ - { - "name": _("Total Received Amount"), - "values": datapoints - } - ] + "labels": labels, + "datasets": [{"name": _("Total Received Amount"), "values": datapoints}], }, - "type" : "bar", - "colors":["#5e64ff"] + "type": "bar", + "colors": ["#5e64ff"], } diff --git a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py index 80ec848e5b6..e439f51dd69 100644 --- a/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py +++ b/erpnext/stock/report/serial_no_ledger/serial_no_ledger.py @@ -12,42 +12,43 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): - columns = [{ - 'label': _('Posting Date'), - 'fieldtype': 'Date', - 'fieldname': 'posting_date' - }, { - 'label': _('Posting Time'), - 'fieldtype': 'Time', - 'fieldname': 'posting_time' - }, { - 'label': _('Voucher Type'), - 'fieldtype': 'Link', - 'fieldname': 'voucher_type', - 'options': 'DocType', - 'width': 220 - }, { - 'label': _('Voucher No'), - 'fieldtype': 'Dynamic Link', - 'fieldname': 'voucher_no', - 'options': 'voucher_type', - 'width': 220 - }, { - 'label': _('Company'), - 'fieldtype': 'Link', - 'fieldname': 'company', - 'options': 'Company', - 'width': 220 - }, { - 'label': _('Warehouse'), - 'fieldtype': 'Link', - 'fieldname': 'warehouse', - 'options': 'Warehouse', - 'width': 220 - }] + columns = [ + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"}, + {"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"}, + { + "label": _("Voucher Type"), + "fieldtype": "Link", + "fieldname": "voucher_type", + "options": "DocType", + "width": 220, + }, + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "options": "voucher_type", + "width": 220, + }, + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 220, + }, + { + "label": _("Warehouse"), + "fieldtype": "Link", + "fieldname": "warehouse", + "options": "Warehouse", + "width": 220, + }, + ] return columns + def get_data(filters): - return get_stock_ledger_entries(filters, '<=', order="asc") or [] + return get_stock_ledger_entries(filters, "<=", order="asc") or [] diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 7ca40033edb..1956238331e 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -25,6 +25,7 @@ def execute(filters: Filters = None) -> Tuple: return columns, data, None, chart_data + def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]: "Returns ordered, formatted data with ranges." _func = itemgetter(1) @@ -38,31 +39,38 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) - if not fifo_queue: continue + if not fifo_queue: + continue average_age = get_average_age(fifo_queue, to_date) earliest_age = date_diff(to_date, fifo_queue[0][1]) latest_age = date_diff(to_date, fifo_queue[-1][1]) range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict) - row = [details.name, details.item_name, details.description, - details.item_group, details.brand] + row = [details.name, details.item_name, details.description, details.item_group, details.brand] if filters.get("show_warehouse_wise_stock"): row.append(details.warehouse) - row.extend([ - flt(item_dict.get("total_qty"), precision), - average_age, - range1, range2, range3, above_range3, - earliest_age, latest_age, - details.stock_uom - ]) + row.extend( + [ + flt(item_dict.get("total_qty"), precision), + average_age, + range1, + range2, + range3, + above_range3, + earliest_age, + latest_age, + details.stock_uom, + ] + ) data.append(row) return data + def get_average_age(fifo_queue: List, to_date: str) -> float: batch_age = age_qty = total_qty = 0.0 for batch in fifo_queue: @@ -77,6 +85,7 @@ def get_average_age(fifo_queue: List, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 + def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) @@ -98,6 +107,7 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D return range1, range2, range3, above_range3 + def get_columns(filters: Filters) -> List[Dict]: range_columns = [] setup_ageing_columns(filters, range_columns) @@ -107,82 +117,55 @@ def get_columns(filters: Filters) -> List[Dict]: "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 100 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 200 + "width": 100, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200}, { "label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 100, }, { "label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", - "width": 100 - }] + "width": 100, + }, + ] if filters.get("show_warehouse_wise_stock"): - columns +=[{ - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100 - }] + columns += [ + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + } + ] - columns.extend([ - { - "label": _("Available Qty"), - "fieldname": "qty", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Average Age"), - "fieldname": "average_age", - "fieldtype": "Float", - "width": 100 - }]) + columns.extend( + [ + {"label": _("Available Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Average Age"), "fieldname": "average_age", "fieldtype": "Float", "width": 100}, + ] + ) columns.extend(range_columns) - columns.extend([ - { - "label": _("Earliest"), - "fieldname": "earliest", - "fieldtype": "Int", - "width": 80 - }, - { - "label": _("Latest"), - "fieldname": "latest", - "fieldtype": "Int", - "width": 80 - }, - { - "label": _("UOM"), - "fieldname": "uom", - "fieldtype": "Link", - "options": "UOM", - "width": 100 - } - ]) + columns.extend( + [ + {"label": _("Earliest"), "fieldname": "earliest", "fieldtype": "Int", "width": 80}, + {"label": _("Latest"), "fieldname": "latest", "fieldtype": "Int", "width": 80}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Link", "options": "UOM", "width": 100}, + ] + ) return columns + def get_chart_data(data: List, filters: Filters) -> Dict: if not data: return [] @@ -192,7 +175,7 @@ def get_chart_data(data: List, filters: Filters) -> Dict: if filters.get("show_warehouse_wise_stock"): return {} - data.sort(key = lambda row: row[6], reverse=True) + data.sort(key=lambda row: row[6], reverse=True) if len(data) > 10: data = data[:10] @@ -202,42 +185,33 @@ def get_chart_data(data: List, filters: Filters) -> Dict: datapoints.append(row[6]) return { - "data" : { - "labels": labels, - "datasets": [ - { - "name": _("Average Age"), - "values": datapoints - } - ] - }, - "type" : "bar" + "data": {"labels": labels, "datasets": [{"name": _("Average Age"), "values": datapoints}]}, + "type": "bar", } + def setup_ageing_columns(filters: Filters, range_columns: List): ranges = [ f"0 - {filters['range1']}", f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}", - f"{cint(filters['range3']) + 1} - {_('Above')}" + f"{cint(filters['range3']) + 1} - {_('Above')}", ] for i, label in enumerate(ranges): - fieldname = 'range' + str(i+1) - add_column(range_columns, label=f"Age ({label})",fieldname=fieldname) + fieldname = "range" + str(i + 1) + add_column(range_columns, label=f"Age ({label})", fieldname=fieldname) -def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str = 'Float', width: int = 140): - range_columns.append(dict( - label=label, - fieldname=fieldname, - fieldtype=fieldtype, - width=width - )) + +def add_column( + range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140 +): + range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width)) class FIFOSlots: "Returns FIFO computed slots of inwarded stock as per date." - def __init__(self, filters: Dict = None , sle: List = None): + def __init__(self, filters: Dict = None, sle: List = None): self.item_details = {} self.transferred_item_details = {} self.serial_no_batch_purchase_details = {} @@ -246,13 +220,13 @@ class FIFOSlots: def generate(self) -> Dict: """ - Returns dict of the foll.g structure: - Key = Item A / (Item A, Warehouse A) - Key: { - 'details' -> Dict: ** item details **, - 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, - consumed/updated and maintained via FIFO. ** - } + Returns dict of the foll.g structure: + Key = Item A / (Item A, Warehouse A) + Key: { + 'details' -> Dict: ** item details **, + 'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock, + consumed/updated and maintained via FIFO. ** + } """ if self.sle is None: self.sle = self.__get_stock_ledger_entries() @@ -292,7 +266,9 @@ class FIFOSlots: return key, fifo_queue, transferred_item_key - def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + def __compute_incoming_stock( + self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List + ): "Update FIFO Queue on inward stock." transfer_data = self.transferred_item_details.get(transfer_key) @@ -318,7 +294,9 @@ class FIFOSlots: self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) fifo_queue.append([serial_no, row.posting_date]) - def __compute_outgoing_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List): + def __compute_outgoing_stock( + self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List + ): "Update FIFO Queue on outward stock." if serial_nos: fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] @@ -384,15 +362,13 @@ class FIFOSlots: def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: "Aggregate Item-Wh wise data into single Item entry." item_aggregated_data = {} - for key,row in wh_wise_data.items(): + for key, row in wh_wise_data.items(): item = key[0] if not item_aggregated_data.get(item): - item_aggregated_data.setdefault(item, { - "details": frappe._dict(), - "fifo_queue": [], - "qty_after_transaction": 0.0, - "total_qty": 0.0 - }) + item_aggregated_data.setdefault( + item, + {"details": frappe._dict(), "fifo_queue": [], "qty_after_transaction": 0.0, "total_qty": 0.0}, + ) item_row = item_aggregated_data.get(item) item_row["details"].update(row["details"]) item_row["fifo_queue"].extend(row["fifo_queue"]) @@ -404,19 +380,29 @@ class FIFOSlots: def __get_stock_ledger_entries(self) -> List[Dict]: sle = frappe.qb.DocType("Stock Ledger Entry") - item = self.__get_item_query() # used as derived table in sle query + item = self.__get_item_query() # used as derived table in sle query sle_query = ( - frappe.qb.from_(sle).from_(item) + frappe.qb.from_(sle) + .from_(item) .select( - item.name, item.item_name, item.item_group, - item.brand, item.description, - item.stock_uom, item.has_serial_no, - sle.actual_qty, sle.posting_date, - sle.voucher_type, sle.voucher_no, - sle.serial_no, sle.batch_no, - sle.qty_after_transaction, sle.warehouse - ).where( + item.name, + item.item_name, + item.item_group, + item.brand, + item.description, + item.stock_uom, + item.has_serial_no, + sle.actual_qty, + sle.posting_date, + sle.voucher_type, + sle.voucher_no, + sle.serial_no, + sle.batch_no, + sle.qty_after_transaction, + sle.warehouse, + ) + .where( (sle.item_code == item.name) & (sle.company == self.filters.get("company")) & (sle.posting_date <= self.filters.get("to_date")) @@ -427,9 +413,7 @@ class FIFOSlots: if self.filters.get("warehouse"): sle_query = self.__get_warehouse_conditions(sle, sle_query) - sle_query = sle_query.orderby( - sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty - ) + sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) return sle_query.run(as_dict=True) @@ -437,8 +421,7 @@ class FIFOSlots: item_table = frappe.qb.DocType("Item") item = frappe.qb.from_("Item").select( - "name", "item_name", "description", "stock_uom", - "brand", "item_group", "has_serial_no" + "name", "item_name", "description", "stock_uom", "brand", "item_group", "has_serial_no" ) if self.filters.get("item_code"): @@ -451,18 +434,13 @@ class FIFOSlots: def __get_warehouse_conditions(self, sle, sle_query) -> str: warehouse = frappe.qb.DocType("Warehouse") - lft, rgt = frappe.db.get_value( - "Warehouse", - self.filters.get("warehouse"), - ['lft', 'rgt'] - ) + lft, rgt = frappe.db.get_value("Warehouse", self.filters.get("warehouse"), ["lft", "rgt"]) warehouse_results = ( frappe.qb.from_(warehouse) - .select("name").where( - (warehouse.lft >= lft) - & (warehouse.rgt <= rgt) - ).run() + .select("name") + .where((warehouse.lft >= lft) & (warehouse.rgt <= rgt)) + .run() ) warehouse_results = [x[0] for x in warehouse_results] diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index ca963b74863..fb363606233 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -10,9 +10,7 @@ from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_rep class TestStockAgeing(FrappeTestCase): def setUp(self) -> None: self.filters = frappe._dict( - company="_Test Company", - to_date="2021-12-10", - range1=30, range2=60, range3=90 + company="_Test Company", to_date="2021-12-10", range1=30, range2=60, range3=90 ) def test_normal_inward_outward_queue(self): @@ -20,28 +18,37 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=30, qty_after_transaction=30, + actual_qty=30, + qty_after_transaction=30, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=50, + actual_qty=20, + qty_after_transaction=50, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Entry", + posting_date="2021-12-02", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=40, + actual_qty=(-10), + qty_after_transaction=40, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -58,36 +65,48 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=(-30), qty_after_transaction=(-30), + actual_qty=(-30), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=(-10), + actual_qty=20, + qty_after_transaction=(-10), warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Entry", + posting_date="2021-12-02", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=20, qty_after_transaction=10, + actual_qty=20, + qty_after_transaction=10, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=10, qty_after_transaction=20, + actual_qty=10, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="004", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -107,28 +126,37 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=30, qty_after_transaction=30, + actual_qty=30, + qty_after_transaction=30, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=50, + actual_qty=0, + qty_after_transaction=50, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=40, + actual_qty=(-10), + qty_after_transaction=40, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -150,28 +178,37 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=1000, + actual_qty=0, + qty_after_transaction=1000, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Reconciliation", + posting_date="2021-12-01", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=400, + actual_qty=0, + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=390, + actual_qty=(-10), + qty_after_transaction=390, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="003", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -196,32 +233,41 @@ class TestStockAgeing(FrappeTestCase): sle = [ frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=1000, + actual_qty=0, + qty_after_transaction=1000, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Reconciliation", + posting_date="2021-12-01", + voucher_type="Stock Reconciliation", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=0, qty_after_transaction=400, + actual_qty=0, + qty_after_transaction=400, warehouse="WH 2", - posting_date="2021-12-02", voucher_type="Stock Reconciliation", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", voucher_no="003", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-10), qty_after_transaction=990, + actual_qty=(-10), + qty_after_transaction=990, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="004", - has_serial_no=False, serial_no=None - ) + has_serial_no=False, + serial_no=None, + ), ] item_wise_slots, item_wh_wise_slots = generate_item_and_item_wh_wise_slots( - filters=self.filters,sle=sle + filters=self.filters, sle=sle ) # test without 'show_warehouse_wise_stock' @@ -234,7 +280,9 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[1][0], 400.0) # test with 'show_warehouse_wise_stock' checked - item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots] + item_wh_balances = [ + item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots + ] self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"]) def test_repack_entry_same_item_split_rows(self): @@ -251,37 +299,49 @@ class TestStockAgeing(FrappeTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=450, + actual_qty=(-50), + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=400, + actual_qty=(-50), + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=100, qty_after_transaction=500, + actual_qty=100, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -308,29 +368,38 @@ class TestStockAgeing(FrappeTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-100), qty_after_transaction=400, + actual_qty=(-100), + qty_after_transaction=400, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=450, + actual_qty=50, + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -355,37 +424,49 @@ class TestStockAgeing(FrappeTestCase): Item 1 | 50 | 002 (repack) """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=20, qty_after_transaction=20, + actual_qty=20, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-30), + actual_qty=(-50), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-80), + actual_qty=(-50), + qty_after_transaction=(-80), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=(-30), + actual_qty=50, + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -397,7 +478,7 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[0][0], -30.0) # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("002", "Flask Item", "WH 1")] self.assertEqual(transfer_bucket[0][0], 50) def test_repack_entry_same_item_overproduce(self): @@ -413,29 +494,38 @@ class TestStockAgeing(FrappeTestCase): Case most likely for batch items. Test time bucket computation. """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=500, qty_after_transaction=500, + actual_qty=500, + qty_after_transaction=500, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=450, + actual_qty=(-50), + qty_after_transaction=450, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=100, qty_after_transaction=550, + actual_qty=100, + qty_after_transaction=550, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() @@ -461,37 +551,49 @@ class TestStockAgeing(FrappeTestCase): Item 1 | 50 | 002 (repack) """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=20, qty_after_transaction=20, + actual_qty=20, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-03", voucher_type="Stock Entry", + posting_date="2021-12-03", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-30), + actual_qty=(-50), + qty_after_transaction=(-30), warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=20, + actual_qty=50, + qty_after_transaction=20, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), frappe._dict( name="Flask Item", - actual_qty=50, qty_after_transaction=70, + actual_qty=50, + qty_after_transaction=70, warehouse="WH 1", - posting_date="2021-12-04", voucher_type="Stock Entry", + posting_date="2021-12-04", + voucher_type="Stock Entry", voucher_no="002", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -504,7 +606,7 @@ class TestStockAgeing(FrappeTestCase): self.assertEqual(queue[1][0], 50.0) # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('002', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("002", "Flask Item", "WH 1")] self.assertFalse(transfer_bucket) def test_negative_stock_same_voucher(self): @@ -519,29 +621,38 @@ class TestStockAgeing(FrappeTestCase): Item 1 | 80 | 001 """ sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-50), + actual_qty=(-50), + qty_after_transaction=(-50), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=(-50), qty_after_transaction=(-100), + actual_qty=(-50), + qty_after_transaction=(-100), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=30, qty_after_transaction=(-70), + actual_qty=30, + qty_after_transaction=(-70), warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] fifo_slots = FIFOSlots(self.filters, sle) @@ -549,59 +660,71 @@ class TestStockAgeing(FrappeTestCase): item_result = slots["Flask Item"] # check transfer bucket - transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("001", "Flask Item", "WH 1")] self.assertEqual(transfer_bucket[0][0], 20) self.assertEqual(transfer_bucket[1][0], 50) self.assertEqual(item_result["fifo_queue"][0][0], -70.0) - sle.append(frappe._dict( - name="Flask Item", - actual_qty=80, qty_after_transaction=10, - warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", - voucher_no="001", - has_serial_no=False, serial_no=None - )) + sle.append( + frappe._dict( + name="Flask Item", + actual_qty=80, + qty_after_transaction=10, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + serial_no=None, + ) + ) fifo_slots = FIFOSlots(self.filters, sle) slots = fifo_slots.generate() item_result = slots["Flask Item"] - transfer_bucket = fifo_slots.transferred_item_details[('001', 'Flask Item', 'WH 1')] + transfer_bucket = fifo_slots.transferred_item_details[("001", "Flask Item", "WH 1")] self.assertFalse(transfer_bucket) self.assertEqual(item_result["fifo_queue"][0][0], 10.0) def test_precision(self): "Test if final balance qty is rounded off correctly." sle = [ - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=0.3, qty_after_transaction=0.3, + actual_qty=0.3, + qty_after_transaction=0.3, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), - frappe._dict( # stock up item + frappe._dict( # stock up item name="Flask Item", - actual_qty=0.6, qty_after_transaction=0.9, + actual_qty=0.6, + qty_after_transaction=0.9, warehouse="WH 1", - posting_date="2021-12-01", voucher_type="Stock Entry", + posting_date="2021-12-01", + voucher_type="Stock Entry", voucher_no="001", - has_serial_no=False, serial_no=None + has_serial_no=False, + serial_no=None, ), ] slots = FIFOSlots(self.filters, sle).generate() report_data = format_report_data(self.filters, slots, self.filters["to_date"]) - row = report_data[0] # first row in report + row = report_data[0] # first row in report bal_qty = row[5] - range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance + range_qty_sum = sum([i for i in row[7:11]]) # get sum of range balance # check if value of Available Qty column matches with range bucket post format self.assertEqual(bal_qty, 0.9) self.assertEqual(bal_qty, range_qty_sum) + def generate_item_and_item_wh_wise_slots(filters, sle): "Return results with and without 'show_warehouse_wise_stock'" item_wise_slots = FIFOSlots(filters, sle).generate() diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index ddc831037bf..da0776b9a84 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -25,84 +25,64 @@ def execute(filters=None): return columns, data, None, chart + def get_columns(filters): columns = [ - { - "label": _("Item"), - "options":"Item", - "fieldname": "name", - "fieldtype": "Link", - "width": 140 - }, + {"label": _("Item"), "options": "Item", "fieldname": "name", "fieldtype": "Link", "width": 140}, { "label": _("Item Name"), - "options":"Item", + "options": "Item", "fieldname": "item_name", "fieldtype": "Link", - "width": 140 + "width": 140, }, { "label": _("Item Group"), - "options":"Item Group", + "options": "Item Group", "fieldname": "item_group", "fieldtype": "Link", - "width": 140 + "width": 140, }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("UOM"), - "fieldname": "uom", - "fieldtype": "Data", - "width": 120 - }] + {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Data", "width": 120}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Data", "width": 120}, + ] ranges = get_period_date_ranges(filters) for dummy, end_date in ranges: period = get_period(end_date, filters) - columns.append({ - "label": _(period), - "fieldname":scrub(period), - "fieldtype": "Float", - "width": 120 - }) + columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120} + ) return columns + def get_period_date_ranges(filters): - from dateutil.relativedelta import relativedelta - from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) - to_date = getdate(filters.to_date) + from dateutil.relativedelta import relativedelta - increment = { - "Monthly": 1, - "Quarterly": 3, - "Half-Yearly": 6, - "Yearly": 12 - }.get(filters.range,1) + from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) + to_date = getdate(filters.to_date) - periodic_daterange = [] - for dummy in range(1, 53, increment): - if filters.range == "Weekly": - period_end_date = from_date + relativedelta(days=6) - else: - period_end_date = from_date + relativedelta(months=increment, days=-1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get(filters.range, 1) - if period_end_date > to_date: - period_end_date = to_date - periodic_daterange.append([from_date, period_end_date]) + periodic_daterange = [] + for dummy in range(1, 53, increment): + if filters.range == "Weekly": + period_end_date = from_date + relativedelta(days=6) + else: + period_end_date = from_date + relativedelta(months=increment, days=-1) - from_date = period_end_date + relativedelta(days=1) - if period_end_date == to_date: - break + if period_end_date > to_date: + period_end_date = to_date + periodic_daterange.append([from_date, period_end_date]) - return periodic_daterange + from_date = period_end_date + relativedelta(days=1) + if period_end_date == to_date: + break + + return periodic_daterange def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datetime: @@ -132,12 +112,12 @@ def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datet def get_period(posting_date, filters): months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - if filters.range == 'Weekly': + if filters.range == "Weekly": period = "Week " + str(posting_date.isocalendar()[1]) + " " + str(posting_date.year) - elif filters.range == 'Monthly': + elif filters.range == "Monthly": period = str(months[posting_date.month - 1]) + " " + str(posting_date.year) - elif filters.range == 'Quarterly': - period = "Quarter " + str(((posting_date.month-1)//3)+1) +" " + str(posting_date.year) + elif filters.range == "Quarterly": + period = "Quarter " + str(((posting_date.month - 1) // 3) + 1) + " " + str(posting_date.year) else: year = get_fiscal_year(posting_date, company=filters.company) period = str(year[2]) @@ -147,26 +127,26 @@ def get_period(posting_date, filters): def get_periodic_data(entry, filters): """Structured as: - Item 1 - - Balance (updated and carried forward): - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jun 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jul 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - Item 2 - - Balance (updated and carried forward): - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jun 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value - - Jul 2021 (sum of warehouse quantities used in report) - - Warehouse A : bal_qty/value - - Warehouse B : bal_qty/value + Item 1 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + Item 2 + - Balance (updated and carried forward): + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jun 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value + - Jul 2021 (sum of warehouse quantities used in report) + - Warehouse A : bal_qty/value + - Warehouse B : bal_qty/value """ periodic_data = {} for d in entry: @@ -176,31 +156,36 @@ def get_periodic_data(entry, filters): # if period against item does not exist yet, instantiate it # insert existing balance dict against period, and add/subtract to it if periodic_data.get(d.item_code) and not periodic_data.get(d.item_code).get(period): - previous_balance = periodic_data[d.item_code]['balance'].copy() + previous_balance = periodic_data[d.item_code]["balance"].copy() periodic_data[d.item_code][period] = previous_balance if d.voucher_type == "Stock Reconciliation": - if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get('balance').get(d.warehouse): - bal_qty = periodic_data[d.item_code]['balance'][d.warehouse] + if periodic_data.get(d.item_code) and periodic_data.get(d.item_code).get("balance").get( + d.warehouse + ): + bal_qty = periodic_data[d.item_code]["balance"][d.warehouse] qty_diff = d.qty_after_transaction - bal_qty else: qty_diff = d.actual_qty - if filters["value_quantity"] == 'Quantity': + if filters["value_quantity"] == "Quantity": value = qty_diff else: value = d.stock_value_difference # period-warehouse wise balance - periodic_data.setdefault(d.item_code, {}).setdefault('balance', {}).setdefault(d.warehouse, 0.0) + periodic_data.setdefault(d.item_code, {}).setdefault("balance", {}).setdefault(d.warehouse, 0.0) periodic_data.setdefault(d.item_code, {}).setdefault(period, {}).setdefault(d.warehouse, 0.0) - periodic_data[d.item_code]['balance'][d.warehouse] += value - periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]['balance'][d.warehouse] + periodic_data[d.item_code]["balance"][d.warehouse] += value + periodic_data[d.item_code][period][d.warehouse] = periodic_data[d.item_code]["balance"][ + d.warehouse + ] return periodic_data + def get_data(filters): data = [] items = get_items(filters) @@ -229,14 +214,10 @@ def get_data(filters): return data + def get_chart_data(columns): labels = [d.get("label") for d in columns[5:]] - chart = { - "data": { - 'labels': labels, - 'datasets':[] - } - } + chart = {"data": {"labels": labels, "datasets": []}} chart["type"] = "line" return chart diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 6fd3fe7da48..99f820ecac6 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -12,21 +12,25 @@ from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_ac def execute(filters=None): if not erpnext.is_perpetual_inventory_enabled(filters.company): - frappe.throw(_("Perpetual inventory required for the company {0} to view this report.") - .format(filters.company)) + frappe.throw( + _("Perpetual inventory required for the company {0} to view this report.").format( + filters.company + ) + ) data = get_data(filters) columns = get_columns(filters) return columns, data + def get_data(report_filters): data = [] filters = { "is_cancelled": 0, "company": report_filters.company, - "posting_date": ("<=", report_filters.as_on_date) + "posting_date": ("<=", report_filters.as_on_date), } currency_precision = get_currency_precision() or 2 @@ -43,18 +47,28 @@ def get_data(report_filters): return data + def get_stock_ledger_data(report_filters, filters): if report_filters.account: - warehouses = get_warehouses_based_on_account(report_filters.account, - report_filters.company) + warehouses = get_warehouses_based_on_account(report_filters.account, report_filters.company) filters["warehouse"] = ("in", warehouses) - return frappe.get_all("Stock Ledger Entry", filters=filters, - fields = ["name", "voucher_type", "voucher_no", - "sum(stock_value_difference) as stock_value", "posting_date", "posting_time"], - group_by = "voucher_type, voucher_no", - order_by = "posting_date ASC, posting_time ASC") + return frappe.get_all( + "Stock Ledger Entry", + filters=filters, + fields=[ + "name", + "voucher_type", + "voucher_no", + "sum(stock_value_difference) as stock_value", + "posting_date", + "posting_time", + ], + group_by="voucher_type, voucher_no", + order_by="posting_date ASC, posting_time ASC", + ) + def get_gl_data(report_filters, filters): if report_filters.account: @@ -62,17 +76,22 @@ def get_gl_data(report_filters, filters): else: stock_accounts = get_stock_accounts(report_filters.company) - filters.update({ - "account": ("in", stock_accounts) - }) + filters.update({"account": ("in", stock_accounts)}) if filters.get("warehouse"): del filters["warehouse"] - gl_entries = frappe.get_all("GL Entry", filters=filters, - fields = ["name", "voucher_type", "voucher_no", - "sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value"], - group_by = "voucher_type, voucher_no") + gl_entries = frappe.get_all( + "GL Entry", + filters=filters, + fields=[ + "name", + "voucher_type", + "voucher_no", + "sum(debit_in_account_currency) - sum(credit_in_account_currency) as account_value", + ], + group_by="voucher_type, voucher_no", + ) voucher_wise_gl_data = {} for d in gl_entries: @@ -81,6 +100,7 @@ def get_gl_data(report_filters, filters): return voucher_wise_gl_data + def get_columns(filters): return [ { @@ -88,46 +108,29 @@ def get_columns(filters): "fieldname": "name", "fieldtype": "Link", "options": "Stock Ledger Entry", - "width": "80" - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date" - }, - { - "label": _("Posting Time"), - "fieldname": "posting_time", - "fieldtype": "Time" - }, - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": "110" + "width": "80", }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"}, + {"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"}, { "label": _("Voucher No"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", - "width": "110" - }, - { - "label": _("Stock Value"), - "fieldname": "stock_value", - "fieldtype": "Currency", - "width": "120" + "width": "110", }, + {"label": _("Stock Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": "120"}, { "label": _("Account Value"), "fieldname": "account_value", "fieldtype": "Currency", - "width": "120" + "width": "120", }, { "label": _("Difference Value"), "fieldname": "difference_value", "fieldtype": "Currency", - "width": "120" - } + "width": "120", + }, ] diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index a4608039310..261383d4a20 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -17,10 +17,11 @@ from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_va def execute(filters=None): is_reposting_item_valuation_in_progress() - if not filters: filters = {} + if not filters: + filters = {} - from_date = filters.get('from_date') - to_date = filters.get('to_date') + from_date = filters.get("from_date") + to_date = filters.get("to_date") if filters.get("company"): company_currency = erpnext.get_company_currency(filters.get("company")) @@ -32,8 +33,8 @@ def execute(filters=None): items = get_items(filters) sle = get_stock_ledger_entries(filters, items) - if filters.get('show_stock_ageing_data'): - filters['show_warehouse_wise_stock'] = True + if filters.get("show_stock_ageing_data"): + filters["show_warehouse_wise_stock"] = True item_wise_fifo_queue = FIFOSlots(filters, sle).generate() # if no stock ledger entry found return @@ -59,12 +60,12 @@ def execute(filters=None): item_reorder_qty = item_reorder_detail_map[item + warehouse]["warehouse_reorder_qty"] report_data = { - 'currency': company_currency, - 'item_code': item, - 'warehouse': warehouse, - 'company': company, - 'reorder_level': item_reorder_level, - 'reorder_qty': item_reorder_qty, + "currency": company_currency, + "item_code": item, + "warehouse": warehouse, + "company": company, + "reorder_level": item_reorder_level, + "reorder_qty": item_reorder_qty, } report_data.update(item_map[item]) report_data.update(qty_dict) @@ -72,21 +73,18 @@ def execute(filters=None): if include_uom: conversion_factors.setdefault(item, item_map[item].conversion_factor) - if filters.get('show_stock_ageing_data'): - fifo_queue = item_wise_fifo_queue[(item, warehouse)].get('fifo_queue') + if filters.get("show_stock_ageing_data"): + fifo_queue = item_wise_fifo_queue[(item, warehouse)].get("fifo_queue") - stock_ageing_data = { - 'average_age': 0, - 'earliest_age': 0, - 'latest_age': 0 - } + stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} if fifo_queue: fifo_queue = sorted(filter(_func, fifo_queue), key=_func) - if not fifo_queue: continue + if not fifo_queue: + continue - stock_ageing_data['average_age'] = get_average_age(fifo_queue, to_date) - stock_ageing_data['earliest_age'] = date_diff(to_date, fifo_queue[0][1]) - stock_ageing_data['latest_age'] = date_diff(to_date, fifo_queue[-1][1]) + stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) + stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) + stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) report_data.update(stock_ageing_data) @@ -95,38 +93,130 @@ def execute(filters=None): add_additional_uom_columns(columns, data, include_uom, conversion_factors) return columns, data + def get_columns(filters): """return columns""" columns = [ - {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, - {"label": _("Balance Qty"), "fieldname": "bal_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Balance Value"), "fieldname": "bal_val", "fieldtype": "Currency", "width": 100, "options": "currency"}, - {"label": _("Opening Qty"), "fieldname": "opening_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Opening Value"), "fieldname": "opening_val", "fieldtype": "Currency", "width": 110, "options": "currency"}, - {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Balance Qty"), + "fieldname": "bal_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Balance Value"), + "fieldname": "bal_val", + "fieldtype": "Currency", + "width": 100, + "options": "currency", + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Opening Value"), + "fieldname": "opening_val", + "fieldtype": "Currency", + "width": 110, + "options": "currency", + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80}, - {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80}, - {"label": _("Valuation Rate"), "fieldname": "val_rate", "fieldtype": "Currency", "width": 90, "convertible": "rate", "options": "currency"}, - {"label": _("Reorder Level"), "fieldname": "reorder_level", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Reorder Qty"), "fieldname": "reorder_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 100} + { + "label": _("Valuation Rate"), + "fieldname": "val_rate", + "fieldtype": "Currency", + "width": 90, + "convertible": "rate", + "options": "currency", + }, + { + "label": _("Reorder Level"), + "fieldname": "reorder_level", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Reorder Qty"), + "fieldname": "reorder_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, ] - if filters.get('show_stock_ageing_data'): - columns += [{'label': _('Average Age'), 'fieldname': 'average_age', 'width': 100}, - {'label': _('Earliest Age'), 'fieldname': 'earliest_age', 'width': 100}, - {'label': _('Latest Age'), 'fieldname': 'latest_age', 'width': 100}] + if filters.get("show_stock_ageing_data"): + columns += [ + {"label": _("Average Age"), "fieldname": "average_age", "width": 100}, + {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100}, + {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100}, + ] - if filters.get('show_variant_attributes'): - columns += [{'label': att_name, 'fieldname': att_name, 'width': 100} for att_name in get_variants_attributes()] + if filters.get("show_variant_attributes"): + columns += [ + {"label": att_name, "fieldname": att_name, "width": 100} + for att_name in get_variants_attributes() + ] return columns + def get_conditions(filters): conditions = "" if not filters.get("from_date"): @@ -141,28 +231,37 @@ def get_conditions(filters): conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company")) if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", - filters.get("warehouse"), ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) if filters.get("warehouse_type") and not filters.get("warehouse"): - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.warehouse_type = '%s' and sle.warehouse = wh.name)"%(filters.get("warehouse_type")) + conditions += ( + " and exists (select name from `tabWarehouse` wh \ + where wh.warehouse_type = '%s' and sle.warehouse = wh.name)" + % (filters.get("warehouse_type")) + ) return conditions + def get_stock_ledger_entries(filters, items): - item_conditions_sql = '' + item_conditions_sql = "" if items: - item_conditions_sql = ' and sle.item_code in ({})'\ - .format(', '.join(frappe.db.escape(i, percent=False) for i in items)) + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i, percent=False) for i in items) + ) conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + """ select sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, @@ -171,8 +270,11 @@ def get_stock_ledger_entries(filters, items): `tabStock Ledger Entry` sle where sle.docstatus < 2 %s %s and is_cancelled = 0 - order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" % #nosec - (item_conditions_sql, conditions), as_dict=1) + order by sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty""" + % (item_conditions_sql, conditions), # nosec + as_dict=1, + ) + def get_item_warehouse_map(filters, sle): iwb_map = {} @@ -184,13 +286,19 @@ def get_item_warehouse_map(filters, sle): for d in sle: key = (d.company, d.item_code, d.warehouse) if key not in iwb_map: - iwb_map[key] = frappe._dict({ - "opening_qty": 0.0, "opening_val": 0.0, - "in_qty": 0.0, "in_val": 0.0, - "out_qty": 0.0, "out_val": 0.0, - "bal_qty": 0.0, "bal_val": 0.0, - "val_rate": 0.0 - }) + iwb_map[key] = frappe._dict( + { + "opening_qty": 0.0, + "opening_val": 0.0, + "in_qty": 0.0, + "in_val": 0.0, + "out_qty": 0.0, + "out_val": 0.0, + "bal_qty": 0.0, + "bal_val": 0.0, + "val_rate": 0.0, + } + ) qty_dict = iwb_map[(d.company, d.item_code, d.warehouse)] @@ -201,9 +309,11 @@ def get_item_warehouse_map(filters, sle): value_diff = flt(d.stock_value_difference) - if d.posting_date < from_date or (d.posting_date == from_date - and d.voucher_type == "Stock Reconciliation" and - frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"): + if d.posting_date < from_date or ( + d.posting_date == from_date + and d.voucher_type == "Stock Reconciliation" + and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock" + ): qty_dict.opening_qty += qty_diff qty_dict.opening_val += value_diff @@ -223,6 +333,7 @@ def get_item_warehouse_map(filters, sle): return iwb_map + def filter_items_with_no_transactions(iwb_map, float_precision): for (company, item, warehouse) in sorted(iwb_map): qty_dict = iwb_map[(company, item, warehouse)] @@ -239,6 +350,7 @@ def filter_items_with_no_transactions(iwb_map, float_precision): return iwb_map + def get_items(filters): "Get items based on item code, item group or brand." conditions = [] @@ -247,15 +359,17 @@ def get_items(filters): else: if filters.get("item_group"): conditions.append(get_item_group_condition(filters.get("item_group"))) - if filters.get("brand"): # used in stock analytics report + if filters.get("brand"): # used in stock analytics report conditions.append("item.brand=%(brand)s") items = [] if conditions: - items = frappe.db.sql_list("""select name from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters) + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters + ) return items + def get_item_details(items, sle, filters): item_details = {} if not items: @@ -267,10 +381,13 @@ def get_item_details(items, sle, filters): cf_field = cf_join = "" if filters.get("include_uom"): cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" % frappe.db.escape(filters.get("include_uom")) + ) - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom %s from @@ -278,40 +395,57 @@ def get_item_details(items, sle, filters): %s where item.name in (%s) - """ % (cf_field, cf_join, ','.join(['%s'] *len(items))), items, as_dict=1) + """ + % (cf_field, cf_join, ",".join(["%s"] * len(items))), + items, + as_dict=1, + ) for item in res: item_details.setdefault(item.name, item) - if filters.get('show_variant_attributes', 0) == 1: + if filters.get("show_variant_attributes", 0) == 1: variant_values = get_variant_values_for(list(item_details)) item_details = {k: v.update(variant_values.get(k, {})) for k, v in iteritems(item_details)} return item_details + def get_item_reorder_details(items): item_reorder_details = frappe._dict() if items: - item_reorder_details = frappe.db.sql(""" + item_reorder_details = frappe.db.sql( + """ select parent, warehouse, warehouse_reorder_qty, warehouse_reorder_level from `tabItem Reorder` where parent in ({0}) - """.format(', '.join(frappe.db.escape(i, percent=False) for i in items)), as_dict=1) + """.format( + ", ".join(frappe.db.escape(i, percent=False) for i in items) + ), + as_dict=1, + ) return dict((d.parent + d.warehouse, d) for d in item_reorder_details) + def get_variants_attributes(): - '''Return all item variant attributes.''' - return [i.name for i in frappe.get_all('Item Attribute')] + """Return all item variant attributes.""" + return [i.name for i in frappe.get_all("Item Attribute")] + def get_variant_values_for(items): - '''Returns variant values for items.''' + """Returns variant values for items.""" attribute_map = {} - for attr in frappe.db.sql('''select parent, attribute, attribute_value + for attr in frappe.db.sql( + """select parent, attribute, attribute_value from `tabItem Variant Attribute` where parent in (%s) - ''' % ", ".join(["%s"] * len(items)), tuple(items), as_dict=1): - attribute_map.setdefault(attr['parent'], {}) - attribute_map[attr['parent']].update({attr['attribute']: attr['attribute_value']}) + """ + % ", ".join(["%s"] * len(items)), + tuple(items), + as_dict=1, + ): + attribute_map.setdefault(attr["parent"], {}) + attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) return attribute_map diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 81fa0458f29..b1fdaacac9d 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -41,19 +41,13 @@ def execute(filters=None): actual_qty += flt(sle.actual_qty, precision) stock_value += sle.stock_value_difference - if sle.voucher_type == 'Stock Reconciliation' and not sle.actual_qty: + if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: actual_qty = sle.qty_after_transaction stock_value = sle.stock_value - sle.update({ - "qty_after_transaction": actual_qty, - "stock_value": stock_value - }) + sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value}) - sle.update({ - "in_qty": max(sle.actual_qty, 0), - "out_qty": min(sle.actual_qty, 0) - }) + sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)}) if sle.serial_no: update_available_serial_nos(available_serial_nos, sle) @@ -66,6 +60,7 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data + def update_available_serial_nos(available_serial_nos, sle): serial_nos = get_serial_nos(sle.serial_no) key = (sle.item_code, sle.warehouse) @@ -85,45 +80,158 @@ def update_available_serial_nos(available_serial_nos, sle): else: existing_serial_no.append(sn) - sle.balance_serial_no = '\n'.join(existing_serial_no) + sle.balance_serial_no = "\n".join(existing_serial_no) + def get_columns(): columns = [ {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150}, - {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, - {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 150}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 150}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Balance Qty"), + "fieldname": "qty_after_transaction", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 150, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 150, + }, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Brand"), + "fieldname": "brand", + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, {"label": _("Description"), "fieldname": "description", "width": 200}, - {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, - {"label": _("Value Change"), "fieldname": "stock_value_difference", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, + { + "label": _("Incoming Rate"), + "fieldname": "incoming_rate", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Valuation Rate"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + "convertible": "rate", + }, + { + "label": _("Balance Value"), + "fieldname": "stock_value", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + }, + { + "label": _("Value Change"), + "fieldname": "stock_value_difference", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + }, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, - {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, - {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, - {"label": _("Serial No"), "fieldname": "serial_no", "fieldtype": "Link", "options": "Serial No", "width": 100}, + { + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 100, + }, + { + "label": _("Batch"), + "fieldname": "batch_no", + "fieldtype": "Link", + "options": "Batch", + "width": 100, + }, + { + "label": _("Serial No"), + "fieldname": "serial_no", + "fieldtype": "Link", + "options": "Serial No", + "width": 100, + }, {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, - {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, - {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} + { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 100, + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 110, + }, ] return columns def get_stock_ledger_entries(filters, items): - item_conditions_sql = '' + item_conditions_sql = "" if items: - item_conditions_sql = 'and sle.item_code in ({})'\ - .format(', '.join(frappe.db.escape(i) for i in items)) + item_conditions_sql = "and sle.item_code in ({})".format( + ", ".join(frappe.db.escape(i) for i in items) + ) - sl_entries = frappe.db.sql(""" + sl_entries = frappe.db.sql( + """ SELECT concat_ws(" ", posting_date, posting_time) AS date, item_code, @@ -149,8 +257,12 @@ def get_stock_ledger_entries(filters, items): {item_conditions_sql} ORDER BY posting_date asc, posting_time asc, creation asc - """.format(sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql), - filters, as_dict=1) + """.format( + sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql + ), + filters, + as_dict=1, + ) return sl_entries @@ -167,8 +279,9 @@ def get_items(filters): items = [] if conditions: - items = frappe.db.sql_list("""select name from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters) + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters + ) return items @@ -183,10 +296,13 @@ def get_item_details(items, sl_entries, include_uom): cf_field = cf_join = "" if include_uom: cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" % frappe.db.escape(include_uom) + ) - res = frappe.db.sql(""" + res = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom {cf_field} from @@ -194,7 +310,12 @@ def get_item_details(items, sl_entries, include_uom): {cf_join} where item.name in ({item_codes}) - """.format(cf_field=cf_field, cf_join=cf_join, item_codes=','.join(['%s'] *len(items))), items, as_dict=1) + """.format( + cf_field=cf_field, cf_join=cf_join, item_codes=",".join(["%s"] * len(items)) + ), + items, + as_dict=1, + ) for item in res: item_details.setdefault(item.name, item) @@ -223,16 +344,20 @@ def get_opening_balance(filters, columns, sl_entries): return from erpnext.stock.stock_ledger import get_previous_sle - last_entry = get_previous_sle({ - "item_code": filters.item_code, - "warehouse_condition": get_warehouse_condition(filters.warehouse), - "posting_date": filters.from_date, - "posting_time": "00:00:00" - }) + + last_entry = get_previous_sle( + { + "item_code": filters.item_code, + "warehouse_condition": get_warehouse_condition(filters.warehouse), + "posting_date": filters.from_date, + "posting_time": "00:00:00", + } + ) # check if any SLEs are actually Opening Stock Reconciliation for sle in sl_entries: - if (sle.get("voucher_type") == "Stock Reconciliation" + if ( + sle.get("voucher_type") == "Stock Reconciliation" and sle.get("date").split()[0] == filters.from_date and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock" ): @@ -243,7 +368,7 @@ def get_opening_balance(filters, columns, sl_entries): "item_code": _("'Opening'"), "qty_after_transaction": last_entry.get("qty_after_transaction", 0), "valuation_rate": last_entry.get("valuation_rate", 0), - "stock_value": last_entry.get("stock_value", 0) + "stock_value": last_entry.get("stock_value", 0), } return row @@ -252,18 +377,22 @@ def get_opening_balance(filters, columns, sl_entries): def get_warehouse_condition(warehouse): warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) if warehouse_details: - return " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt) + return ( + " exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) - return '' + return "" def get_item_group_condition(item_group): item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1) if item_group_details: - return "item.item_group in (select ig.name from `tabItem Group` ig \ - where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)"%(item_group_details.lft, - item_group_details.rgt) + return ( + "item.item_group in (select ig.name from `tabItem Group` ig \ + where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)" + % (item_group_details.lft, item_group_details.rgt) + ) - return '' + return "" diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 1b61863ce6a..98f0387a0fc 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -40,11 +40,7 @@ def get_stock_ledger_entries(filters): return frappe.get_all( "Stock Ledger Entry", fields=SLE_FIELDS, - filters={ - "item_code": filters.item_code, - "warehouse": filters.warehouse, - "is_cancelled": 0 - }, + filters={"item_code": filters.item_code, "warehouse": filters.warehouse, "is_cancelled": 0}, order_by="timestamp(posting_date, posting_time), creation", ) @@ -62,7 +58,7 @@ def add_invariant_check_fields(sles): fifo_value += qty * rate if sle.actual_qty < 0: - sle.consumption_rate = sle.stock_value_difference / sle.actual_qty + sle.consumption_rate = sle.stock_value_difference / sle.actual_qty balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference @@ -90,7 +86,7 @@ def add_invariant_check_fields(sles): sle.valuation_diff = ( sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None ) - sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value + sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value if idx > 0: sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value @@ -175,7 +171,6 @@ def get_columns(): "fieldtype": "Data", "label": "FIFO Queue", }, - { "fieldname": "fifo_queue_qty", "fieldtype": "Float", @@ -236,7 +231,6 @@ def get_columns(): "fieldtype": "Float", "label": "(I) Valuation Rate as per FIFO", }, - { "fieldname": "fifo_valuation_diff", "fieldtype": "Float", diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index a28b75250bf..49e797d6a30 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -32,8 +32,9 @@ def execute(filters=None): continue # item = item_map.setdefault(bin.item_code, get_item(bin.item_code)) - company = warehouse_company.setdefault(bin.warehouse, - frappe.db.get_value("Warehouse", bin.warehouse, "company")) + company = warehouse_company.setdefault( + bin.warehouse, frappe.db.get_value("Warehouse", bin.warehouse, "company") + ) if filters.brand and filters.brand != item.brand: continue @@ -59,10 +60,29 @@ def execute(filters=None): if reserved_qty_for_pos: bin.projected_qty -= reserved_qty_for_pos - data.append([item.name, item.item_name, item.description, item.item_group, item.brand, bin.warehouse, - item.stock_uom, bin.actual_qty, bin.planned_qty, bin.indented_qty, bin.ordered_qty, - bin.reserved_qty, bin.reserved_qty_for_production, bin.reserved_qty_for_sub_contract, reserved_qty_for_pos, - bin.projected_qty, re_order_level, re_order_qty, shortage_qty]) + data.append( + [ + item.name, + item.item_name, + item.description, + item.item_group, + item.brand, + bin.warehouse, + item.stock_uom, + bin.actual_qty, + bin.planned_qty, + bin.indented_qty, + bin.ordered_qty, + bin.reserved_qty, + bin.reserved_qty_for_production, + bin.reserved_qty_for_sub_contract, + reserved_qty_for_pos, + bin.projected_qty, + re_order_level, + re_order_qty, + shortage_qty, + ] + ) if include_uom: conversion_factors.append(item.conversion_factor) @@ -70,66 +90,180 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data + def get_columns(): return [ - {"label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 140}, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140, + }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, {"label": _("Description"), "fieldname": "description", "width": 200}, - {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 120}, - {"label": _("UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 100}, - {"label": _("Actual Qty"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Planned Qty"), "fieldname": "planned_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Requested Qty"), "fieldname": "indented_qty", "fieldtype": "Float", "width": 110, "convertible": "qty"}, - {"label": _("Ordered Qty"), "fieldname": "ordered_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reserved Qty"), "fieldname": "reserved_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reserved for Production"), "fieldname": "reserved_qty_for_production", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Reserved for Sub Contracting"), "fieldname": "reserved_qty_for_sub_contract", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Reserved for POS Transactions"), "fieldname": "reserved_qty_for_pos", "fieldtype": "Float", - "width": 100, "convertible": "qty"}, - {"label": _("Projected Qty"), "fieldname": "projected_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reorder Level"), "fieldname": "re_order_level", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Reorder Qty"), "fieldname": "re_order_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Shortage Qty"), "fieldname": "shortage_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"} + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Brand"), + "fieldname": "brand", + "fieldtype": "Link", + "options": "Brand", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 120, + }, + { + "label": _("UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, + }, + { + "label": _("Actual Qty"), + "fieldname": "actual_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Planned Qty"), + "fieldname": "planned_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Requested Qty"), + "fieldname": "indented_qty", + "fieldtype": "Float", + "width": 110, + "convertible": "qty", + }, + { + "label": _("Ordered Qty"), + "fieldname": "ordered_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved Qty"), + "fieldname": "reserved_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for Production"), + "fieldname": "reserved_qty_for_production", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for Sub Contracting"), + "fieldname": "reserved_qty_for_sub_contract", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reserved for POS Transactions"), + "fieldname": "reserved_qty_for_pos", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Projected Qty"), + "fieldname": "projected_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reorder Level"), + "fieldname": "re_order_level", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Reorder Qty"), + "fieldname": "re_order_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Shortage Qty"), + "fieldname": "shortage_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, ] + def get_bin_list(filters): conditions = [] if filters.item_code: - conditions.append("item_code = '%s' "%filters.item_code) + conditions.append("item_code = '%s' " % filters.item_code) if filters.warehouse: - warehouse_details = frappe.db.get_value("Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.warehouse, ["lft", "rgt"], as_dict=1 + ) if warehouse_details: - conditions.append(" exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)"%(warehouse_details.lft, - warehouse_details.rgt)) + conditions.append( + " exists (select name from `tabWarehouse` wh \ + where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)" + % (warehouse_details.lft, warehouse_details.rgt) + ) - bin_list = frappe.db.sql("""select item_code, warehouse, actual_qty, planned_qty, indented_qty, + bin_list = frappe.db.sql( + """select item_code, warehouse, actual_qty, planned_qty, indented_qty, ordered_qty, reserved_qty, reserved_qty_for_production, reserved_qty_for_sub_contract, projected_qty from tabBin bin {conditions} order by item_code, warehouse - """.format(conditions=" where " + " and ".join(conditions) if conditions else ""), as_dict=1) + """.format( + conditions=" where " + " and ".join(conditions) if conditions else "" + ), + as_dict=1, + ) return bin_list + def get_item_map(item_code, include_uom): """Optimization: get only the item doc and re_order_levels table""" condition = "" if item_code: - condition = 'and item_code = {0}'.format(frappe.db.escape(item_code, percent=False)) + condition = "and item_code = {0}".format(frappe.db.escape(item_code, percent=False)) cf_field = cf_join = "" if include_uom: cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + cf_join = ( + "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + ) - items = frappe.db.sql(""" + items = frappe.db.sql( + """ select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom{cf_field} from `tabItem` item {cf_join} @@ -137,16 +271,21 @@ def get_item_map(item_code, include_uom): and item.disabled=0 {condition} and (item.end_of_life > %(today)s or item.end_of_life is null or item.end_of_life='0000-00-00') - and exists (select name from `tabBin` bin where bin.item_code=item.name)"""\ - .format(cf_field=cf_field, cf_join=cf_join, condition=condition), - {"today": today(), "include_uom": include_uom}, as_dict=True) + and exists (select name from `tabBin` bin where bin.item_code=item.name)""".format( + cf_field=cf_field, cf_join=cf_join, condition=condition + ), + {"today": today(), "include_uom": include_uom}, + as_dict=True, + ) condition = "" if item_code: - condition = 'where parent={0}'.format(frappe.db.escape(item_code, percent=False)) + condition = "where parent={0}".format(frappe.db.escape(item_code, percent=False)) reorder_levels = frappe._dict() - for ir in frappe.db.sql("""select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1): + for ir in frappe.db.sql( + """select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1 + ): if ir.parent not in reorder_levels: reorder_levels[ir.parent] = [] diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py index a7b48356b8d..70f04da4753 100644 --- a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -12,12 +12,14 @@ def execute(filters=None): data = get_data(filters.warehouse) return columns, data + def validate_warehouse(filters): company = filters.company warehouse = filters.warehouse if not frappe.db.exists("Warehouse", {"name": warehouse, "company": company}): frappe.throw(_("Warehouse: {0} does not belong to {1}").format(warehouse, company)) + def get_columns(): columns = [ { @@ -25,49 +27,37 @@ def get_columns(): "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 200 - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Serial No Count"), - "fieldname": "total", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Stock Qty"), - "fieldname": "stock_qty", - "fieldtype": "Float", - "width": 150 - }, - { - "label": _("Difference"), - "fieldname": "difference", - "fieldtype": "Float", - "width": 150 + "width": 200, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200}, + {"label": _("Serial No Count"), "fieldname": "total", "fieldtype": "Float", "width": 150}, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 150}, + {"label": _("Difference"), "fieldname": "difference", "fieldtype": "Float", "width": 150}, ] return columns -def get_data(warehouse): - serial_item_list = frappe.get_all("Item", filters={ - 'has_serial_no': True, - }, fields=['item_code', 'item_name']) - status_list = ['Active', 'Expired'] +def get_data(warehouse): + serial_item_list = frappe.get_all( + "Item", + filters={ + "has_serial_no": True, + }, + fields=["item_code", "item_name"], + ) + + status_list = ["Active", "Expired"] data = [] for item in serial_item_list: - total_serial_no = frappe.db.count("Serial No", - filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) + total_serial_no = frappe.db.count( + "Serial No", + filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}, + ) - actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], - filters={"warehouse": warehouse, "item_code": item.item_code}) + actual_qty = frappe.db.get_value( + "Bin", fieldname=["actual_qty"], filters={"warehouse": warehouse, "item_code": item.item_code} + ) # frappe.db.get_value returns null if no record exist. if not actual_qty: diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index 11559aa2081..0a482fe90a9 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -21,11 +21,11 @@ def execute(filters=None): if consumed_details.get(item_code): for cd in consumed_details.get(item_code): - if (cd.voucher_no not in material_transfer_vouchers): + if cd.voucher_no not in material_transfer_vouchers: if cd.voucher_type in ["Delivery Note", "Sales Invoice"]: delivered_qty += abs(flt(cd.actual_qty)) delivered_amount += abs(flt(cd.stock_value_difference)) - elif cd.voucher_type!="Delivery Note": + elif cd.voucher_type != "Delivery Note": consumed_qty += abs(flt(cd.actual_qty)) consumed_amount += abs(flt(cd.stock_value_difference)) @@ -33,66 +33,98 @@ def execute(filters=None): total_qty += delivered_qty + consumed_qty total_amount += delivered_amount + consumed_amount - row = [cd.item_code, cd.item_name, cd.description, cd.stock_uom, \ - consumed_qty, consumed_amount, delivered_qty, delivered_amount, \ - total_qty, total_amount, ','.join(list(set(suppliers)))] + row = [ + cd.item_code, + cd.item_name, + cd.description, + cd.stock_uom, + consumed_qty, + consumed_amount, + delivered_qty, + delivered_amount, + total_qty, + total_amount, + ",".join(list(set(suppliers))), + ] data.append(row) return columns, data + def get_columns(filters): """return columns based on filters""" - columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::100"] + \ - [_("Description") + "::150"] + [_("UOM") + ":Link/UOM:90"] + \ - [_("Consumed Qty") + ":Float:110"] + [_("Consumed Amount") + ":Currency:130"] + \ - [_("Delivered Qty") + ":Float:110"] + [_("Delivered Amount") + ":Currency:130"] + \ - [_("Total Qty") + ":Float:110"] + [_("Total Amount") + ":Currency:130"] + \ - [_("Supplier(s)") + "::250"] + columns = ( + [_("Item") + ":Link/Item:100"] + + [_("Item Name") + "::100"] + + [_("Description") + "::150"] + + [_("UOM") + ":Link/UOM:90"] + + [_("Consumed Qty") + ":Float:110"] + + [_("Consumed Amount") + ":Currency:130"] + + [_("Delivered Qty") + ":Float:110"] + + [_("Delivered Amount") + ":Currency:130"] + + [_("Total Qty") + ":Float:110"] + + [_("Total Amount") + ":Currency:130"] + + [_("Supplier(s)") + "::250"] + ) return columns + def get_conditions(filters): conditions = "" values = [] - if filters.get('from_date') and filters.get('to_date'): + if filters.get("from_date") and filters.get("to_date"): conditions = "and sle.posting_date>=%s and sle.posting_date<=%s" - values = [filters.get('from_date'), filters.get('to_date')] + values = [filters.get("from_date"), filters.get("to_date")] return conditions, values + def get_consumed_details(filters): conditions, values = get_conditions(filters) consumed_details = {} - for d in frappe.db.sql("""select sle.item_code, i.item_name, i.description, + for d in frappe.db.sql( + """select sle.item_code, i.item_name, i.description, i.stock_uom, sle.actual_qty, sle.stock_value_difference, sle.voucher_no, sle.voucher_type from `tabStock Ledger Entry` sle, `tabItem` i - where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): - consumed_details.setdefault(d.item_code, []).append(d) + where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" + % conditions, + values, + as_dict=1, + ): + consumed_details.setdefault(d.item_code, []).append(d) return consumed_details + def get_suppliers_details(filters): item_supplier_map = {} - supplier = filters.get('supplier') + supplier = filters.get("supplier") - for d in frappe.db.sql("""select pr.supplier, pri.item_code from + for d in frappe.db.sql( + """select pr.supplier, pri.item_code from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri where pr.name=pri.parent and pr.docstatus=1 and pri.item_code=(select name from `tabItem` where - is_stock_item=1 and name=pri.item_code)""", as_dict=1): - item_supplier_map.setdefault(d.item_code, []).append(d.supplier) + is_stock_item=1 and name=pri.item_code)""", + as_dict=1, + ): + item_supplier_map.setdefault(d.item_code, []).append(d.supplier) - for d in frappe.db.sql("""select pr.supplier, pri.item_code from + for d in frappe.db.sql( + """select pr.supplier, pri.item_code from `tabPurchase Invoice` pr, `tabPurchase Invoice Item` pri where pr.name=pri.parent and pr.docstatus=1 and ifnull(pr.update_stock, 0) = 1 and pri.item_code=(select name from `tabItem` - where is_stock_item=1 and name=pri.item_code)""", as_dict=1): - if d.item_code not in item_supplier_map: - item_supplier_map.setdefault(d.item_code, []).append(d.supplier) + where is_stock_item=1 and name=pri.item_code)""", + as_dict=1, + ): + if d.item_code not in item_supplier_map: + item_supplier_map.setdefault(d.item_code, []).append(d.supplier) if supplier: invalid_items = [] @@ -105,6 +137,9 @@ def get_suppliers_details(filters): return item_supplier_map + def get_material_transfer_vouchers(): - return frappe.db.sql_list("""select name from `tabStock Entry` where - purpose='Material Transfer' and docstatus=1""") + return frappe.db.sql_list( + """select name from `tabStock Entry` where + purpose='Material Transfer' and docstatus=1""" + ) diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 1dcf863a9d0..4be492c0fd4 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -37,16 +37,21 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ }, ), ("Warehouse wise Item Balance Age and Value", {"_optional": True}), - ("Item Variant Details", {"item": "_Test Variant Item",}), - ("Total Stock Summary", {"group_by": "warehouse",}), + ( + "Item Variant Details", + { + "item": "_Test Variant Item", + }, + ), + ( + "Total Stock Summary", + { + "group_by": "warehouse", + }, + ), ("Batch Item Expiry Status", {}), ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}), - ("Stock Ledger Invariant Check", - { - "warehouse": "_Test Warehouse - _TC", - "item": "_Test Item" - } - ), + ("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}), ] OPTIONAL_FILTERS = { diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py index 6f27558b887..21529da2a12 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py @@ -15,6 +15,7 @@ def execute(filters=None): return columns, stock + def get_columns(): columns = [ _("Company") + ":Link/Company:250", @@ -26,13 +27,16 @@ def get_columns(): return columns + def get_total_stock(filters): conditions = "" columns = "" if filters.get("group_by") == "Warehouse": if filters.get("company"): - conditions += " AND warehouse.company = %s" % frappe.db.escape(filters.get("company"), percent=False) + conditions += " AND warehouse.company = %s" % frappe.db.escape( + filters.get("company"), percent=False + ) conditions += " GROUP BY ledger.warehouse, item.item_code" columns += "'' as company, ledger.warehouse" @@ -40,7 +44,8 @@ def get_total_stock(filters): conditions += " GROUP BY warehouse.company, item.item_code" columns += " warehouse.company, '' as warehouse" - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT %s, item.item_code, @@ -53,4 +58,6 @@ def get_total_stock(filters): INNER JOIN `tabWarehouse` warehouse ON warehouse.name = ledger.warehouse WHERE - ledger.actual_qty != 0 %s""" % (columns, conditions)) + ledger.actual_qty != 0 %s""" + % (columns, conditions) + ) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index 294edb7378b..10d951b7bd4 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -22,7 +22,8 @@ from erpnext.stock.utils import is_reposting_item_valuation_in_progress def execute(filters=None): is_reposting_item_valuation_in_progress() - if not filters: filters = {} + if not filters: + filters = {} validate_filters(filters) @@ -40,7 +41,8 @@ def execute(filters=None): item_value = {} for (company, item, warehouse) in sorted(iwb_map): - if not item_map.get(item): continue + if not item_map.get(item): + continue row = [] qty_dict = iwb_map[(company, item, warehouse)] @@ -51,13 +53,13 @@ def execute(filters=None): total_stock_value += qty_dict.bal_val if wh.name == warehouse else 0.00 item_balance[(item, item_map[item]["item_group"])].append(row) - item_value.setdefault((item, item_map[item]["item_group"]),[]) + item_value.setdefault((item, item_map[item]["item_group"]), []) item_value[(item, item_map[item]["item_group"])].append(total_stock_value) - # sum bal_qty by item for (item, item_group), wh_balance in iteritems(item_balance): - if not item_ageing.get(item): continue + if not item_ageing.get(item): + continue total_stock_value = sum(item_value[(item, item_group)]) row = [item, item_group, total_stock_value] @@ -82,17 +84,19 @@ def execute(filters=None): add_warehouse_column(columns, warehouse_list) return columns, data + def get_columns(filters): """return columns""" columns = [ - _("Item")+":Link/Item:180", - _("Item Group")+"::100", - _("Value")+":Currency:100", - _("Age")+":Float:60", + _("Item") + ":Link/Item:180", + _("Item Group") + "::100", + _("Value") + ":Currency:100", + _("Age") + ":Float:60", ] return columns + def validate_filters(filters): if not (filters.get("item_code") or filters.get("warehouse")): sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) @@ -101,11 +105,12 @@ def validate_filters(filters): if not filters.get("company"): filters["company"] = frappe.defaults.get_user_default("Company") + def get_warehouse_list(filters): from frappe.core.doctype.user_permission.user_permission import get_permitted_documents - condition = '' - user_permitted_warehouse = get_permitted_documents('Warehouse') + condition = "" + user_permitted_warehouse = get_permitted_documents("Warehouse") value = () if user_permitted_warehouse: condition = "and name in %s" @@ -114,13 +119,20 @@ def get_warehouse_list(filters): condition = "and name = %s" value = filters.get("warehouse") - return frappe.db.sql("""select name + return frappe.db.sql( + """select name from `tabWarehouse` where is_group = 0 - {condition}""".format(condition=condition), value, as_dict=1) + {condition}""".format( + condition=condition + ), + value, + as_dict=1, + ) + def add_warehouse_column(columns, warehouse_list): if len(warehouse_list) > 1: - columns += [_("Total Qty")+":Int:50"] + columns += [_("Total Qty") + ":Int:50"] for wh in warehouse_list: - columns += [_(wh.name)+":Int:54"] + columns += [_(wh.name) + ":Int:54"] diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py index 35cad2ba305..9a7d8bbfe42 100644 --- a/erpnext/stock/stock_balance.py +++ b/erpnext/stock/stock_balance.py @@ -16,16 +16,20 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.auto_commit_on_many_writes = 1 if allow_negative_stock: - existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") + 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) - item_warehouses = frappe.db.sql(""" + item_warehouses = frappe.db.sql( + """ select distinct item_code, warehouse from (select item_code, warehouse from tabBin union select item_code, warehouse from `tabStock Ledger Entry`) a - """) + """ + ) for d in item_warehouses: try: repost_stock(d[0], d[1], allow_zero_rate, only_actual, only_bin, allow_negative_stock) @@ -34,11 +38,20 @@ def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, frappe.db.rollback() if allow_negative_stock: - frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) + frappe.db.set_value( + "Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock + ) frappe.db.auto_commit_on_many_writes = 0 -def repost_stock(item_code, warehouse, allow_zero_rate=False, - only_actual=False, only_bin=False, allow_negative_stock=False): + +def repost_stock( + item_code, + warehouse, + allow_zero_rate=False, + only_actual=False, + only_bin=False, + allow_negative_stock=False, +): if not only_bin: repost_actual_qty(item_code, warehouse, allow_zero_rate, allow_negative_stock) @@ -48,35 +61,42 @@ def repost_stock(item_code, warehouse, allow_zero_rate=False, "reserved_qty": get_reserved_qty(item_code, warehouse), "indented_qty": get_indented_qty(item_code, warehouse), "ordered_qty": get_ordered_qty(item_code, warehouse), - "planned_qty": get_planned_qty(item_code, warehouse) + "planned_qty": get_planned_qty(item_code, warehouse), } if only_bin: - qty_dict.update({ - "actual_qty": get_balance_qty_from_sle(item_code, warehouse) - }) + qty_dict.update({"actual_qty": get_balance_qty_from_sle(item_code, warehouse)}) update_bin_qty(item_code, warehouse, qty_dict) + def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False): - create_repost_item_valuation_entry({ - "item_code": item_code, - "warehouse": warehouse, - "posting_date": "1900-01-01", - "posting_time": "00:01", - "allow_negative_stock": allow_negative_stock, - "allow_zero_rate": allow_zero_rate - }) + create_repost_item_valuation_entry( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": "1900-01-01", + "posting_time": "00:01", + "allow_negative_stock": allow_negative_stock, + "allow_zero_rate": allow_zero_rate, + } + ) + def get_balance_qty_from_sle(item_code, warehouse): - balance_qty = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry` + balance_qty = frappe.db.sql( + """select qty_after_transaction from `tabStock Ledger Entry` where item_code=%s and warehouse=%s and is_cancelled=0 order by posting_date desc, posting_time desc, creation desc - limit 1""", (item_code, warehouse)) + limit 1""", + (item_code, warehouse), + ) return flt(balance_qty[0][0]) if balance_qty else 0.0 + def get_reserved_qty(item_code, warehouse): - reserved_qty = frappe.db.sql(""" + reserved_qty = frappe.db.sql( + """ select sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty)) from @@ -116,58 +136,76 @@ def get_reserved_qty(item_code, warehouse): ) tab where so_item_qty >= so_item_delivered_qty - """, (item_code, warehouse, item_code, warehouse)) + """, + (item_code, warehouse, item_code, warehouse), + ) return flt(reserved_qty[0][0]) if reserved_qty else 0 + def get_indented_qty(item_code, warehouse): # Ordered Qty is always maintained in stock UOM - inward_qty = frappe.db.sql(""" + inward_qty = frappe.db.sql( + """ select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer') and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 - """, (item_code, warehouse)) + """, + (item_code, warehouse), + ) inward_qty = flt(inward_qty[0][0]) if inward_qty else 0 - outward_qty = frappe.db.sql(""" + outward_qty = frappe.db.sql( + """ select sum(mr_item.stock_qty - mr_item.ordered_qty) from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr where mr_item.item_code=%s and mr_item.warehouse=%s and mr.material_request_type = 'Material Issue' and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr.status!='Stopped' and mr.docstatus=1 - """, (item_code, warehouse)) + """, + (item_code, warehouse), + ) outward_qty = flt(outward_qty[0][0]) if outward_qty else 0 requested_qty = inward_qty - outward_qty return requested_qty + def get_ordered_qty(item_code, warehouse): - ordered_qty = frappe.db.sql(""" + ordered_qty = frappe.db.sql( + """ select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor) from `tabPurchase Order Item` po_item, `tabPurchase Order` po where po_item.item_code=%s and po_item.warehouse=%s and po_item.qty > po_item.received_qty and po_item.parent=po.name and po.status not in ('Closed', 'Delivered') and po.docstatus=1 - and po_item.delivered_by_supplier = 0""", (item_code, warehouse)) + and po_item.delivered_by_supplier = 0""", + (item_code, warehouse), + ) return flt(ordered_qty[0][0]) if ordered_qty else 0 + def get_planned_qty(item_code, warehouse): - planned_qty = frappe.db.sql(""" + planned_qty = frappe.db.sql( + """ select sum(qty - produced_qty) from `tabWork Order` where production_item = %s and fg_warehouse = %s and status not in ("Stopped", "Completed", "Closed") - and docstatus=1 and qty > produced_qty""", (item_code, warehouse)) + and docstatus=1 and qty > produced_qty""", + (item_code, warehouse), + ) return flt(planned_qty[0][0]) if planned_qty else 0 def update_bin_qty(item_code, warehouse, qty_dict=None): from erpnext.stock.utils import get_bin + bin = get_bin(item_code, warehouse) mismatch = False for field, value in qty_dict.items(): @@ -181,41 +219,54 @@ def update_bin_qty(item_code, warehouse, qty_dict=None): bin.db_update() bin.clear_cache() -def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, posting_time=None, - fiscal_year=None): - if not posting_date: posting_date = nowdate() - if not posting_time: posting_time = nowtime() - condition = " and item.name='%s'" % item_code.replace("'", "\'") if item_code else "" +def set_stock_balance_as_per_serial_no( + item_code=None, posting_date=None, posting_time=None, fiscal_year=None +): + if not posting_date: + posting_date = nowdate() + if not posting_time: + posting_time = nowtime() - bin = frappe.db.sql("""select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom + condition = " and item.name='%s'" % item_code.replace("'", "'") if item_code else "" + + bin = frappe.db.sql( + """select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom from `tabBin` bin, tabItem item - where bin.item_code = item.name and item.has_serial_no = 1 %s""" % condition) + where bin.item_code = item.name and item.has_serial_no = 1 %s""" + % condition + ) for d in bin: - serial_nos = frappe.db.sql("""select count(name) from `tabSerial No` - where item_code=%s and warehouse=%s and docstatus < 2""", (d[0], d[1])) + serial_nos = frappe.db.sql( + """select count(name) from `tabSerial No` + where item_code=%s and warehouse=%s and docstatus < 2""", + (d[0], d[1]), + ) - sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry` + sle = frappe.db.sql( + """select valuation_rate, company from `tabStock Ledger Entry` where item_code = %s and warehouse = %s and is_cancelled = 0 - order by posting_date desc limit 1""", (d[0], d[1])) + order by posting_date desc limit 1""", + (d[0], d[1]), + ) sle_dict = { - 'doctype' : 'Stock Ledger Entry', - 'item_code' : d[0], - 'warehouse' : d[1], - 'transaction_date' : nowdate(), - 'posting_date' : posting_date, - 'posting_time' : posting_time, - 'voucher_type' : 'Stock Reconciliation (Manual)', - 'voucher_no' : '', - 'voucher_detail_no' : '', - 'actual_qty' : flt(serial_nos[0][0]) - flt(d[2]), - 'stock_uom' : d[3], - 'incoming_rate' : sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0, - 'company' : sle and cstr(sle[0][1]) or 0, - 'batch_no' : '', - 'serial_no' : '' + "doctype": "Stock Ledger Entry", + "item_code": d[0], + "warehouse": d[1], + "transaction_date": nowdate(), + "posting_date": posting_date, + "posting_time": posting_time, + "voucher_type": "Stock Reconciliation (Manual)", + "voucher_no": "", + "voucher_detail_no": "", + "actual_qty": flt(serial_nos[0][0]) - flt(d[2]), + "stock_uom": d[3], + "incoming_rate": sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0, + "company": sle and cstr(sle[0][1]) or 0, + "batch_no": "", + "serial_no": "", } sle_doc = frappe.get_doc(sle_dict) @@ -224,18 +275,19 @@ def set_stock_balance_as_per_serial_no(item_code=None, posting_date=None, postin sle_doc.insert() args = sle_dict.copy() - args.update({ - "sle_id": sle_doc.name - }) + args.update({"sle_id": sle_doc.name}) update_bin(args) - create_repost_item_valuation_entry({ - "item_code": d[0], - "warehouse": d[1], - "posting_date": posting_date, - "posting_time": posting_time - }) + create_repost_item_valuation_entry( + { + "item_code": d[0], + "warehouse": d[1], + "posting_date": posting_date, + "posting_time": posting_time, + } + ) + def reset_serial_no_status_and_warehouse(serial_nos=None): if not serial_nos: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4b9686cb2f6..0664a8352b9 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -19,28 +19,32 @@ from erpnext.stock.utils import ( ) -class NegativeStockError(frappe.ValidationError): pass +class NegativeStockError(frappe.ValidationError): + pass + + class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): - """ Create SL entries from SL entry dicts + """Create SL entries from SL entry dicts - args: - - allow_negative_stock: disable negative stock valiations if true - - via_landed_cost_voucher: landed cost voucher cancels and reposts - entries of purchase document. This flag is used to identify if - cancellation and repost is happening via landed cost voucher, in - such cases certain validations need to be ignored (like negative - stock) + args: + - allow_negative_stock: disable negative stock valiations if true + - via_landed_cost_voucher: landed cost voucher cancels and reposts + entries of purchase document. This flag is used to identify if + cancellation and repost is happening via landed cost voucher, in + such cases certain validations need to be ignored (like negative + stock) """ from erpnext.controllers.stock_controller import future_sle_exists + if sl_entries: cancel = sl_entries[0].get("is_cancelled") if cancel: validate_cancellation(sl_entries) - set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) + set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no")) args = get_args_for_future_sle(sl_entries[0]) future_sle_exists(args, sl_entries) @@ -50,19 +54,21 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc validate_serial_no(sle) if cancel: - sle['actual_qty'] = -flt(sle.get('actual_qty')) + sle["actual_qty"] = -flt(sle.get("actual_qty")) - if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): - sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, - sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) - sle['incoming_rate'] = 0.0 + if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"): + sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel( + sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no + ) + sle["incoming_rate"] = 0.0 - if sle['actual_qty'] > 0 and not sle.get('incoming_rate'): - sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, - sle.voucher_type, sle.voucher_no, sle.voucher_detail_no) - sle['outgoing_rate'] = 0.0 + if sle["actual_qty"] > 0 and not sle.get("incoming_rate"): + sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel( + sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no + ) + sle["outgoing_rate"] = 0.0 - if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation": + if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation": sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() @@ -71,13 +77,16 @@ 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") - is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') + 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")) repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher) update_bin_qty(bin_name, args) else: - frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) + 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": @@ -89,28 +98,35 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou # 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_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({ - 'voucher_type': row.get('voucher_type'), - 'voucher_no': row.get('voucher_no'), - 'posting_date': row.get('posting_date'), - 'posting_time': row.get('posting_time') - }) + return frappe._dict( + { + "voucher_type": row.get("voucher_type"), + "voucher_no": row.get("voucher_no"), + "posting_date": row.get("posting_date"), + "posting_time": row.get("posting_time"), + } + ) + def validate_serial_no(sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -118,58 +134,79 @@ def validate_serial_no(sle): for sn in get_serial_nos(sle.serial_no): args = copy.deepcopy(sle) args.serial_no = sn - args.warehouse = '' + args.warehouse = "" vouchers = [] - for row in get_stock_ledger_entries(args, '>'): + for row in get_stock_ledger_entries(args, ">"): voucher_type = frappe.bold(row.voucher_type) voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no)) - vouchers.append(f'{voucher_type} {voucher_no}') + vouchers.append(f"{voucher_type} {voucher_no}") if vouchers: serial_no = frappe.bold(sn) - msg = (f'''The serial no {serial_no} has been used in the future transactions so you need to cancel them first. - The list of the transactions are as below.''' + '

    • ') + msg = ( + f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first. + The list of the transactions are as below.""" + + "

      • " + ) - msg += '
      • '.join(vouchers) - msg += '
      ' + msg += "
    • ".join(vouchers) + msg += "
    " - title = 'Cannot Submit' if not sle.get('is_cancelled') else 'Cannot Cancel' + title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel" frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction) + def validate_cancellation(args): if args[0].get("is_cancelled"): - repost_entry = frappe.db.get_value("Repost Item Valuation", { - 'voucher_type': args[0].voucher_type, - 'voucher_no': args[0].voucher_no, - 'docstatus': 1 - }, ['name', 'status'], as_dict=1) + repost_entry = frappe.db.get_value( + "Repost Item Valuation", + {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1}, + ["name", "status"], + as_dict=1, + ) if repost_entry: - if repost_entry.status == 'In Progress': - frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.")) - if repost_entry.status == 'Queued': + if repost_entry.status == "In Progress": + frappe.throw( + _( + "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet." + ) + ) + if repost_entry.status == "Queued": doc = frappe.get_doc("Repost Item Valuation", repost_entry.name) doc.flags.ignore_permissions = True doc.cancel() doc.delete() + def set_as_cancel(voucher_type, voucher_no): - frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1, + frappe.db.sql( + """update `tabStock Ledger Entry` set is_cancelled=1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", - (now(), frappe.session.user, voucher_type, voucher_no)) + (now(), frappe.session.user, voucher_type, voucher_no), + ) + def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): args["doctype"] = "Stock Ledger Entry" sle = frappe.get_doc(args) sle.flags.ignore_permissions = 1 - sle.allow_negative_stock=allow_negative_stock + sle.allow_negative_stock = allow_negative_stock sle.via_landed_cost_voucher = via_landed_cost_voucher sle.submit() return sle -def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): + +def repost_future_sle( + args=None, + doc=None, + voucher_type=None, + voucher_no=None, + allow_negative_stock=None, + via_landed_cost_voucher=False, +): if not args and voucher_type and voucher_no: args = get_items_to_be_repost(voucher_type, voucher_no, doc) @@ -177,20 +214,28 @@ def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, a i = get_current_index(doc) or 0 while i < len(args): - obj = update_entries_after({ - "item_code": args[i].get('item_code'), - "warehouse": args[i].get('warehouse'), - "posting_date": args[i].get('posting_date'), - "posting_time": args[i].get('posting_time'), - "creation": args[i].get("creation"), - "distinct_item_warehouses": distinct_item_warehouses - }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) + obj = update_entries_after( + { + "item_code": args[i].get("item_code"), + "warehouse": args[i].get("warehouse"), + "posting_date": args[i].get("posting_date"), + "posting_time": args[i].get("posting_time"), + "creation": args[i].get("creation"), + "distinct_item_warehouses": distinct_item_warehouses, + }, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) - distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True + distinct_item_warehouses[ + (args[i].get("item_code"), args[i].get("warehouse")) + ].reposting_status = True if obj.new_items_found: for item_wh, data in iteritems(distinct_item_warehouses): - if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status): + if ("args_idx" not in data and not data.reposting_status) or ( + data.sle_changed and data.reposting_status + ): data.args_idx = len(args) args.append(data.sle) elif data.sle_changed and not data.reposting_status: @@ -205,77 +250,97 @@ def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, a if doc and args: update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): - frappe.db.set_value(doc.doctype, doc.name, { - 'items_to_be_repost': json.dumps(args, default=str), - 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str), - 'current_index': index - }) + frappe.db.set_value( + doc.doctype, + doc.name, + { + "items_to_be_repost": json.dumps(args, default=str), + "distinct_item_and_warehouse": json.dumps( + {str(k): v for k, v in distinct_item_warehouses.items()}, default=str + ), + "current_index": index, + }, + ) frappe.db.commit() - frappe.publish_realtime('item_reposting_progress', { - 'name': doc.name, - 'items_to_be_repost': json.dumps(args, default=str), - 'current_index': index - }) + frappe.publish_realtime( + "item_reposting_progress", + {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index}, + ) + def get_items_to_be_repost(voucher_type, voucher_no, doc=None): if doc and doc.items_to_be_repost: return json.loads(doc.items_to_be_repost) or [] - return frappe.db.get_all("Stock Ledger Entry", + return frappe.db.get_all( + "Stock Ledger Entry", filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], order_by="creation asc", - group_by="item_code, warehouse" + group_by="item_code, warehouse", ) + def get_distinct_item_warehouse(args=None, doc=None): distinct_item_warehouses = {} if doc and doc.distinct_item_and_warehouse: distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse) - distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()} + distinct_item_warehouses = { + frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items() + } else: for i, d in enumerate(args): - distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({ - "reposting_status": False, - "sle": d, - "args_idx": i - })) + distinct_item_warehouses.setdefault( + (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i}) + ) return distinct_item_warehouses + def get_current_index(doc=None): if doc and doc.current_index: return doc.current_index + class update_entries_after(object): """ - update valution rate and qty after transaction - from the current time-bucket onwards + update valution rate and qty after transaction + from the current time-bucket onwards - :param args: args as dict + :param args: args as dict - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00" + } """ - def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1): + + def __init__( + self, + args, + allow_zero_rate=False, + allow_negative_stock=None, + via_landed_cost_voucher=False, + verbose=1, + ): self.exceptions = {} self.verbose = verbose self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher - self.allow_negative_stock = allow_negative_stock \ - or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + self.allow_negative_stock = allow_negative_stock or cint( + frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + ) self.args = frappe._dict(args) self.item_code = args.get("item_code") if self.args.sle_id: - self.args['name'] = self.args.sle_id + self.args["name"] = self.args.sle_id self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.get_precision() @@ -289,28 +354,29 @@ class update_entries_after(object): self.build() def get_precision(self): - company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency") - self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), - currency=company_base_currency) + company_base_currency = frappe.get_cached_value("Company", self.company, "default_currency") + self.precision = get_field_precision( + frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), currency=company_base_currency + ) def initialize_previous_data(self, args): """ - Get previous sl entries for current item for each related warehouse - and assigns into self.data dict + Get previous sl entries for current item for each related warehouse + and assigns into self.data dict - :Data Structure: + :Data Structure: - self.data = { - warehouse1: { - 'previus_sle': {}, - 'qty_after_transaction': 10, - 'valuation_rate': 100, - 'stock_value': 1000, - 'prev_stock_value': 1000, - 'stock_queue': '[[10, 100]]', - 'stock_value_difference': 1000 - } - } + self.data = { + warehouse1: { + 'previus_sle': {}, + 'qty_after_transaction': 10, + 'valuation_rate': 100, + 'stock_value': 1000, + 'prev_stock_value': 1000, + 'stock_queue': '[[10, 100]]', + 'stock_value_difference': 1000 + } + } """ self.data.setdefault(args.warehouse, frappe._dict()) @@ -321,11 +387,13 @@ class update_entries_after(object): for key in ("qty_after_transaction", "valuation_rate", "stock_value"): setattr(warehouse_dict, key, flt(previous_sle.get(key))) - warehouse_dict.update({ - "prev_stock_value": previous_sle.stock_value or 0.0, - "stock_queue": json.loads(previous_sle.stock_queue or "[]"), - "stock_value_difference": 0.0 - }) + warehouse_dict.update( + { + "prev_stock_value": previous_sle.stock_value or 0.0, + "stock_queue": json.loads(previous_sle.stock_queue or "[]"), + "stock_value_difference": 0.0, + } + ) def build(self): from erpnext.controllers.stock_controller import future_sle_exists @@ -358,9 +426,10 @@ class update_entries_after(object): self.process_sle(sle) def get_sle_against_current_voucher(self): - self.args['time_format'] = '%H:%i:%s' + self.args["time_format"] = "%H:%i:%s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from @@ -374,22 +443,29 @@ class update_entries_after(object): order by creation ASC for update - """, self.args, as_dict=1) + """, + self.args, + as_dict=1, + ) def get_future_entries_to_fix(self): # includes current entry! - args = self.data[self.args.warehouse].previous_sle \ - or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse}) + args = self.data[self.args.warehouse].previous_sle or frappe._dict( + {"item_code": self.item_code, "warehouse": self.args.warehouse} + ) return list(self.get_sle_after_datetime(args)) def get_dependent_entries_to_fix(self, entries_to_fix, sle): - dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no, - excluded_sle=sle.name) + dependant_sle = get_sle_by_voucher_detail_no( + sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name + ) if not dependant_sle: return entries_to_fix - elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse: + elif ( + dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse + ): return entries_to_fix elif dependant_sle.item_code != self.item_code: self.update_distinct_item_warehouses(dependant_sle) @@ -401,14 +477,14 @@ class update_entries_after(object): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({ - "sle": dependant_sle - }) + val = frappe._dict({"sle": dependant_sle}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val self.new_items_found = True else: - existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + existing_sle_posting_date = ( + self.distinct_item_warehouses[key].get("sle", {}).get("posting_date") + ) if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True self.distinct_item_warehouses[key] = val @@ -417,12 +493,13 @@ class update_entries_after(object): def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) - args = self.data[dependant_sle.warehouse].previous_sle \ - or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse}) + args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict( + {"item_code": self.item_code, "warehouse": dependant_sle.warehouse} + ) future_sle_for_dependant = list(self.get_sle_after_datetime(args)) entries_to_fix.extend(future_sle_for_dependant) - return sorted(entries_to_fix, key=lambda k: k['timestamp']) + return sorted(entries_to_fix, key=lambda k: k["timestamp"]) def process_sle(self, sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -447,24 +524,32 @@ class update_entries_after(object): if sle.voucher_type == "Stock Reconciliation": self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) else: - if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) if self.valuation_method != "Moving Average": self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]] else: if self.valuation_method == "Moving Average": self.get_moving_average_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) - self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) + self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( + self.wh_data.valuation_rate + ) else: self.get_fifo_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) - self.wh_data.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)) + self.wh_data.stock_value = sum( + (flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue) + ) # rounding as per precision self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) @@ -479,7 +564,7 @@ class update_entries_after(object): sle.stock_value = self.wh_data.stock_value sle.stock_queue = json.dumps(self.wh_data.stock_queue) sle.stock_value_difference = stock_value_difference - sle.doctype="Stock Ledger Entry" + sle.doctype = "Stock Ledger Entry" frappe.get_doc(sle).db_update() if not self.args.get("sle_id"): @@ -487,8 +572,8 @@ class update_entries_after(object): def validate_negative_stock(self, sle): """ - validate negative stock for entries current datetime onwards - will not consider cancelled entries + validate negative stock for entries current datetime onwards + will not consider cancelled entries """ diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) @@ -517,13 +602,24 @@ class update_entries_after(object): self.recalculate_amounts_in_stock_entry(sle.voucher_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return - elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): + elif sle.voucher_type in ( + "Purchase Receipt", + "Purchase Invoice", + "Delivery Note", + "Sales Invoice", + ): if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"): from erpnext.controllers.sales_and_purchase_return import ( get_rate_for_return, # don't move this import to top ) - rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, - voucher_detail_no=sle.voucher_detail_no, sle = sle) + + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate" @@ -531,8 +627,9 @@ class update_entries_after(object): rate_field = "incoming_rate" # check in item table - item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item", - sle.voucher_detail_no, ["item_code", rate_field]) + item_code, incoming_rate = frappe.db.get_value( + sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field] + ) if item_code == sle.item_code: rate = incoming_rate @@ -542,15 +639,18 @@ class update_entries_after(object): else: ref_doctype = "Purchase Receipt Item Supplied" - rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no, - "item_code": sle.item_code}, rate_field) + rate = frappe.db.get_value( + ref_doctype, + {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, + rate_field, + ) return rate def update_outgoing_rate_on_transaction(self, sle): """ - Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return - In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount + Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return + In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount """ if sle.actual_qty and sle.voucher_detail_no: outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty) @@ -580,24 +680,33 @@ class update_entries_after(object): # Update item's incoming rate on transaction item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code") if item_code == sle.item_code: - frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate) + frappe.db.set_value( + sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate + ) else: # packed item - frappe.db.set_value("Packed Item", + frappe.db.set_value( + "Packed Item", {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code}, - "incoming_rate", outgoing_rate) + "incoming_rate", + outgoing_rate, + ) def update_rate_on_purchase_receipt(self, sle, outgoing_rate): if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): - frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate) + frappe.db.set_value( + sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate + ) else: - frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate) + frappe.db.set_value( + "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate + ) # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice - if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes': + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == "Yes": doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) - for d in (doc.items + doc.supplied_items): + for d in doc.items + doc.supplied_items: d.db_update() def get_serialized_values(self, sle): @@ -624,29 +733,34 @@ class update_entries_after(object): new_stock_qty = self.wh_data.qty_after_transaction + actual_qty if new_stock_qty > 0: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change + new_stock_value = ( + self.wh_data.qty_after_transaction * self.wh_data.valuation_rate + ) + stock_value_change if new_stock_value >= 0: # calculate new valuation rate only if stock value is positive # else it remains the same as that of previous entry self.wh_data.valuation_rate = new_stock_value / new_stock_qty if not self.wh_data.valuation_rate and sle.voucher_detail_no: - allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) + allow_zero_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) if not allow_zero_rate: self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company - all_serial_nos = frappe.get_all("Serial No", - fields=["purchase_rate", "name", "company"], - filters = {'name': ('in', serial_nos)}) + all_serial_nos = frappe.get_all( + "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)} + ) - incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company==sle.company) + incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company) # Get rate for serial nos which has been transferred to other company - invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company] + invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company] for serial_no in invalid_serial_nos: - incoming_rate = frappe.db.sql(""" + incoming_rate = frappe.db.sql( + """ select incoming_rate from `tabStock Ledger Entry` where @@ -660,7 +774,9 @@ class update_entries_after(object): ) order by posting_date desc limit 1 - """, (sle.company, serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%')) + """, + (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"), + ) incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0 @@ -674,15 +790,17 @@ class update_entries_after(object): if flt(self.wh_data.qty_after_transaction) <= 0: self.wh_data.valuation_rate = sle.incoming_rate else: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ - (actual_qty * sle.incoming_rate) + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + ( + actual_qty * sle.incoming_rate + ) self.wh_data.valuation_rate = new_stock_value / new_stock_qty elif sle.outgoing_rate: if new_stock_qty: - new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \ - (actual_qty * sle.outgoing_rate) + new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + ( + actual_qty * sle.outgoing_rate + ) self.wh_data.valuation_rate = new_stock_value / new_stock_qty else: @@ -697,7 +815,9 @@ class update_entries_after(object): # Get valuation rate from previous SLE or Item master, if item does not have the # allow zero valuration rate flag set if not self.wh_data.valuation_rate and sle.voucher_detail_no: - allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) + allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) if not allow_zero_valuation_rate: self.wh_data.valuation_rate = self.get_fallback_rate(sle) @@ -711,24 +831,26 @@ class update_entries_after(object): self.wh_data.stock_queue.append([0, 0]) # last row has the same rate, just updated the qty - if self.wh_data.stock_queue[-1][1]==incoming_rate: + if self.wh_data.stock_queue[-1][1] == incoming_rate: self.wh_data.stock_queue[-1][0] += actual_qty else: # Item has a positive balance qty, add new entry if self.wh_data.stock_queue[-1][0] > 0: self.wh_data.stock_queue.append([actual_qty, incoming_rate]) - else: # negative balance qty + else: # negative balance qty qty = self.wh_data.stock_queue[-1][0] + actual_qty - if qty > 0: # new balance qty is positive + if qty > 0: # new balance qty is positive self.wh_data.stock_queue[-1] = [qty, incoming_rate] - else: # new balance qty is still negative, maintain same rate + else: # new balance qty is still negative, maintain same rate self.wh_data.stock_queue[-1][0] = qty else: qty_to_pop = abs(actual_qty) while qty_to_pop: if not self.wh_data.stock_queue: # Get valuation rate from last sle if exists or from valuation rate field in item master - allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) + allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate( + sle.voucher_type, sle.voucher_detail_no + ) if not allow_zero_valuation_rate: _rate = self.get_fallback_rate(sle) else: @@ -746,9 +868,13 @@ class update_entries_after(object): # If no entry found with outgoing rate, collapse stack if index is None: # nosemgrep - new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate + new_stock_value = ( + sum((d[0] * d[1] for d in self.wh_data.stock_queue)) - qty_to_pop * outgoing_rate + ) new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop - self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]] + self.wh_data.stock_queue = [ + [new_stock_qty, new_stock_value / new_stock_qty if new_stock_qty > 0 else outgoing_rate] + ] break else: index = 0 @@ -771,14 +897,18 @@ class update_entries_after(object): batch[0] = batch[0] - qty_to_pop qty_to_pop = 0 - stock_value = _round_off_if_near_zero(sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))) + stock_value = _round_off_if_near_zero( + sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)) + ) stock_qty = _round_off_if_near_zero(sum((flt(batch[0]) for batch in self.wh_data.stock_queue))) if stock_qty: self.wh_data.valuation_rate = stock_value / flt(stock_qty) if not self.wh_data.stock_queue: - self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]) + self.wh_data.stock_queue.append( + [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate] + ) def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no): ref_item_dt = "" @@ -795,10 +925,16 @@ class update_entries_after(object): def get_fallback_rate(self, sle) -> float: """When exact incoming rate isn't available use any of other "average" rates as fallback. - This should only get used for negative stock.""" - return get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + This should only get used for negative stock.""" + return get_valuation_rate( + sle.item_code, + sle.warehouse, + sle.voucher_type, + sle.voucher_no, + self.allow_zero_rate, + currency=erpnext.get_company_currency(sle.company), + company=sle.company, + ) def get_sle_before_datetime(self, args): """get previous stock ledger entry before current time-bucket""" @@ -815,18 +951,27 @@ class update_entries_after(object): for warehouse, exceptions in iteritems(self.exceptions): deficiency = min(e["diff"] for e in exceptions) - if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in - frappe.local.flags.currently_saving): + if ( + exceptions[0]["voucher_type"], + exceptions[0]["voucher_no"], + ) in frappe.local.flags.currently_saving: msg = _("{0} units of {1} needed in {2} to complete this transaction.").format( - abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]), - frappe.get_desk_link('Warehouse', warehouse)) + abs(deficiency), + frappe.get_desk_link("Item", exceptions[0]["item_code"]), + frappe.get_desk_link("Warehouse", warehouse), + ) else: - msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( - abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]), - frappe.get_desk_link('Warehouse', warehouse), - exceptions[0]["posting_date"], exceptions[0]["posting_time"], - frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"])) + msg = _( + "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction." + ).format( + abs(deficiency), + frappe.get_desk_link("Item", exceptions[0]["item_code"]), + frappe.get_desk_link("Warehouse", warehouse), + exceptions[0]["posting_date"], + exceptions[0]["posting_time"], + frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]), + ) if msg: msg_list.append(msg) @@ -834,7 +979,7 @@ class update_entries_after(object): if msg_list: message = "\n\n".join(msg_list) if self.verbose: - frappe.throw(message, NegativeStockError, title=_('Insufficient Stock')) + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock")) else: raise NegativeStockError(message) @@ -843,19 +988,16 @@ class update_entries_after(object): for warehouse, data in self.data.items(): bin_name = get_or_make_bin(self.item_code, warehouse) - updated_values = { - "actual_qty": data.qty_after_transaction, - "stock_value": data.stock_value - } + updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} if data.valuation_rate is not None: updated_values["valuation_rate"] = data.valuation_rate - frappe.db.set_value('Bin', bin_name, updated_values) + frappe.db.set_value("Bin", bin_name, updated_values) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" - args['time_format'] = '%H:%i:%s' + args["time_format"] = "%H:%i:%s" if not args.get("posting_date"): args["posting_date"] = "1900-01-01" if not args.get("posting_time"): @@ -866,7 +1008,8 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): voucher_no = args.get("voucher_no") voucher_condition = f"and voucher_no != '{voucher_no}'" - sle = frappe.db.sql(""" + sle = frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %(item_code)s @@ -876,32 +1019,48 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) order by timestamp(posting_date, posting_time) desc, creation desc limit 1 - for update""".format(voucher_condition=voucher_condition), args, as_dict=1) + for update""".format( + voucher_condition=voucher_condition + ), + args, + as_dict=1, + ) return sle[0] if sle else frappe._dict() + def get_previous_sle(args, for_update=False): """ - get the last sle on or before the current time-bucket, - to get actual qty before transaction, this function - is called from various transaction like stock entry, reco etc + get the last sle on or before the current time-bucket, + to get actual qty before transaction, this function + is called from various transaction like stock entry, reco etc - args = { - "item_code": "ABC", - "warehouse": "XYZ", - "posting_date": "2012-12-12", - "posting_time": "12:00", - "sle": "name of reference Stock Ledger Entry" - } + args = { + "item_code": "ABC", + "warehouse": "XYZ", + "posting_date": "2012-12-12", + "posting_time": "12:00", + "sle": "name of reference Stock Ledger Entry" + } """ args["name"] = args.get("sle", None) or "" sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) return sle and sle[0] or {} -def get_stock_ledger_entries(previous_sle, operator=None, - order="desc", limit=None, for_update=False, debug=False, check_serial_no=True): + +def get_stock_ledger_entries( + previous_sle, + operator=None, + order="desc", + limit=None, + for_update=False, + debug=False, + check_serial_no=True, +): """get stock ledger entries filtered by specific posting datetime conditions""" - conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator) + conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( + operator + ) if previous_sle.get("warehouse"): conditions += " and warehouse = %(warehouse)s" elif previous_sle.get("warehouse_condition"): @@ -910,15 +1069,21 @@ def get_stock_ledger_entries(previous_sle, operator=None, if check_serial_no and previous_sle.get("serial_no"): # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no")))) serial_no = previous_sle.get("serial_no") - conditions += (""" and + conditions += ( + """ and ( serial_no = {0} or serial_no like {1} or serial_no like {2} or serial_no like {3} ) - """).format(frappe.db.escape(serial_no), frappe.db.escape('{}\n%'.format(serial_no)), - frappe.db.escape('%\n{}'.format(serial_no)), frappe.db.escape('%\n{}\n%'.format(serial_no))) + """ + ).format( + frappe.db.escape(serial_no), + frappe.db.escape("{}\n%".format(serial_no)), + frappe.db.escape("%\n{}".format(serial_no)), + frappe.db.escape("%\n{}\n%".format(serial_no)), + ) if not previous_sle.get("posting_date"): previous_sle["posting_date"] = "1900-01-01" @@ -928,34 +1093,59 @@ def get_stock_ledger_entries(previous_sle, operator=None, if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" - return frappe.db.sql(""" + return frappe.db.sql( + """ select *, timestamp(posting_date, posting_time) as "timestamp" from `tabStock Ledger Entry` where item_code = %%(item_code)s and is_cancelled = 0 %(conditions)s order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s - %(limit)s %(for_update)s""" % { + %(limit)s %(for_update)s""" + % { "conditions": conditions, "limit": limit or "", "for_update": for_update and "for update" or "", - "order": order - }, previous_sle, as_dict=1, debug=debug) + "order": order, + }, + previous_sle, + as_dict=1, + debug=debug, + ) + def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): - return frappe.db.get_value('Stock Ledger Entry', - {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]}, - ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'], - as_dict=1) + return frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]}, + [ + "item_code", + "warehouse", + "posting_date", + "posting_time", + "timestamp(posting_date, posting_time) as timestamp", + ], + as_dict=1, + ) -def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, - allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True): + +def get_valuation_rate( + item_code, + warehouse, + voucher_type, + voucher_no, + allow_zero_rate=False, + currency=None, + company=None, + raise_error_if_no_rate=True, +): if not company: - company = frappe.get_cached_value("Warehouse", warehouse, "company") + company = frappe.get_cached_value("Warehouse", warehouse, "company") # Get valuation rate from last sle for the same item and warehouse - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` force index (item_warehouse) where item_code = %s @@ -963,18 +1153,23 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, AND valuation_rate >= 0 AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) + order by posting_date desc, posting_time desc, name desc limit 1""", + (item_code, warehouse, voucher_no, voucher_type), + ) if not last_valuation_rate: # Get valuation rate from last sle for the item against any warehouse - last_valuation_rate = frappe.db.sql("""select valuation_rate + last_valuation_rate = frappe.db.sql( + """select valuation_rate from `tabStock Ledger Entry` force index (item_code) where item_code = %s AND valuation_rate > 0 AND is_cancelled = 0 AND NOT(voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type)) + order by posting_date desc, posting_time desc, name desc limit 1""", + (item_code, voucher_no, voucher_type), + ) if last_valuation_rate: return flt(last_valuation_rate[0][0]) @@ -989,19 +1184,37 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, if not valuation_rate: # try in price list - valuation_rate = frappe.db.get_value('Item Price', - dict(item_code=item_code, buying=1, currency=currency), - 'price_list_rate') + valuation_rate = frappe.db.get_value( + "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate" + ) - if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ - and cint(erpnext.is_perpetual_inventory_enabled(company)): + if ( + not allow_zero_rate + and not valuation_rate + and raise_error_if_no_rate + and cint(erpnext.is_perpetual_inventory_enabled(company)) + ): frappe.local.message_log = [] form_link = get_link_to_form("Item", item_code) - message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no) + message = _( + "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}." + ).format(form_link, voucher_type, voucher_no) message += "

    " + _("Here are the options to proceed:") - solutions = "
  • " + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + "
  • " - solutions += "
  • " + _("If not, you can Cancel / Submit this entry") + " {0} ".format(frappe.bold("after")) + _("performing either one below:") + "
  • " + solutions = ( + "
  • " + + _( + "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table." + ).format(voucher_type) + + "
  • " + ) + solutions += ( + "
  • " + + _("If not, you can Cancel / Submit this entry") + + " {0} ".format(frappe.bold("after")) + + _("performing either one below:") + + "
  • " + ) sub_solutions = "
    • " + _("Create an incoming stock transaction for the Item.") + "
    • " sub_solutions += "
    • " + _("Mention Valuation Rate in the Item master.") + "
    " msg = message + solutions + sub_solutions + "" @@ -1010,6 +1223,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, return valuation_rate + def update_qty_in_future_sle(args, allow_negative_stock=False): """Recalculate Qty after Transaction in future SLEs based on current SLE.""" datetime_limit_condition = "" @@ -1026,7 +1240,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) - frappe.db.sql(""" + frappe.db.sql( + """ update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty_shift} where @@ -1041,10 +1256,15 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): ) ) {datetime_limit_condition} - """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args) + """.format( + qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition + ), + args, + ) validate_negative_qty_in_future_sle(args, allow_negative_stock) + def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): @@ -1056,8 +1276,9 @@ def get_stock_reco_qty_shift(args): stock_reco_qty_shift = flt(args.actual_qty) else: # reco is being submitted - last_balance = get_previous_sle_of_current_voucher(args, - exclude_current_voucher=True).get("qty_after_transaction") + last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get( + "qty_after_transaction" + ) if last_balance is not None: stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance) @@ -1066,10 +1287,12 @@ def get_stock_reco_qty_shift(args): return stock_reco_qty_shift + def get_next_stock_reco(args): """Returns next nearest stock reconciliaton's details.""" - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, posting_date, posting_time, creation, voucher_no from @@ -1087,7 +1310,11 @@ def get_next_stock_reco(args): ) ) limit 1 - """, args, as_dict=1) + """, + args, + as_dict=1, + ) + def get_datetime_limit_condition(detail): return f""" @@ -1099,9 +1326,11 @@ def get_datetime_limit_condition(detail): ) )""" + 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")) + allow_negative_stock = cint(allow_negative_stock) or cint( + frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + ) if allow_negative_stock: return @@ -1110,32 +1339,40 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): 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( + 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')) + 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( + 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.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")) def get_future_sle_with_negative_qty(args): - return frappe.db.sql(""" + return frappe.db.sql( + """ select qty_after_transaction, posting_date, posting_time, voucher_type, voucher_no @@ -1149,11 +1386,15 @@ def get_future_sle_with_negative_qty(args): and qty_after_transaction < 0 order by timestamp(posting_date, posting_time) asc limit 1 - """, args, as_dict=1) + """, + args, + as_dict=1, + ) def get_future_sle_with_negative_batch_qty(args): - batch_ledger = frappe.db.sql(""" + batch_ledger = frappe.db.sql( + """ select posting_date, posting_time, voucher_type, voucher_no, actual_qty from `tabStock Ledger Entry` @@ -1163,7 +1404,10 @@ def get_future_sle_with_negative_batch_qty(args): and batch_no=%(batch_no)s and is_cancelled = 0 order by timestamp(posting_date, posting_time), creation - """, args, as_dict=1) + """, + args, + as_dict=1, + ) cumulative_total = 0.0 current_posting_datetime = get_datetime(str(args.posting_date) + " " + str(args.posting_time)) @@ -1172,16 +1416,18 @@ def get_future_sle_with_negative_batch_qty(args): if cumulative_total > -1e-6: continue - if (get_datetime(str(entry.posting_date) + " " + str(entry.posting_time)) - >= current_posting_datetime): + if ( + get_datetime(str(entry.posting_date) + " " + str(entry.posting_time)) + >= current_posting_datetime + ): entry.cumulative_total = cumulative_total return [entry] 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. + """Rounds off the number to zero only if number is close to zero for decimal + specified in precision. Precision defaults to 6. """ if abs(0.0 - flt(number)) < (1.0 / (10**precision)): return 0.0 diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 77f6ec38082..98eaecf7907 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -12,8 +12,13 @@ from six import string_types import erpnext -class InvalidWarehouseCompany(frappe.ValidationError): pass -class PendingRepostingError(frappe.ValidationError): pass +class InvalidWarehouseCompany(frappe.ValidationError): + pass + + +class PendingRepostingError(frappe.ValidationError): + pass + def get_stock_value_from_bin(warehouse=None, item_code=None): values = {} @@ -26,22 +31,27 @@ def get_stock_value_from_bin(warehouse=None, item_code=None): and w2.lft between w1.lft and w1.rgt ) """ - values['warehouse'] = warehouse + values["warehouse"] = warehouse if item_code: conditions += " and `tabBin`.item_code = %(item_code)s" - values['item_code'] = item_code + values["item_code"] = item_code - query = """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1 - and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s""" % conditions + query = ( + """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1 + and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s""" + % conditions + ) stock_value = frappe.db.sql(query, values) return stock_value + def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): - if not posting_date: posting_date = nowdate() + if not posting_date: + posting_date = nowdate() values, condition = [posting_date], "" @@ -63,13 +73,19 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): values.append(item_code) condition += " AND item_code = %s" - stock_ledger_entries = frappe.db.sql(""" + stock_ledger_entries = frappe.db.sql( + """ SELECT item_code, stock_value, name, warehouse FROM `tabStock Ledger Entry` sle WHERE posting_date <= %s {0} and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC - """.format(condition), values, as_dict=1) + """.format( + condition + ), + values, + as_dict=1, + ) sle_map = {} for sle in stock_ledger_entries: @@ -78,23 +94,32 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): return sum(sle_map.values()) + @frappe.whitelist() -def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None, - with_valuation_rate=False, with_serial_no=False): +def get_stock_balance( + item_code, + warehouse, + posting_date=None, + posting_time=None, + with_valuation_rate=False, + with_serial_no=False, +): """Returns stock balance quantity at given warehouse on given posting date or current date. If `with_valuation_rate` is True, will return tuple (qty, rate)""" from erpnext.stock.stock_ledger import get_previous_sle - if posting_date is None: posting_date = nowdate() - if posting_time is None: posting_time = nowtime() + if posting_date is None: + posting_date = nowdate() + if posting_time is None: + posting_time = nowtime() args = { "item_code": item_code, - "warehouse":warehouse, + "warehouse": warehouse, "posting_date": posting_date, - "posting_time": posting_time + "posting_time": posting_time, } last_entry = get_previous_sle(args) @@ -103,33 +128,41 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None if with_serial_no: serial_nos = get_serial_nos_data_after_transactions(args) - return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) - if last_entry else (0.0, 0.0, None)) + return ( + (last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) + if last_entry + else (0.0, 0.0, None) + ) else: - return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) + return ( + (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0) + ) else: return last_entry.qty_after_transaction if last_entry else 0.0 + def get_serial_nos_data_after_transactions(args): from pypika import CustomFunction serial_nos = set() args = frappe._dict(args) - sle = frappe.qb.DocType('Stock Ledger Entry') - Timestamp = CustomFunction('timestamp', ['date', 'time']) + sle = frappe.qb.DocType("Stock Ledger Entry") + Timestamp = CustomFunction("timestamp", ["date", "time"]) - stock_ledger_entries = frappe.qb.from_( - sle - ).select( - 'serial_no','actual_qty' - ).where( - (sle.item_code == args.item_code) - & (sle.warehouse == args.warehouse) - & (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time)) - & (sle.is_cancelled == 0) - ).orderby( - sle.posting_date, sle.posting_time, sle.creation - ).run(as_dict=1) + stock_ledger_entries = ( + frappe.qb.from_(sle) + .select("serial_no", "actual_qty") + .where( + (sle.item_code == args.item_code) + & (sle.warehouse == args.warehouse) + & ( + Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time) + ) + & (sle.is_cancelled == 0) + ) + .orderby(sle.posting_date, sle.posting_time, sle.creation) + .run(as_dict=1) + ) for stock_ledger_entry in stock_ledger_entries: changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no) @@ -138,12 +171,15 @@ def get_serial_nos_data_after_transactions(args): else: serial_nos.difference_update(changed_serial_no) - return '\n'.join(serial_nos) + return "\n".join(serial_nos) + def get_serial_nos_data(serial_nos): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + return get_serial_nos(serial_nos) + @frappe.whitelist() def get_latest_stock_qty(item_code, warehouse=None): values, condition = [item_code], "" @@ -160,37 +196,48 @@ def get_latest_stock_qty(item_code, warehouse=None): values.append(warehouse) condition += " AND warehouse = %s" - actual_qty = frappe.db.sql("""select sum(actual_qty) from tabBin - where item_code=%s {0}""".format(condition), values)[0][0] + actual_qty = frappe.db.sql( + """select sum(actual_qty) from tabBin + where item_code=%s {0}""".format( + condition + ), + values, + )[0][0] return actual_qty def get_latest_stock_balance(): bin_map = {} - for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value - FROM tabBin""", as_dict=1): - bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value)) + for d in frappe.db.sql( + """SELECT item_code, warehouse, stock_value as stock_value + FROM tabBin""", + as_dict=1, + ): + bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value)) return bin_map + def get_bin(item_code, warehouse): bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) if not bin: bin_obj = _create_bin(item_code, warehouse) else: - bin_obj = frappe.get_doc('Bin', bin, for_update=True) + bin_obj = frappe.get_doc("Bin", bin, for_update=True) bin_obj.flags.ignore_permissions = True return bin_obj -def get_or_make_bin(item_code: str , warehouse: str) -> str: - bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) + +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: bin_obj = _create_bin(item_code, warehouse) bin_record = bin_obj.name return bin_record + def _create_bin(item_code, warehouse): """Create a bin and take care of concurrent inserts.""" @@ -206,20 +253,24 @@ def _create_bin(item_code, warehouse): return bin_obj + 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') + + 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_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"))) + @frappe.whitelist() def get_incoming_rate(args, raise_error_if_no_rate=True): """Get Incoming Rate based on valuation method""" from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate + if isinstance(args, string_types): args = json.loads(args) @@ -229,37 +280,53 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args) - if valuation_method == 'FIFO': + if valuation_method == "FIFO": if previous_sle: - previous_stock_queue = json.loads(previous_sle.get('stock_queue', '[]') or '[]') - in_rate = get_fifo_rate(previous_stock_queue, args.get("qty") or 0) if previous_stock_queue else 0 - elif valuation_method == 'Moving Average': - in_rate = previous_sle.get('valuation_rate') or 0 + previous_stock_queue = json.loads(previous_sle.get("stock_queue", "[]") or "[]") + in_rate = ( + get_fifo_rate(previous_stock_queue, args.get("qty") or 0) if previous_stock_queue else 0 + ) + elif valuation_method == "Moving Average": + in_rate = previous_sle.get("valuation_rate") or 0 if not in_rate: - voucher_no = args.get('voucher_no') or args.get('name') - in_rate = get_valuation_rate(args.get('item_code'), args.get('warehouse'), - args.get('voucher_type'), voucher_no, args.get('allow_zero_valuation'), - currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'), - raise_error_if_no_rate=raise_error_if_no_rate) + voucher_no = args.get("voucher_no") or args.get("name") + in_rate = get_valuation_rate( + args.get("item_code"), + args.get("warehouse"), + args.get("voucher_type"), + voucher_no, + args.get("allow_zero_valuation"), + currency=erpnext.get_company_currency(args.get("company")), + company=args.get("company"), + raise_error_if_no_rate=raise_error_if_no_rate, + ) return flt(in_rate) + def get_avg_purchase_rate(serial_nos): """get average value of serial numbers""" serial_nos = get_valid_serial_nos(serial_nos) - return flt(frappe.db.sql("""select avg(purchase_rate) from `tabSerial No` - where name in (%s)""" % ", ".join(["%s"] * len(serial_nos)), - tuple(serial_nos))[0][0]) + return flt( + frappe.db.sql( + """select avg(purchase_rate) from `tabSerial No` + where name in (%s)""" + % ", ".join(["%s"] * len(serial_nos)), + tuple(serial_nos), + )[0][0] + ) + def get_valuation_method(item_code): """get valuation method from item or default""" - val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True) + val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True) if not val_method: val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO" return val_method + def get_fifo_rate(previous_stock_queue, qty): """get FIFO (average) Rate from Queue""" if flt(qty) >= 0: @@ -286,10 +353,11 @@ def get_fifo_rate(previous_stock_queue, qty): return outgoing_cost / available_qty_for_outgoing -def get_valid_serial_nos(sr_nos, qty=0, item_code=''): + +def get_valid_serial_nos(sr_nos, qty=0, item_code=""): """split serial nos, validate and return list of valid serial nos""" # TODO: remove duplicates in client side - serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n') + serial_nos = cstr(sr_nos).strip().replace(",", "\n").split("\n") valid_serial_nos = [] for val in serial_nos: @@ -305,19 +373,29 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''): return valid_serial_nos + def validate_warehouse_company(warehouse, company): warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True) if warehouse_company and warehouse_company != company: - frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company), - InvalidWarehouseCompany) + frappe.throw( + _("Warehouse {0} does not belong to company {1}").format(warehouse, company), + InvalidWarehouseCompany, + ) + def is_group_warehouse(warehouse): if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True): frappe.throw(_("Group node warehouse is not allowed to select for transactions")) + def validate_disabled_warehouse(warehouse): if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True): - frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse))) + frappe.throw( + _("Disabled Warehouse {0} cannot be used for this transaction.").format( + get_link_to_form("Warehouse", warehouse) + ) + ) + def update_included_uom_in_report(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: @@ -335,11 +413,14 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto convertible_columns.setdefault(key, d.get("convertible")) # Add new column to show qty/rate as per the selected UOM - columns.insert(idx+1, { - 'label': "{0} (per {1})".format(d.get("label"), include_uom), - 'fieldname': "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)), - 'fieldtype': 'Currency' if d.get("convertible") == 'rate' else 'Float' - }) + columns.insert( + idx + 1, + { + "label": "{0} (per {1})".format(d.get("label"), include_uom), + "fieldname": "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)), + "fieldtype": "Currency" if d.get("convertible") == "rate" else "Float", + }, + ) update_dict_values = [] for row_idx, row in enumerate(result): @@ -351,13 +432,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto if not conversion_factors[row_idx]: conversion_factors[row_idx] = 1 - if convertible_columns.get(key) == 'rate': + if convertible_columns.get(key) == "rate": new_value = flt(value) * conversion_factors[row_idx] else: new_value = flt(value) / conversion_factors[row_idx] if not is_dict_obj: - row.insert(key+1, new_value) + row.insert(key + 1, new_value) else: new_key = "{0}_{1}".format(key, frappe.scrub(include_uom)) update_dict_values.append([row, new_key, new_value]) @@ -366,11 +447,17 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto row, key, value = data row[key] = value + def get_available_serial_nos(args): - return frappe.db.sql(""" SELECT name from `tabSerial No` + return frappe.db.sql( + """ SELECT name from `tabSerial No` WHERE item_code = %(item_code)s and warehouse = %(warehouse)s and timestamp(purchase_date, purchase_time) <= timestamp(%(posting_date)s, %(posting_time)s) - """, args, as_dict=1) + """, + args, + as_dict=1, + ) + def add_additional_uom_columns(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: @@ -379,48 +466,55 @@ def add_additional_uom_columns(columns, result, include_uom, conversion_factors) convertible_column_map = {} for col_idx in list(reversed(range(0, len(columns)))): col = columns[col_idx] - if isinstance(col, dict) and col.get('convertible') in ['rate', 'qty']: + if isinstance(col, dict) and col.get("convertible") in ["rate", "qty"]: next_col = col_idx + 1 columns.insert(next_col, col.copy()) - columns[next_col]['fieldname'] += '_alt' - convertible_column_map[col.get('fieldname')] = frappe._dict({ - 'converted_col': columns[next_col]['fieldname'], - 'for_type': col.get('convertible') - }) - if col.get('convertible') == 'rate': - columns[next_col]['label'] += ' (per {})'.format(include_uom) + columns[next_col]["fieldname"] += "_alt" + convertible_column_map[col.get("fieldname")] = frappe._dict( + {"converted_col": columns[next_col]["fieldname"], "for_type": col.get("convertible")} + ) + if col.get("convertible") == "rate": + columns[next_col]["label"] += " (per {})".format(include_uom) else: - columns[next_col]['label'] += ' ({})'.format(include_uom) + columns[next_col]["label"] += " ({})".format(include_uom) for row_idx, row in enumerate(result): for convertible_col, data in convertible_column_map.items(): - conversion_factor = conversion_factors[row.get('item_code')] or 1 + conversion_factor = conversion_factors[row.get("item_code")] or 1 for_type = data.for_type value_before_conversion = row.get(convertible_col) - if for_type == 'rate': + if for_type == "rate": row[data.converted_col] = flt(value_before_conversion) * conversion_factor else: row[data.converted_col] = flt(value_before_conversion) / conversion_factor result[row_idx] = row + def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, voucher_detail_no): - outgoing_rate = frappe.db.sql("""SELECT abs(stock_value_difference / actual_qty) + outgoing_rate = frappe.db.sql( + """SELECT abs(stock_value_difference / actual_qty) FROM `tabStock Ledger Entry` WHERE voucher_type = %s and voucher_no = %s and item_code = %s and voucher_detail_no = %s ORDER BY CREATION DESC limit 1""", - (voucher_type, voucher_no, item_code, voucher_detail_no)) + (voucher_type, voucher_no, item_code, voucher_detail_no), + ) outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0 return outgoing_rate + def is_reposting_item_valuation_in_progress(): - reposting_in_progress = frappe.db.exists("Repost Item Valuation", - {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) + reposting_in_progress = frappe.db.exists( + "Repost Item Valuation", {"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) + 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.""" @@ -431,18 +525,21 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool "posting_date": ["<=", posting_date], } - reposting_pending = frappe.db.exists("Repost Item Valuation", filters) + 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, - } - ) + 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/support/__init__.py b/erpnext/support/__init__.py index b9a7c1e8cee..7b6845d2fd1 100644 --- a/erpnext/support/__init__.py +++ b/erpnext/support/__init__.py @@ -1,6 +1,5 @@ - install_docs = [ - {'doctype':'Role', 'role_name':'Support Team', 'name':'Support Team'}, - {'doctype':'Role', 'role_name':'Maintenance User', 'name':'Maintenance User'}, - {'doctype':'Role', 'role_name':'Maintenance Manager', 'name':'Maintenance Manager'} + {"doctype": "Role", "role_name": "Support Team", "name": "Support Team"}, + {"doctype": "Role", "role_name": "Maintenance User", "name": "Maintenance User"}, + {"doctype": "Role", "role_name": "Maintenance Manager", "name": "Maintenance Manager"}, ] diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 744b2989ff2..62e8d342f94 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -67,8 +67,9 @@ class Issue(Document): self.customer = contact.get_link_for("Customer") if not self.company: - self.company = frappe.db.get_value("Lead", self.lead, "company") or \ - frappe.db.get_default("Company") + self.company = frappe.db.get_value("Lead", self.lead, "company") or frappe.db.get_default( + "Company" + ) def reset_sla_fields(self): self.agreement_status = "" @@ -103,19 +104,20 @@ class Issue(Document): def handle_hold_time(self, status): if self.service_level_agreement: # set response and resolution variance as None as the issue is on Hold - pause_sla_on = frappe.db.get_all("Pause SLA On Status", fields=["status"], - filters={"parent": self.service_level_agreement}) + pause_sla_on = frappe.db.get_all( + "Pause SLA On Status", fields=["status"], filters={"parent": self.service_level_agreement} + ) hold_statuses = [entry.status for entry in pause_sla_on] update_values = {} if hold_statuses: if self.status in hold_statuses and status not in hold_statuses: - update_values['on_hold_since'] = frappe.flags.current_time or now_datetime() + update_values["on_hold_since"] = frappe.flags.current_time or now_datetime() if not self.first_responded_on: - update_values['response_by'] = None - update_values['response_by_variance'] = 0 - update_values['resolution_by'] = None - update_values['resolution_by_variance'] = 0 + update_values["response_by"] = None + update_values["response_by_variance"] = 0 + update_values["resolution_by"] = None + update_values["resolution_by_variance"] = 0 # calculate hold time when status is changed from any hold status to any non-hold status if self.status not in hold_statuses and status in hold_statuses: @@ -125,7 +127,7 @@ class Issue(Document): if self.on_hold_since: # last_hold_time will be added to the sla variables last_hold_time = time_diff_in_seconds(now_time, self.on_hold_since) - update_values['total_hold_time'] = hold_time + last_hold_time + update_values["total_hold_time"] = hold_time + last_hold_time # re-calculate SLA variables after issue changes from any hold status to any non-hold status # add hold time to SLA variables @@ -134,25 +136,31 @@ class Issue(Document): now_time = frappe.flags.current_time or now_datetime() if not self.first_responded_on: - response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) + response_by = get_expected_time_for( + parameter="response", service_level=priority, start_date_time=start_date_time + ) response_by = add_to_date(response_by, seconds=round(last_hold_time)) response_by_variance = round(time_diff_in_seconds(response_by, now_time)) - update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + last_hold_time + update_values["response_by"] = response_by + update_values["response_by_variance"] = response_by_variance + last_hold_time - resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + resolution_by = get_expected_time_for( + parameter="resolution", service_level=priority, start_date_time=start_date_time + ) resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) - update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time - update_values['on_hold_since'] = None + update_values["resolution_by"] = resolution_by + update_values["resolution_by_variance"] = resolution_by_variance + last_hold_time + update_values["on_hold_since"] = None self.db_set(update_values) def update_agreement_status(self): if self.service_level_agreement and self.agreement_status == "Ongoing": - if cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 or \ - cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0: + if ( + cint(frappe.db.get_value("Issue", self.name, "response_by_variance")) < 0 + or cint(frappe.db.get_value("Issue", self.name, "resolution_by_variance")) < 0 + ): self.agreement_status = "Failed" else: @@ -160,30 +168,34 @@ class Issue(Document): def update_agreement_status_on_custom_status(self): """ - Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status + Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status """ - if not self.first_responded_on: # first_responded_on set when first reply is sent to customer + if not self.first_responded_on: # first_responded_on set when first reply is sent to customer self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2) - if not self.resolution_date: # resolution_date set when issue has been closed + if not self.resolution_date: # resolution_date set when issue has been closed self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2) - self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" + self.agreement_status = ( + "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" + ) def create_communication(self): communication = frappe.new_doc("Communication") - communication.update({ - "communication_type": "Communication", - "communication_medium": "Email", - "sent_or_received": "Received", - "email_status": "Open", - "subject": self.subject, - "sender": self.raised_by, - "content": self.description, - "status": "Linked", - "reference_doctype": "Issue", - "reference_name": self.name - }) + communication.update( + { + "communication_type": "Communication", + "communication_medium": "Email", + "sent_or_received": "Received", + "email_status": "Open", + "subject": self.subject, + "sender": self.raised_by, + "content": self.description, + "status": "Linked", + "reference_doctype": "Issue", + "reference_name": self.name, + } + ) communication.ignore_permissions = True communication.ignore_mandatory = True communication.save() @@ -216,23 +228,31 @@ class Issue(Document): # Replicate linked Communications # TODO: get all communications in timeline before this, and modify them to append them to new doc comm_to_split_from = frappe.get_doc("Communication", communication_id) - communications = frappe.get_all("Communication", - filters={"reference_doctype": "Issue", + communications = frappe.get_all( + "Communication", + filters={ + "reference_doctype": "Issue", "reference_name": comm_to_split_from.reference_name, - "creation": (">=", comm_to_split_from.creation)}) + "creation": (">=", comm_to_split_from.creation), + }, + ) for communication in communications: doc = frappe.get_doc("Communication", communication.name) doc.reference_name = replicated_issue.name doc.save(ignore_permissions=True) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": "Issue", - "reference_name": replicated_issue.name, - "content": " - Split the Issue from {1}".format(self.name, frappe.bold(self.name)), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": "Issue", + "reference_name": replicated_issue.name, + "content": " - Split the Issue from {1}".format( + self.name, frappe.bold(self.name) + ), + } + ).insert(ignore_permissions=True) return replicated_issue.name @@ -243,7 +263,9 @@ class Issue(Document): def before_insert(self): if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): if frappe.flags.in_test: - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) else: self.set_response_and_resolution_time() @@ -252,11 +274,19 @@ class Issue(Document): if not service_level_agreement: if frappe.db.get_value("Issue", self.name, "service_level_agreement"): - frappe.throw(_("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement)) + frappe.throw( + _("Couldn't Set Service Level Agreement {0}.").format(self.service_level_agreement) + ) return - if (service_level_agreement.customer and self.customer) and not (service_level_agreement.customer == self.customer): - frappe.throw(_("This Service Level Agreement is specific to Customer {0}").format(service_level_agreement.customer)) + if (service_level_agreement.customer and self.customer) and not ( + service_level_agreement.customer == self.customer + ): + frappe.throw( + _("This Service Level Agreement is specific to Customer {0}").format( + service_level_agreement.customer + ) + ) self.service_level_agreement = service_level_agreement.name if not self.priority: @@ -269,40 +299,59 @@ class Issue(Document): self.service_level_agreement_creation = now_datetime() start_date_time = get_datetime(self.service_level_agreement_creation) - self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) - self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) + self.response_by = get_expected_time_for( + parameter="response", service_level=priority, start_date_time=start_date_time + ) + self.resolution_by = get_expected_time_for( + parameter="resolution", service_level=priority, start_date_time=start_date_time + ) self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime())) self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime())) def change_service_level_agreement_and_priority(self): - if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ - frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): + if ( + self.service_level_agreement + and frappe.db.exists("Issue", self.name) + and frappe.db.get_single_value("Support Settings", "track_service_level_agreement") + ): if not self.priority == frappe.db.get_value("Issue", self.name, "priority"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) frappe.msgprint(_("Priority has been changed to {0}.").format(self.priority)) - if not self.service_level_agreement == frappe.db.get_value("Issue", self.name, "service_level_agreement"): - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) + if not self.service_level_agreement == frappe.db.get_value( + "Issue", self.name, "service_level_agreement" + ): + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) + frappe.msgprint( + _("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement) + ) @frappe.whitelist() def reset_service_level_agreement(self, reason, user): if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) - frappe.get_doc({ - "doctype": "Comment", - "comment_type": "Info", - "reference_doctype": self.doctype, - "reference_name": self.name, - "comment_email": user, - "content": " resetted Service Level Agreement - {0}".format(_(reason)), - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Info", + "reference_doctype": self.doctype, + "reference_name": self.name, + "comment_email": user, + "content": " resetted Service Level Agreement - {0}".format(_(reason)), + } + ).insert(ignore_permissions=True) self.service_level_agreement_creation = now_datetime() - self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) + self.set_response_and_resolution_time( + priority=self.priority, service_level_agreement=self.service_level_agreement + ) self.agreement_status = "Ongoing" self.save() @@ -310,10 +359,12 @@ class Issue(Document): def get_priority(issue): service_level_agreement = frappe.get_doc("Service Level Agreement", issue.service_level_agreement) priority = service_level_agreement.get_service_level_agreement_priority(issue.priority) - priority.update({ - "support_and_resolution": service_level_agreement.support_and_resolution, - "holiday_list": service_level_agreement.holiday_list - }) + priority.update( + { + "support_and_resolution": service_level_agreement.support_and_resolution, + "holiday_list": service_level_agreement.holiday_list, + } + ) return priority @@ -334,10 +385,12 @@ def get_expected_time_for(parameter, service_level, start_date_time): support_days = {} for service in service_level.get("support_and_resolution"): - support_days[service.workday] = frappe._dict({ - "start_time": service.start_time, - "end_time": service.end_time, - }) + support_days[service.workday] = frappe._dict( + { + "start_time": service.start_time, + "end_time": service.end_time, + } + ) holidays = get_holidays(service_level.get("holiday_list")) weekdays = get_weekdays() @@ -346,14 +399,19 @@ def get_expected_time_for(parameter, service_level, start_date_time): current_weekday = weekdays[current_date_time.weekday()] if not is_holiday(current_date_time, holidays) and current_weekday in support_days: - start_time = current_date_time - datetime(current_date_time.year, current_date_time.month, current_date_time.day) \ - if getdate(current_date_time) == getdate(start_date_time) and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time \ + start_time = ( + current_date_time + - datetime(current_date_time.year, current_date_time.month, current_date_time.day) + if getdate(current_date_time) == getdate(start_date_time) + and get_time_in_timedelta(current_date_time.time()) > support_days[current_weekday].start_time else support_days[current_weekday].start_time + ) end_time = support_days[current_weekday].end_time time_left_today = time_diff_in_seconds(end_time, start_time) # no time left for support today - if time_left_today <= 0: pass + if time_left_today <= 0: + pass elif allotted_seconds: if time_left_today >= allotted_seconds: expected_time = datetime.combine(getdate(current_date_time), get_time(start_time)) @@ -372,6 +430,7 @@ def get_expected_time_for(parameter, service_level, start_date_time): return current_date_time + def set_service_level_agreement_variance(issue=None): current_time = frappe.flags.current_time or now_datetime() @@ -382,17 +441,25 @@ def set_service_level_agreement_variance(issue=None): for issue in frappe.get_list("Issue", filters=filters): doc = frappe.get_doc("Issue", issue.name) - if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer + if not doc.first_responded_on: # first_responded_on set when first reply is sent to customer variance = round(time_diff_in_seconds(doc.response_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False + ) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False + ) - if not doc.resolution_date: # resolution_date set when issue has been closed + if not doc.resolution_date: # resolution_date set when issue has been closed variance = round(time_diff_in_seconds(doc.resolution_by, current_time), 2) - frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False + ) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) + frappe.db.set_value( + dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False + ) def set_resolution_time(issue): @@ -403,18 +470,20 @@ def set_resolution_time(issue): def set_user_resolution_time(issue): # total time taken by a user to close the issue apart from wait_time - communications = frappe.get_list("Communication", filters={ - "reference_doctype": issue.doctype, - "reference_name": issue.name - }, + communications = frappe.get_list( + "Communication", + filters={"reference_doctype": issue.doctype, "reference_name": issue.name}, fields=["sent_or_received", "name", "creation"], - order_by="creation" + order_by="creation", ) pending_time = [] for i in range(len(communications)): - if communications[i].sent_or_received == "Received" and communications[i-1].sent_or_received == "Sent": - wait_time = time_diff_in_seconds(communications[i].creation, communications[i-1].creation) + if ( + communications[i].sent_or_received == "Received" + and communications[i - 1].sent_or_received == "Sent" + ): + wait_time = time_diff_in_seconds(communications[i].creation, communications[i - 1].creation) if wait_time > 0: pending_time.append(wait_time) @@ -431,7 +500,7 @@ def get_list_context(context=None): "row_template": "templates/includes/issue_row.html", "show_sidebar": True, "show_search": True, - "no_breadcrumbs": True + "no_breadcrumbs": True, } @@ -448,7 +517,8 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord ignore_permissions = False if is_website_user(): - if not filters: filters = {} + if not filters: + filters = {} if customer: filters["customer"] = customer @@ -457,7 +527,9 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord ignore_permissions = True - return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions) + return get_list( + doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=ignore_permissions + ) @frappe.whitelist() @@ -466,18 +538,26 @@ def set_multiple_status(names, status): for name in names: set_status(name, status) + @frappe.whitelist() def set_status(name, status): st = frappe.get_doc("Issue", name) st.status = status st.save() + def auto_close_tickets(): """Auto-close replied support tickets after 7 days""" - auto_close_after_days = frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 + auto_close_after_days = ( + frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 + ) - issues = frappe.db.sql(""" select name from tabIssue where status='Replied' and - modified resolution: - frappe.throw(_("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format(priority.priority, priority.idx)) + frappe.throw( + _("Response Time for {0} priority in row {1} can't be greater than Resolution Time.").format( + priority.priority, priority.idx + ) + ) # Check if repeated priority if not len(set(priorities)) == len(priorities): @@ -59,15 +66,27 @@ class ServiceLevelAgreement(Document): for support_and_resolution in self.support_and_resolution: # Check if start and end time is set for every support day if not (support_and_resolution.start_time or support_and_resolution.end_time): - frappe.throw(_("Set Start Time and End Time for \ - Support Day {0} at index {1}.".format(support_and_resolution.workday, support_and_resolution.idx))) + frappe.throw( + _( + "Set Start Time and End Time for \ + Support Day {0} at index {1}.".format( + support_and_resolution.workday, support_and_resolution.idx + ) + ) + ) support_days.append(support_and_resolution.workday) support_and_resolution.idx = week.index(support_and_resolution.workday) + 1 if support_and_resolution.start_time >= support_and_resolution.end_time: - frappe.throw(_("Start Time can't be greater than or equal to End Time \ - for {0}.".format(support_and_resolution.workday))) + frappe.throw( + _( + "Start Time can't be greater than or equal to End Time \ + for {0}.".format( + support_and_resolution.workday + ) + ) + ) # Check for repeated workday if not len(set(support_days)) == len(support_days): @@ -75,12 +94,21 @@ class ServiceLevelAgreement(Document): frappe.throw(_("Workday {0} has been repeated.").format(repeated_days)) def validate_doc(self): - if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement") and self.enable: - frappe.throw(_("{0} is not enabled in {1}").format(frappe.bold("Track Service Level Agreement"), - get_link_to_form("Support Settings", "Support Settings"))) + if ( + not frappe.db.get_single_value("Support Settings", "track_service_level_agreement") + and self.enable + ): + frappe.throw( + _("{0} is not enabled in {1}").format( + frappe.bold("Track Service Level Agreement"), + get_link_to_form("Support Settings", "Support Settings"), + ) + ) if self.default_service_level_agreement: - if frappe.db.exists("Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]}): + if frappe.db.exists( + "Service Level Agreement", {"default_service_level_agreement": "1", "name": ["!=", self.name]} + ): frappe.throw(_("A Default Service Level Agreement already exists.")) else: if self.start_date and self.end_date: @@ -91,11 +119,18 @@ class ServiceLevelAgreement(Document): frappe.throw(_("End Date of Agreement can't be less than today.")) if self.entity_type and self.entity: - if frappe.db.exists("Service Level Agreement", {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}): - frappe.throw(_("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format(self.entity_type, self.entity)) + if frappe.db.exists( + "Service Level Agreement", + {"entity_type": self.entity_type, "entity": self.entity, "name": ["!=", self.name]}, + ): + frappe.throw( + _("Service Level Agreement with Entity Type {0} and Entity {1} already exists.").format( + self.entity_type, self.entity + ) + ) def validate_condition(self): - temp_doc = frappe.new_doc('Issue') + temp_doc = frappe.new_doc("Issue") if self.condition: try: frappe.safe_eval(self.condition, None, get_context(temp_doc)) @@ -105,58 +140,77 @@ class ServiceLevelAgreement(Document): def get_service_level_agreement_priority(self, priority): priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name}) - return frappe._dict({ - "priority": priority.priority, - "response_time": priority.response_time, - "resolution_time": priority.resolution_time - }) + return frappe._dict( + { + "priority": priority.priority, + "response_time": priority.response_time, + "resolution_time": priority.resolution_time, + } + ) + def check_agreement_status(): - service_level_agreements = frappe.get_list("Service Level Agreement", filters=[ - {"active": 1}, - {"default_service_level_agreement": 0} - ], fields=["name"]) + service_level_agreements = frappe.get_list( + "Service Level Agreement", + filters=[{"active": 1}, {"default_service_level_agreement": 0}], + fields=["name"], + ) for service_level_agreement in service_level_agreements: doc = frappe.get_doc("Service Level Agreement", service_level_agreement.name) if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()): frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "active", 0) + def get_active_service_level_agreement_for(doc): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): return filters = [ ["Service Level Agreement", "active", "=", 1], - ["Service Level Agreement", "enable", "=", 1] + ["Service Level Agreement", "enable", "=", 1], ] - if doc.get('priority'): - filters.append(["Service Level Priority", "priority", "=", doc.get('priority')]) + if doc.get("priority"): + filters.append(["Service Level Priority", "priority", "=", doc.get("priority")]) - customer = doc.get('customer') + customer = doc.get("customer") or_filters = [ - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]] + [ + "Service Level Agreement", + "entity", + "in", + [customer, get_customer_group(customer), get_customer_territory(customer)], + ] ] - service_level_agreement = doc.get('service_level_agreement') + service_level_agreement = doc.get("service_level_agreement") if service_level_agreement: or_filters = [ - ["Service Level Agreement", "name", "=", doc.get('service_level_agreement')], + ["Service Level Agreement", "name", "=", doc.get("service_level_agreement")], ] - default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] - default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter, - fields=["name", "default_priority", "condition"]) + default_sla_filter = filters + [ + ["Service Level Agreement", "default_service_level_agreement", "=", 1] + ] + default_sla = frappe.get_all( + "Service Level Agreement", + filters=default_sla_filter, + fields=["name", "default_priority", "condition"], + ) filters += [["Service Level Agreement", "default_service_level_agreement", "=", 0]] - agreements = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters, - fields=["name", "default_priority", "condition"]) + agreements = frappe.get_all( + "Service Level Agreement", + filters=filters, + or_filters=or_filters, + fields=["name", "default_priority", "condition"], + ) # check if the current document on which SLA is to be applied fulfills all the conditions filtered_agreements = [] for agreement in agreements: - condition = agreement.get('condition') + condition = agreement.get("condition") if not condition or (condition and frappe.safe_eval(condition, None, get_context(doc))): filtered_agreements.append(agreement) @@ -165,17 +219,25 @@ def get_active_service_level_agreement_for(doc): return filtered_agreements[0] if filtered_agreements else None + def get_context(doc): - return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))} + return { + "doc": doc.as_dict(), + "nowdate": nowdate, + "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils")), + } + def get_customer_group(customer): if customer: return frappe.db.get_value("Customer", customer, "customer_group") + def get_customer_territory(customer): if customer: return frappe.db.get_value("Customer", customer, "territory") + @frappe.whitelist() def get_service_level_agreement_filters(name, customer=None): if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"): @@ -183,25 +245,37 @@ def get_service_level_agreement_filters(name, customer=None): filters = [ ["Service Level Agreement", "active", "=", 1], - ["Service Level Agreement", "enable", "=", 1] + ["Service Level Agreement", "enable", "=", 1], ] if not customer: - or_filters = [ - ["Service Level Agreement", "default_service_level_agreement", "=", 1] - ] + or_filters = [["Service Level Agreement", "default_service_level_agreement", "=", 1]] else: # Include SLA with No Entity and Entity Type or_filters = [ - ["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer), ""]], - ["Service Level Agreement", "default_service_level_agreement", "=", 1] + [ + "Service Level Agreement", + "entity", + "in", + [customer, get_customer_group(customer), get_customer_territory(customer), ""], + ], + ["Service Level Agreement", "default_service_level_agreement", "=", 1], ] return { - "priority": [priority.priority for priority in frappe.get_list("Service Level Priority", filters={"parent": name}, fields=["priority"])], - "service_level_agreements": [d.name for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters)] + "priority": [ + priority.priority + for priority in frappe.get_list( + "Service Level Priority", filters={"parent": name}, fields=["priority"] + ) + ], + "service_level_agreements": [ + d.name + for d in frappe.get_list("Service Level Agreement", filters=filters, or_filters=or_filters) + ], } + def get_repeated(values): unique_list = [] diff = [] diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py index 22e2c374e12..8fd3025c30a 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement_dashboard.py @@ -3,11 +3,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'service_level_agreement', - 'transactions': [ - { - 'label': _('Issue'), - 'items': ['Issue'] - } - ] + "fieldname": "service_level_agreement", + "transactions": [{"label": _("Issue"), "items": ["Issue"]}], } diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4c4a684333f..0137be08f8d 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -16,55 +16,131 @@ class TestServiceLevelAgreement(unittest.TestCase): def test_service_level_agreement(self): # Default Service Level Agreement - create_default_service_level_agreement = create_service_level_agreement(default_service_level_agreement=1, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type=None, entity=None, response_time=14400, resolution_time=21600) + create_default_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + ) - get_default_service_level_agreement = get_service_level_agreement(default_service_level_agreement=1) + get_default_service_level_agreement = get_service_level_agreement( + default_service_level_agreement=1 + ) - self.assertEqual(create_default_service_level_agreement.name, get_default_service_level_agreement.name) - self.assertEqual(create_default_service_level_agreement.entity_type, get_default_service_level_agreement.entity_type) - self.assertEqual(create_default_service_level_agreement.entity, get_default_service_level_agreement.entity) - self.assertEqual(create_default_service_level_agreement.default_service_level_agreement, get_default_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_default_service_level_agreement.name, get_default_service_level_agreement.name + ) + self.assertEqual( + create_default_service_level_agreement.entity_type, + get_default_service_level_agreement.entity_type, + ) + self.assertEqual( + create_default_service_level_agreement.entity, get_default_service_level_agreement.entity + ) + self.assertEqual( + create_default_service_level_agreement.default_service_level_agreement, + get_default_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Customer customer = create_customer() - create_customer_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer", entity=customer, response_time=7200, resolution_time=10800) - get_customer_service_level_agreement = get_service_level_agreement(entity_type="Customer", entity=customer) + create_customer_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer", + entity=customer, + response_time=7200, + resolution_time=10800, + ) + get_customer_service_level_agreement = get_service_level_agreement( + entity_type="Customer", entity=customer + ) - self.assertEqual(create_customer_service_level_agreement.name, get_customer_service_level_agreement.name) - self.assertEqual(create_customer_service_level_agreement.entity_type, get_customer_service_level_agreement.entity_type) - self.assertEqual(create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity) - self.assertEqual(create_customer_service_level_agreement.default_service_level_agreement, get_customer_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_customer_service_level_agreement.name, get_customer_service_level_agreement.name + ) + self.assertEqual( + create_customer_service_level_agreement.entity_type, + get_customer_service_level_agreement.entity_type, + ) + self.assertEqual( + create_customer_service_level_agreement.entity, get_customer_service_level_agreement.entity + ) + self.assertEqual( + create_customer_service_level_agreement.default_service_level_agreement, + get_customer_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Customer Group customer_group = create_customer_group() - create_customer_group_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Customer Group", entity=customer_group, response_time=7200, resolution_time=10800) - get_customer_group_service_level_agreement = get_service_level_agreement(entity_type="Customer Group", entity=customer_group) + create_customer_group_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer Group", + entity=customer_group, + response_time=7200, + resolution_time=10800, + ) + get_customer_group_service_level_agreement = get_service_level_agreement( + entity_type="Customer Group", entity=customer_group + ) - self.assertEqual(create_customer_group_service_level_agreement.name, get_customer_group_service_level_agreement.name) - self.assertEqual(create_customer_group_service_level_agreement.entity_type, get_customer_group_service_level_agreement.entity_type) - self.assertEqual(create_customer_group_service_level_agreement.entity, get_customer_group_service_level_agreement.entity) - self.assertEqual(create_customer_group_service_level_agreement.default_service_level_agreement, get_customer_group_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_customer_group_service_level_agreement.name, + get_customer_group_service_level_agreement.name, + ) + self.assertEqual( + create_customer_group_service_level_agreement.entity_type, + get_customer_group_service_level_agreement.entity_type, + ) + self.assertEqual( + create_customer_group_service_level_agreement.entity, + get_customer_group_service_level_agreement.entity, + ) + self.assertEqual( + create_customer_group_service_level_agreement.default_service_level_agreement, + get_customer_group_service_level_agreement.default_service_level_agreement, + ) # Service Level Agreement for Territory territory = create_territory() - create_territory_service_level_agreement = create_service_level_agreement(default_service_level_agreement=0, - holiday_list="__Test Holiday List", employee_group="_Test Employee Group", - entity_type="Territory", entity=territory, response_time=7200, resolution_time=10800) - get_territory_service_level_agreement = get_service_level_agreement(entity_type="Territory", entity=territory) + create_territory_service_level_agreement = create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Territory", + entity=territory, + response_time=7200, + resolution_time=10800, + ) + get_territory_service_level_agreement = get_service_level_agreement( + entity_type="Territory", entity=territory + ) - self.assertEqual(create_territory_service_level_agreement.name, get_territory_service_level_agreement.name) - self.assertEqual(create_territory_service_level_agreement.entity_type, get_territory_service_level_agreement.entity_type) - self.assertEqual(create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity) - self.assertEqual(create_territory_service_level_agreement.default_service_level_agreement, get_territory_service_level_agreement.default_service_level_agreement) + self.assertEqual( + create_territory_service_level_agreement.name, get_territory_service_level_agreement.name + ) + self.assertEqual( + create_territory_service_level_agreement.entity_type, + get_territory_service_level_agreement.entity_type, + ) + self.assertEqual( + create_territory_service_level_agreement.entity, get_territory_service_level_agreement.entity + ) + self.assertEqual( + create_territory_service_level_agreement.default_service_level_agreement, + get_territory_service_level_agreement.default_service_level_agreement, + ) -def get_service_level_agreement(default_service_level_agreement=None, entity_type=None, entity=None): +def get_service_level_agreement( + default_service_level_agreement=None, entity_type=None, entity=None +): if default_service_level_agreement: filters = {"default_service_level_agreement": default_service_level_agreement} else: @@ -73,93 +149,96 @@ def get_service_level_agreement(default_service_level_agreement=None, entity_typ service_level_agreement = frappe.get_doc("Service Level Agreement", filters) return service_level_agreement -def create_service_level_agreement(default_service_level_agreement, holiday_list, employee_group, - response_time, entity_type, entity, resolution_time): + +def create_service_level_agreement( + default_service_level_agreement, + holiday_list, + employee_group, + response_time, + entity_type, + entity, + resolution_time, +): employee_group = make_employee_group() make_holiday_list() make_priorities() - service_level_agreement = frappe.get_doc({ - "doctype": "Service Level Agreement", - "enable": 1, - "service_level": "__Test Service Level", - "default_service_level_agreement": default_service_level_agreement, - "default_priority": "Medium", - "holiday_list": holiday_list, - "employee_group": employee_group, - "entity_type": entity_type, - "entity": entity, - "start_date": frappe.utils.getdate(), - "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), - "priorities": [ - { - "priority": "Low", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "Medium", - "response_time": response_time, - "default_priority": 1, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - }, - { - "priority": "High", - "response_time": response_time, - "response_time_period": "Hour", - "resolution_time": resolution_time, - "resolution_time_period": "Hour", - } - ], - "pause_sla_on": [ - { - "status": "Replied" - } - ], - "support_and_resolution": [ - { - "workday": "Monday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Tuesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Wednesday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Thursday", - "start_time": "10:00:00", - "end_time": "18:00:00", - }, - { - "workday": "Friday", - "start_time": "10:00:00", - "end_time": "18:00:00", - } - ] - }) + service_level_agreement = frappe.get_doc( + { + "doctype": "Service Level Agreement", + "enable": 1, + "service_level": "__Test Service Level", + "default_service_level_agreement": default_service_level_agreement, + "default_priority": "Medium", + "holiday_list": holiday_list, + "employee_group": employee_group, + "entity_type": entity_type, + "entity": entity, + "start_date": frappe.utils.getdate(), + "end_date": frappe.utils.add_to_date(frappe.utils.getdate(), days=100), + "priorities": [ + { + "priority": "Low", + "response_time": response_time, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", + }, + { + "priority": "Medium", + "response_time": response_time, + "default_priority": 1, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", + }, + { + "priority": "High", + "response_time": response_time, + "response_time_period": "Hour", + "resolution_time": resolution_time, + "resolution_time_period": "Hour", + }, + ], + "pause_sla_on": [{"status": "Replied"}], + "support_and_resolution": [ + { + "workday": "Monday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Tuesday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Wednesday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Thursday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + { + "workday": "Friday", + "start_time": "10:00:00", + "end_time": "18:00:00", + }, + ], + } + ) filters = { "default_service_level_agreement": service_level_agreement.default_service_level_agreement, - "service_level": service_level_agreement.service_level + "service_level": service_level_agreement.service_level, } if not default_service_level_agreement: - filters.update({ - "entity_type": entity_type, - "entity": entity - }) + filters.update({"entity_type": entity_type, "entity": entity}) service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters) @@ -171,24 +250,26 @@ def create_service_level_agreement(default_service_level_agreement, holiday_list def create_customer(): - customer = frappe.get_doc({ - "doctype": "Customer", - "customer_name": "_Test Customer", - "customer_group": "Commercial", - "customer_type": "Individual", - "territory": "Rest Of The World" - }) + customer = frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test Customer", + "customer_group": "Commercial", + "customer_type": "Individual", + "territory": "Rest Of The World", + } + ) if not frappe.db.exists("Customer", "_Test Customer"): customer.insert(ignore_permissions=True) return customer.name else: return frappe.db.exists("Customer", "_Test Customer") + def create_customer_group(): - customer_group = frappe.get_doc({ - "doctype": "Customer Group", - "customer_group_name": "_Test SLA Customer Group" - }) + customer_group = frappe.get_doc( + {"doctype": "Customer Group", "customer_group_name": "_Test SLA Customer Group"} + ) if not frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}): customer_group.insert() @@ -196,11 +277,14 @@ def create_customer_group(): else: return frappe.db.exists("Customer Group", {"customer_group_name": "_Test SLA Customer Group"}) + def create_territory(): - territory = frappe.get_doc({ - "doctype": "Territory", - "territory_name": "_Test SLA Territory", - }) + territory = frappe.get_doc( + { + "doctype": "Territory", + "territory_name": "_Test SLA Territory", + } + ) if not frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}): territory.insert() @@ -208,42 +292,65 @@ def create_territory(): else: return frappe.db.exists("Territory", {"territory_name": "_Test SLA Territory"}) + def create_service_level_agreements_for_issues(): - create_service_level_agreement(default_service_level_agreement=1, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type=None, entity=None, response_time=14400, resolution_time=21600) + create_service_level_agreement( + default_service_level_agreement=1, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type=None, + entity=None, + response_time=14400, + resolution_time=21600, + ) create_customer() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Customer", entity="_Test Customer", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer", + entity="_Test Customer", + response_time=7200, + resolution_time=10800, + ) create_customer_group() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Customer Group", entity="_Test SLA Customer Group", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Customer Group", + entity="_Test SLA Customer Group", + response_time=7200, + resolution_time=10800, + ) create_territory() - create_service_level_agreement(default_service_level_agreement=0, holiday_list="__Test Holiday List", - employee_group="_Test Employee Group", entity_type="Territory", entity="_Test SLA Territory", response_time=7200, resolution_time=10800) + create_service_level_agreement( + default_service_level_agreement=0, + holiday_list="__Test Holiday List", + employee_group="_Test Employee Group", + entity_type="Territory", + entity="_Test SLA Territory", + response_time=7200, + resolution_time=10800, + ) + def make_holiday_list(): holiday_list = frappe.db.exists("Holiday List", "__Test Holiday List") if not holiday_list: - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "__Test Holiday List", - "from_date": "2019-01-01", - "to_date": "2019-12-31", - "holidays": [ - { - "description": "Test Holiday 1", - "holiday_date": "2019-03-05" - }, - { - "description": "Test Holiday 2", - "holiday_date": "2019-03-07" - }, - { - "description": "Test Holiday 3", - "holiday_date": "2019-02-11" - }, - ] - }).insert() + holiday_list = frappe.get_doc( + { + "doctype": "Holiday List", + "holiday_list_name": "__Test Holiday List", + "from_date": "2019-01-01", + "to_date": "2019-12-31", + "holidays": [ + {"description": "Test Holiday 1", "holiday_date": "2019-03-05"}, + {"description": "Test Holiday 2", "holiday_date": "2019-03-07"}, + {"description": "Test Holiday 3", "holiday_date": "2019-02-11"}, + ], + } + ).insert() diff --git a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py index f022d55a4b1..19e23493fe5 100644 --- a/erpnext/support/doctype/warranty_claim/test_warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/test_warranty_claim.py @@ -5,7 +5,8 @@ import unittest import frappe -test_records = frappe.get_test_records('Warranty Claim') +test_records = frappe.get_test_records("Warranty Claim") + class TestWarrantyClaim(unittest.TestCase): pass diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py index 87e95410264..5e2ea067a86 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.py +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import frappe from frappe import _, session from frappe.utils import now_datetime @@ -15,27 +14,33 @@ class WarrantyClaim(TransactionBase): return _("{0}: From {1}").format(self.status, self.customer_name) def validate(self): - if session['user'] != 'Guest' and not self.customer: + if session["user"] != "Guest" and not self.customer: frappe.throw(_("Customer is required")) - if self.status=="Closed" and not self.resolution_date and \ - frappe.db.get_value("Warranty Claim", self.name, "status")!="Closed": + if ( + self.status == "Closed" + and not self.resolution_date + and frappe.db.get_value("Warranty Claim", self.name, "status") != "Closed" + ): self.resolution_date = now_datetime() def on_cancel(self): - lst = frappe.db.sql("""select t1.name + lst = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent = t1.name and t2.prevdoc_docname = %s and t1.docstatus!=2""", - (self.name)) + (self.name), + ) if lst: - lst1 = ','.join(x[0] for x in lst) + lst1 = ",".join(x[0] for x in lst) frappe.throw(_("Cancel Material Visit {0} before cancelling this Warranty Claim").format(lst1)) else: - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") def on_update(self): pass + @frappe.whitelist() def make_maintenance_visit(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc, map_child_doc @@ -44,25 +49,25 @@ def make_maintenance_visit(source_name, target_doc=None): target_doc.prevdoc_doctype = source_parent.doctype target_doc.prevdoc_docname = source_parent.name - visit = frappe.db.sql("""select t1.name + visit = frappe.db.sql( + """select t1.name from `tabMaintenance Visit` t1, `tabMaintenance Visit Purpose` t2 where t2.parent=t1.name and t2.prevdoc_docname=%s - and t1.docstatus=1 and t1.completion_status='Fully Completed'""", source_name) + and t1.docstatus=1 and t1.completion_status='Fully Completed'""", + source_name, + ) if not visit: - target_doc = get_mapped_doc("Warranty Claim", source_name, { - "Warranty Claim": { - "doctype": "Maintenance Visit", - "field_map": {} - } - }, target_doc) + target_doc = get_mapped_doc( + "Warranty Claim", + source_name, + {"Warranty Claim": {"doctype": "Maintenance Visit", "field_map": {}}}, + target_doc, + ) source_doc = frappe.get_doc("Warranty Claim", source_name) if source_doc.get("item_code"): - table_map = { - "doctype": "Maintenance Visit Purpose", - "postprocess": _update_links - } + table_map = {"doctype": "Maintenance Visit Purpose", "postprocess": _update_links} map_child_doc(source_doc, target_doc, table_map, source_doc) return target_doc diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py index 2ab0fb88a7f..5b51ef81c7b 100644 --- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py @@ -7,21 +7,17 @@ import frappe def execute(filters=None): columns = [ + {"fieldname": "creation_date", "label": "Date", "fieldtype": "Date", "width": 300}, { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date', - 'width': 300 - }, - { - 'fieldname': 'first_response_time', - 'fieldtype': 'Duration', - 'label': 'First Response Time', - 'width': 300 + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "width": 300, }, ] - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT date(creation) as creation_date, avg(first_response_time) as avg_response_time @@ -31,6 +27,8 @@ def execute(filters=None): and first_response_time > 0 GROUP BY creation_date ORDER BY creation_date desc - ''', (filters.from_date, filters.to_date)) + """, + (filters.from_date, filters.to_date), + ) return columns, data diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py index 543a34cb304..d8a200ad686 100644 --- a/erpnext/support/report/issue_analytics/issue_analytics.py +++ b/erpnext/support/report/issue_analytics/issue_analytics.py @@ -15,6 +15,7 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): return IssueAnalytics(filters).run() + class IssueAnalytics(object): def __init__(self, filters=None): """Issue Analytics Report""" @@ -31,101 +32,98 @@ class IssueAnalytics(object): def get_columns(self): self.columns = [] - if self.filters.based_on == 'Customer': - self.columns.append({ - 'label': _('Customer'), - 'options': 'Customer', - 'fieldname': 'customer', - 'fieldtype': 'Link', - 'width': 200 - }) + if self.filters.based_on == "Customer": + self.columns.append( + { + "label": _("Customer"), + "options": "Customer", + "fieldname": "customer", + "fieldtype": "Link", + "width": 200, + } + ) - elif self.filters.based_on == 'Assigned To': - self.columns.append({ - 'label': _('User'), - 'fieldname': 'user', - 'fieldtype': 'Link', - 'options': 'User', - 'width': 200 - }) + elif self.filters.based_on == "Assigned To": + self.columns.append( + {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200} + ) - elif self.filters.based_on == 'Issue Type': - self.columns.append({ - 'label': _('Issue Type'), - 'fieldname': 'issue_type', - 'fieldtype': 'Link', - 'options': 'Issue Type', - 'width': 200 - }) + elif self.filters.based_on == "Issue Type": + self.columns.append( + { + "label": _("Issue Type"), + "fieldname": "issue_type", + "fieldtype": "Link", + "options": "Issue Type", + "width": 200, + } + ) - elif self.filters.based_on == 'Issue Priority': - self.columns.append({ - 'label': _('Issue Priority'), - 'fieldname': 'priority', - 'fieldtype': 'Link', - 'options': 'Issue Priority', - 'width': 200 - }) + elif self.filters.based_on == "Issue Priority": + self.columns.append( + { + "label": _("Issue Priority"), + "fieldname": "priority", + "fieldtype": "Link", + "options": "Issue Priority", + "width": 200, + } + ) for end_date in self.periodic_daterange: period = self.get_period(end_date) - self.columns.append({ - 'label': _(period), - 'fieldname': scrub(period), - 'fieldtype': 'Int', - 'width': 120 - }) + self.columns.append( + {"label": _(period), "fieldname": scrub(period), "fieldtype": "Int", "width": 120} + ) - self.columns.append({ - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Int', - 'width': 120 - }) + self.columns.append( + {"label": _("Total"), "fieldname": "total", "fieldtype": "Int", "width": 120} + ) def get_data(self): self.get_issues() self.get_rows() def get_period(self, date): - months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - if self.filters.range == 'Weekly': - period = 'Week ' + str(date.isocalendar()[1]) - elif self.filters.range == 'Monthly': + if self.filters.range == "Weekly": + period = "Week " + str(date.isocalendar()[1]) + elif self.filters.range == "Monthly": period = str(months[date.month - 1]) - elif self.filters.range == 'Quarterly': - period = 'Quarter ' + str(((date.month - 1) // 3) + 1) + elif self.filters.range == "Quarterly": + period = "Quarter " + str(((date.month - 1) // 3) + 1) else: year = get_fiscal_year(date, self.filters.company) period = str(year[0]) - if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year and self.filters.range != 'Yearly': - period += ' ' + str(date.year) + if ( + getdate(self.filters.from_date).year != getdate(self.filters.to_date).year + and self.filters.range != "Yearly" + ): + period += " " + str(date.year) return period def get_period_date_ranges(self): from dateutil.relativedelta import MO, relativedelta + from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date) - increment = { - 'Monthly': 1, - 'Quarterly': 3, - 'Half-Yearly': 6, - 'Yearly': 12 - }.get(self.filters.range, 1) + increment = {"Monthly": 1, "Quarterly": 3, "Half-Yearly": 6, "Yearly": 12}.get( + self.filters.range, 1 + ) - if self.filters.range in ['Monthly', 'Quarterly']: + if self.filters.range in ["Monthly", "Quarterly"]: from_date = from_date.replace(day=1) - elif self.filters.range == 'Yearly': + elif self.filters.range == "Yearly": from_date = get_fiscal_year(from_date)[1] else: from_date = from_date + relativedelta(from_date, weekday=MO(-1)) self.periodic_daterange = [] for dummy in range(1, 53): - if self.filters.range == 'Weekly': + if self.filters.range == "Weekly": period_end_date = add_days(from_date, 6) else: period_end_date = add_to_date(from_date, months=increment, days=-1) @@ -142,25 +140,26 @@ class IssueAnalytics(object): def get_issues(self): filters = self.get_common_filters() self.field_map = { - 'Customer': 'customer', - 'Issue Type': 'issue_type', - 'Issue Priority': 'priority', - 'Assigned To': '_assign' + "Customer": "customer", + "Issue Type": "issue_type", + "Issue Priority": "priority", + "Assigned To": "_assign", } - self.entries = frappe.db.get_all('Issue', - fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'], - filters=filters + self.entries = frappe.db.get_all( + "Issue", + fields=[self.field_map.get(self.filters.based_on), "name", "opening_date"], + filters=filters, ) def get_common_filters(self): filters = {} - filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + filters["opening_date"] = ("between", [self.filters.from_date, self.filters.to_date]) - if self.filters.get('assigned_to'): - filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%') + if self.filters.get("assigned_to"): + filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%") - for entry in ['company', 'status', 'priority', 'customer', 'project']: + for entry in ["company", "status", "priority", "customer", "project"]: if self.filters.get(entry): filters[entry] = self.filters.get(entry) @@ -171,14 +170,14 @@ class IssueAnalytics(object): self.get_periodic_data() for entity, period_data in iteritems(self.issue_periodic_data): - if self.filters.based_on == 'Customer': - row = {'customer': entity} - elif self.filters.based_on == 'Assigned To': - row = {'user': entity} - elif self.filters.based_on == 'Issue Type': - row = {'issue_type': entity} - elif self.filters.based_on == 'Issue Priority': - row = {'priority': entity} + if self.filters.based_on == "Customer": + row = {"customer": entity} + elif self.filters.based_on == "Assigned To": + row = {"user": entity} + elif self.filters.based_on == "Issue Type": + row = {"issue_type": entity} + elif self.filters.based_on == "Issue Priority": + row = {"priority": entity} total = 0 for end_date in self.periodic_daterange: @@ -187,7 +186,7 @@ class IssueAnalytics(object): row[scrub(period)] = amount total += amount - row['total'] = total + row["total"] = total self.data.append(row) @@ -195,9 +194,9 @@ class IssueAnalytics(object): self.issue_periodic_data = frappe._dict() for d in self.entries: - period = self.get_period(d.get('opening_date')) + period = self.get_period(d.get("opening_date")) - if self.filters.based_on == 'Assigned To': + if self.filters.based_on == "Assigned To": if d._assign: for entry in json.loads(d._assign): self.issue_periodic_data.setdefault(entry, frappe._dict()).setdefault(period, 0.0) @@ -207,18 +206,12 @@ class IssueAnalytics(object): field = self.field_map.get(self.filters.based_on) value = d.get(field) if not value: - value = _('Not Specified') + value = _("Not Specified") self.issue_periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0.0) self.issue_periodic_data[value][period] += 1 def get_chart_data(self): length = len(self.columns) - labels = [d.get('label') for d in self.columns[1:length-1]] - self.chart = { - 'data': { - 'labels': labels, - 'datasets': [] - }, - 'type': 'line' - } + labels = [d.get("label") for d in self.columns[1 : length - 1]] + self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py index b27dc46ad28..169392e5e92 100644 --- a/erpnext/support/report/issue_analytics/test_issue_analytics.py +++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -11,7 +10,8 @@ from erpnext.support.doctype.service_level_agreement.test_service_level_agreemen ) from erpnext.support.report.issue_analytics.issue_analytics import execute -months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + class TestIssueAnalytics(unittest.TestCase): @classmethod @@ -24,8 +24,8 @@ class TestIssueAnalytics(unittest.TestCase): self.current_month = str(months[current_month_date.month - 1]).lower() self.last_month = str(months[last_month_date.month - 1]).lower() if current_month_date.year != last_month_date.year: - self.current_month += '_' + str(current_month_date.year) - self.last_month += '_' + str(last_month_date.year) + self.current_month += "_" + str(current_month_date.year) + self.last_month += "_" + str(last_month_date.year) def test_issue_analytics(self): create_service_level_agreements_for_issues() @@ -39,146 +39,88 @@ class TestIssueAnalytics(unittest.TestCase): def compare_result_for_customer(self): filters = { - 'company': '_Test Company', - 'based_on': 'Customer', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Customer", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'customer': '__Test Customer 2', - self.last_month: 1.0, - self.current_month: 0.0, - 'total': 1.0 - }, - { - 'customer': '__Test Customer 1', - self.last_month: 0.0, - self.current_month: 1.0, - 'total': 1.0 - }, - { - 'customer': '__Test Customer', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - } + {"customer": "__Test Customer 2", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0}, + {"customer": "__Test Customer 1", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0}, + {"customer": "__Test Customer", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def compare_result_for_issue_type(self): filters = { - 'company': '_Test Company', - 'based_on': 'Issue Type', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Issue Type", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'issue_type': 'Discomfort', - self.last_month: 1.0, - self.current_month: 0.0, - 'total': 1.0 - }, - { - 'issue_type': 'Service Request', - self.last_month: 0.0, - self.current_month: 1.0, - 'total': 1.0 - }, - { - 'issue_type': 'Bug', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - } + {"issue_type": "Discomfort", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0}, + {"issue_type": "Service Request", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0}, + {"issue_type": "Bug", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def compare_result_for_issue_priority(self): filters = { - 'company': '_Test Company', - 'based_on': 'Issue Priority', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Issue Priority", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'priority': 'Medium', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - }, - { - 'priority': 'Low', - self.last_month: 1.0, - self.current_month: 0.0, - 'total': 1.0 - }, - { - 'priority': 'High', - self.last_month: 0.0, - self.current_month: 1.0, - 'total': 1.0 - } + {"priority": "Medium", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, + {"priority": "Low", self.last_month: 1.0, self.current_month: 0.0, "total": 1.0}, + {"priority": "High", self.last_month: 0.0, self.current_month: 1.0, "total": 1.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def compare_result_for_assignment(self): filters = { - 'company': '_Test Company', - 'based_on': 'Assigned To', - 'from_date': add_months(getdate(), -1), - 'to_date': getdate(), - 'range': 'Monthly' + "company": "_Test Company", + "based_on": "Assigned To", + "from_date": add_months(getdate(), -1), + "to_date": getdate(), + "range": "Monthly", } report = execute(filters) expected_data = [ - { - 'user': 'test@example.com', - self.last_month: 1.0, - self.current_month: 1.0, - 'total': 2.0 - }, - { - 'user': 'test1@example.com', - self.last_month: 2.0, - self.current_month: 1.0, - 'total': 3.0 - } + {"user": "test@example.com", self.last_month: 1.0, self.current_month: 1.0, "total": 2.0}, + {"user": "test1@example.com", self.last_month: 2.0, self.current_month: 1.0, "total": 3.0}, ] - self.assertEqual(expected_data, report[1]) # rows - self.assertEqual(len(report[0]), 4) # cols + self.assertEqual(expected_data, report[1]) # rows + self.assertEqual(len(report[0]), 4) # cols def create_issue_types(): - for entry in ['Bug', 'Service Request', 'Discomfort']: - if not frappe.db.exists('Issue Type', entry): - frappe.get_doc({ - 'doctype': 'Issue Type', - '__newname': entry - }).insert() + for entry in ["Bug", "Service Request", "Discomfort"]: + if not frappe.db.exists("Issue Type", entry): + frappe.get_doc({"doctype": "Issue Type", "__newname": entry}).insert() def create_records(): @@ -190,29 +132,15 @@ def create_records(): last_month_date = add_months(current_month_date, -1) issue = make_issue(current_month_date, "__Test Customer", 2, "High", "Bug") - add_assignment({ - "assign_to": ["test@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment({"assign_to": ["test@example.com"], "doctype": "Issue", "name": issue.name}) issue = make_issue(last_month_date, "__Test Customer", 2, "Low", "Bug") - add_assignment({ - "assign_to": ["test1@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment({"assign_to": ["test1@example.com"], "doctype": "Issue", "name": issue.name}) issue = make_issue(current_month_date, "__Test Customer 1", 2, "Medium", "Service Request") - add_assignment({ - "assign_to": ["test1@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment({"assign_to": ["test1@example.com"], "doctype": "Issue", "name": issue.name}) issue = make_issue(last_month_date, "__Test Customer 2", 2, "Medium", "Discomfort") - add_assignment({ - "assign_to": ["test@example.com", "test1@example.com"], - "doctype": "Issue", - "name": issue.name - }) + add_assignment( + {"assign_to": ["test@example.com", "test1@example.com"], "doctype": "Issue", "name": issue.name} + ) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index b5d52278b89..a81bcc7f32a 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -13,6 +13,7 @@ from six import iteritems def execute(filters=None): return IssueSummary(filters).run() + class IssueSummary(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -28,82 +29,77 @@ class IssueSummary(object): def get_columns(self): self.columns = [] - if self.filters.based_on == 'Customer': - self.columns.append({ - 'label': _('Customer'), - 'options': 'Customer', - 'fieldname': 'customer', - 'fieldtype': 'Link', - 'width': 200 - }) + if self.filters.based_on == "Customer": + self.columns.append( + { + "label": _("Customer"), + "options": "Customer", + "fieldname": "customer", + "fieldtype": "Link", + "width": 200, + } + ) - elif self.filters.based_on == 'Assigned To': - self.columns.append({ - 'label': _('User'), - 'fieldname': 'user', - 'fieldtype': 'Link', - 'options': 'User', - 'width': 200 - }) + elif self.filters.based_on == "Assigned To": + self.columns.append( + {"label": _("User"), "fieldname": "user", "fieldtype": "Link", "options": "User", "width": 200} + ) - elif self.filters.based_on == 'Issue Type': - self.columns.append({ - 'label': _('Issue Type'), - 'fieldname': 'issue_type', - 'fieldtype': 'Link', - 'options': 'Issue Type', - 'width': 200 - }) + elif self.filters.based_on == "Issue Type": + self.columns.append( + { + "label": _("Issue Type"), + "fieldname": "issue_type", + "fieldtype": "Link", + "options": "Issue Type", + "width": 200, + } + ) - elif self.filters.based_on == 'Issue Priority': - self.columns.append({ - 'label': _('Issue Priority'), - 'fieldname': 'priority', - 'fieldtype': 'Link', - 'options': 'Issue Priority', - 'width': 200 - }) + elif self.filters.based_on == "Issue Priority": + self.columns.append( + { + "label": _("Issue Priority"), + "fieldname": "priority", + "fieldtype": "Link", + "options": "Issue Priority", + "width": 200, + } + ) - self.statuses = ['Open', 'Replied', 'On Hold', 'Resolved', 'Closed'] + self.statuses = ["Open", "Replied", "On Hold", "Resolved", "Closed"] for status in self.statuses: - self.columns.append({ - 'label': _(status), - 'fieldname': scrub(status), - 'fieldtype': 'Int', - 'width': 80 - }) + self.columns.append( + {"label": _(status), "fieldname": scrub(status), "fieldtype": "Int", "width": 80} + ) - self.columns.append({ - 'label': _('Total Issues'), - 'fieldname': 'total_issues', - 'fieldtype': 'Int', - 'width': 100 - }) + self.columns.append( + {"label": _("Total Issues"), "fieldname": "total_issues", "fieldtype": "Int", "width": 100} + ) self.sla_status_map = { - 'SLA Failed': 'failed', - 'SLA Fulfilled': 'fulfilled', - 'SLA Ongoing': 'ongoing' + "SLA Failed": "failed", + "SLA Fulfilled": "fulfilled", + "SLA Ongoing": "ongoing", } for label, fieldname in self.sla_status_map.items(): - self.columns.append({ - 'label': _(label), - 'fieldname': fieldname, - 'fieldtype': 'Int', - 'width': 100 - }) + self.columns.append( + {"label": _(label), "fieldname": fieldname, "fieldtype": "Int", "width": 100} + ) - self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time', - 'Avg Resolution Time', 'Avg User Resolution Time'] + self.metrics = [ + "Avg First Response Time", + "Avg Response Time", + "Avg Hold Time", + "Avg Resolution Time", + "Avg User Resolution Time", + ] for metric in self.metrics: - self.columns.append({ - 'label': _(metric), - 'fieldname': scrub(metric), - 'fieldtype': 'Duration', - 'width': 170 - }) + self.columns.append( + {"label": _(metric), "fieldname": scrub(metric), "fieldtype": "Duration", "width": 170} + ) def get_data(self): self.get_issues() @@ -112,26 +108,37 @@ class IssueSummary(object): def get_issues(self): filters = self.get_common_filters() self.field_map = { - 'Customer': 'customer', - 'Issue Type': 'issue_type', - 'Issue Priority': 'priority', - 'Assigned To': '_assign' + "Customer": "customer", + "Issue Type": "issue_type", + "Issue Priority": "priority", + "Assigned To": "_assign", } - self.entries = frappe.db.get_all('Issue', - fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time', - 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'], - filters=filters + self.entries = frappe.db.get_all( + "Issue", + fields=[ + self.field_map.get(self.filters.based_on), + "name", + "opening_date", + "status", + "avg_response_time", + "first_response_time", + "total_hold_time", + "user_resolution_time", + "resolution_time", + "agreement_status", + ], + filters=filters, ) def get_common_filters(self): filters = {} - filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date]) + filters["opening_date"] = ("between", [self.filters.from_date, self.filters.to_date]) - if self.filters.get('assigned_to'): - filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%') + if self.filters.get("assigned_to"): + filters["_assign"] = ("like", "%" + self.filters.get("assigned_to") + "%") - for entry in ['company', 'status', 'priority', 'customer', 'project']: + for entry in ["company", "status", "priority", "customer", "project"]: if self.filters.get(entry): filters[entry] = self.filters.get(entry) @@ -142,20 +149,20 @@ class IssueSummary(object): self.get_summary_data() for entity, data in iteritems(self.issue_summary_data): - if self.filters.based_on == 'Customer': - row = {'customer': entity} - elif self.filters.based_on == 'Assigned To': - row = {'user': entity} - elif self.filters.based_on == 'Issue Type': - row = {'issue_type': entity} - elif self.filters.based_on == 'Issue Priority': - row = {'priority': entity} + if self.filters.based_on == "Customer": + row = {"customer": entity} + elif self.filters.based_on == "Assigned To": + row = {"user": entity} + elif self.filters.based_on == "Issue Type": + row = {"issue_type": entity} + elif self.filters.based_on == "Issue Priority": + row = {"priority": entity} for status in self.statuses: count = flt(data.get(status, 0.0)) row[scrub(status)] = count - row['total_issues'] = data.get('total_issues', 0.0) + row["total_issues"] = data.get("total_issues", 0.0) for sla_status in self.sla_status_map.values(): value = flt(data.get(sla_status), 0.0) @@ -174,36 +181,41 @@ class IssueSummary(object): status = d.status agreement_status = scrub(d.agreement_status) - if self.filters.based_on == 'Assigned To': + if self.filters.based_on == "Assigned To": if d._assign: for entry in json.loads(d._assign): self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0) self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0) - self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault("total_issues", 0.0) self.issue_summary_data[entry][status] += 1 self.issue_summary_data[entry][agreement_status] += 1 - self.issue_summary_data[entry]['total_issues'] += 1 + self.issue_summary_data[entry]["total_issues"] += 1 else: field = self.field_map.get(self.filters.based_on) value = d.get(field) if not value: - value = _('Not Specified') + value = _("Not Specified") self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0) self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0) - self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0) + self.issue_summary_data.setdefault(value, frappe._dict()).setdefault("total_issues", 0.0) self.issue_summary_data[value][status] += 1 self.issue_summary_data[value][agreement_status] += 1 - self.issue_summary_data[value]['total_issues'] += 1 + self.issue_summary_data[value]["total_issues"] += 1 self.get_metrics_data() def get_metrics_data(self): issues = [] - metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time', - 'avg_resolution_time', 'avg_user_resolution_time'] + metrics_list = [ + "avg_response_time", + "avg_first_response_time", + "avg_hold_time", + "avg_resolution_time", + "avg_user_resolution_time", + ] for entry in self.entries: issues.append(entry.name) @@ -211,7 +223,7 @@ class IssueSummary(object): field = self.field_map.get(self.filters.based_on) if issues: - if self.filters.based_on == 'Assigned To': + if self.filters.based_on == "Assigned To": assignment_map = frappe._dict() for d in self.entries: if d._assign: @@ -219,11 +231,15 @@ class IssueSummary(object): for metric in metrics_list: self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0) - self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0 - self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0 - self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0 - self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0 - self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0 + self.issue_summary_data[entry]["avg_response_time"] += d.get("avg_response_time") or 0.0 + self.issue_summary_data[entry]["avg_first_response_time"] += ( + d.get("first_response_time") or 0.0 + ) + self.issue_summary_data[entry]["avg_hold_time"] += d.get("total_hold_time") or 0.0 + self.issue_summary_data[entry]["avg_resolution_time"] += d.get("resolution_time") or 0.0 + self.issue_summary_data[entry]["avg_user_resolution_time"] += ( + d.get("user_resolution_time") or 0.0 + ) if not assignment_map.get(entry): assignment_map[entry] = 0 @@ -234,7 +250,8 @@ class IssueSummary(object): self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry)) else: - data = frappe.db.sql(""" + data = frappe.db.sql( + """ SELECT {0}, AVG(first_response_time) as avg_frt, AVG(avg_response_time) as avg_resp_time, @@ -245,21 +262,30 @@ class IssueSummary(object): WHERE name IN %(issues)s GROUP BY {0} - """.format(field), {'issues': issues}, as_dict=1) + """.format( + field + ), + {"issues": issues}, + as_dict=1, + ) for entry in data: value = entry.get(field) if not value: - value = _('Not Specified') + value = _("Not Specified") for metric in metrics_list: self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0) - self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0 - self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0 - self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0 - self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0 - self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0 + self.issue_summary_data[value]["avg_response_time"] = entry.get("avg_resp_time") or 0.0 + self.issue_summary_data[value]["avg_first_response_time"] = entry.get("avg_frt") or 0.0 + self.issue_summary_data[value]["avg_hold_time"] = entry.get("avg_hold_time") or 0.0 + self.issue_summary_data[value]["avg_resolution_time"] = ( + entry.get("avg_resolution_time") or 0.0 + ) + self.issue_summary_data[value]["avg_user_resolution_time"] = ( + entry.get("avg_user_resolution_time") or 0.0 + ) def get_chart_data(self): self.chart = [] @@ -273,47 +299,30 @@ class IssueSummary(object): entity = self.filters.based_on entity_field = self.field_map.get(entity) - if entity == 'Assigned To': - entity_field = 'user' + if entity == "Assigned To": + entity_field = "user" for entry in self.data: labels.append(entry.get(entity_field)) - open_issues.append(entry.get('open')) - replied_issues.append(entry.get('replied')) - on_hold_issues.append(entry.get('on_hold')) - resolved_issues.append(entry.get('resolved')) - closed_issues.append(entry.get('closed')) + open_issues.append(entry.get("open")) + replied_issues.append(entry.get("replied")) + on_hold_issues.append(entry.get("on_hold")) + resolved_issues.append(entry.get("resolved")) + closed_issues.append(entry.get("closed")) self.chart = { - 'data': { - 'labels': labels[:30], - 'datasets': [ - { - 'name': 'Open', - 'values': open_issues[:30] - }, - { - 'name': 'Replied', - 'values': replied_issues[:30] - }, - { - 'name': 'On Hold', - 'values': on_hold_issues[:30] - }, - { - 'name': 'Resolved', - 'values': resolved_issues[:30] - }, - { - 'name': 'Closed', - 'values': closed_issues[:30] - } - ] + "data": { + "labels": labels[:30], + "datasets": [ + {"name": "Open", "values": open_issues[:30]}, + {"name": "Replied", "values": replied_issues[:30]}, + {"name": "On Hold", "values": on_hold_issues[:30]}, + {"name": "Resolved", "values": resolved_issues[:30]}, + {"name": "Closed", "values": closed_issues[:30]}, + ], }, - 'type': 'bar', - 'barOptions': { - 'stacked': True - } + "type": "bar", + "barOptions": {"stacked": True}, } def get_report_summary(self): @@ -326,41 +335,41 @@ class IssueSummary(object): closed = 0 for entry in self.data: - open_issues += entry.get('open') - replied += entry.get('replied') - on_hold += entry.get('on_hold') - resolved += entry.get('resolved') - closed += entry.get('closed') + open_issues += entry.get("open") + replied += entry.get("replied") + on_hold += entry.get("on_hold") + resolved += entry.get("resolved") + closed += entry.get("closed") self.report_summary = [ { - 'value': open_issues, - 'indicator': 'Red', - 'label': _('Open'), - 'datatype': 'Int', + "value": open_issues, + "indicator": "Red", + "label": _("Open"), + "datatype": "Int", }, { - 'value': replied, - 'indicator': 'Grey', - 'label': _('Replied'), - 'datatype': 'Int', + "value": replied, + "indicator": "Grey", + "label": _("Replied"), + "datatype": "Int", }, { - 'value': on_hold, - 'indicator': 'Grey', - 'label': _('On Hold'), - 'datatype': 'Int', + "value": on_hold, + "indicator": "Grey", + "label": _("On Hold"), + "datatype": "Int", }, { - 'value': resolved, - 'indicator': 'Green', - 'label': _('Resolved'), - 'datatype': 'Int', + "value": resolved, + "indicator": "Green", + "label": _("Resolved"), + "datatype": "Int", }, { - 'value': closed, - 'indicator': 'Green', - 'label': _('Closed'), - 'datatype': 'Int', - } + "value": closed, + "indicator": "Green", + "label": _("Closed"), + "datatype": "Int", + }, ] diff --git a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py index e3a7e5f54b1..b4bd460a67f 100644 --- a/erpnext/support/report/support_hour_distribution/support_hour_distribution.py +++ b/erpnext/support/report/support_hour_distribution/support_hour_distribution.py @@ -8,34 +8,36 @@ from frappe.utils import add_to_date, get_datetime, getdate from six import iteritems time_slots = { - '12AM - 3AM': '00:00:00-03:00:00', - '3AM - 6AM': '03:00:00-06:00:00', - '6AM - 9AM': '06:00:00-09:00:00', - '9AM - 12PM': '09:00:00-12:00:00', - '12PM - 3PM': '12:00:00-15:00:00', - '3PM - 6PM': '15:00:00-18:00:00', - '6PM - 9PM': '18:00:00-21:00:00', - '9PM - 12AM': '21:00:00-23:00:00' + "12AM - 3AM": "00:00:00-03:00:00", + "3AM - 6AM": "03:00:00-06:00:00", + "6AM - 9AM": "06:00:00-09:00:00", + "9AM - 12PM": "09:00:00-12:00:00", + "12PM - 3PM": "12:00:00-15:00:00", + "3PM - 6PM": "15:00:00-18:00:00", + "6PM - 9PM": "18:00:00-21:00:00", + "9PM - 12AM": "21:00:00-23:00:00", } + def execute(filters=None): columns, data = [], [] - if not filters.get('periodicity'): - filters['periodicity'] = 'Daily' + if not filters.get("periodicity"): + filters["periodicity"] = "Daily" columns = get_columns() data, timeslot_wise_count = get_data(filters) chart = get_chart_data(timeslot_wise_count) return columns, data, None, chart + def get_data(filters): start_date = getdate(filters.from_date) data = [] time_slot_wise_total_count = {} - while(start_date <= getdate(filters.to_date)): - hours_count = {'date': start_date} + while start_date <= getdate(filters.to_date): + hours_count = {"date": start_date} for key, value in iteritems(time_slots): - start_time, end_time = value.split('-') + start_time, end_time = value.split("-") start_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), start_time)) end_time = get_datetime("{0} {1}".format(start_date.strftime("%Y-%m-%d"), end_time)) hours_count[key] = get_hours_count(start_time, end_time) @@ -48,49 +50,57 @@ def get_data(filters): return data, time_slot_wise_total_count + def get_hours_count(start_time, end_time): - data = frappe.db.sql(""" select count(*) from `tabIssue` where creation - between %(start_time)s and %(end_time)s""", { - 'start_time': start_time, - 'end_time': end_time - }, as_list=1) or [] + data = ( + frappe.db.sql( + """ select count(*) from `tabIssue` where creation + between %(start_time)s and %(end_time)s""", + {"start_time": start_time, "end_time": end_time}, + as_list=1, + ) + or [] + ) return data[0][0] if len(data) > 0 else 0 -def get_columns(): - columns = [{ - "fieldname": "date", - "label": _("Date"), - "fieldtype": "Date", - "width": 100 - }] - for label in ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM', - '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM']: - columns.append({ - "fieldname": label, - "label": _(label), - "fieldtype": "Data", - "width": 120 - }) +def get_columns(): + columns = [{"fieldname": "date", "label": _("Date"), "fieldtype": "Date", "width": 100}] + + for label in [ + "12AM - 3AM", + "3AM - 6AM", + "6AM - 9AM", + "9AM - 12PM", + "12PM - 3PM", + "3PM - 6PM", + "6PM - 9PM", + "9PM - 12AM", + ]: + columns.append({"fieldname": label, "label": _(label), "fieldtype": "Data", "width": 120}) return columns + def get_chart_data(timeslot_wise_count): total_count = [] - timeslots = ['12AM - 3AM', '3AM - 6AM', '6AM - 9AM', - '9AM - 12PM', '12PM - 3PM', '3PM - 6PM', '6PM - 9PM', '9PM - 12AM'] + timeslots = [ + "12AM - 3AM", + "3AM - 6AM", + "6AM - 9AM", + "9AM - 12PM", + "12PM - 3PM", + "3PM - 6PM", + "6PM - 9PM", + "9PM - 12AM", + ] datasets = [] for data in timeslots: total_count.append(timeslot_wise_count.get(data, 0)) - datasets.append({'values': total_count}) + datasets.append({"values": total_count}) - chart = { - "data": { - 'labels': timeslots, - 'datasets': datasets - } - } + chart = {"data": {"labels": timeslots, "datasets": datasets}} chart["type"] = "line" return chart diff --git a/erpnext/support/web_form/issues/issues.py b/erpnext/support/web_form/issues/issues.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/support/web_form/issues/issues.py +++ b/erpnext/support/web_form/issues/issues.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py index 0c24484bdfb..1c88883abce 100644 --- a/erpnext/telephony/doctype/call_log/call_log.py +++ b/erpnext/telephony/doctype/call_log/call_log.py @@ -11,8 +11,8 @@ from frappe.model.document import Document from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number -END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed'] -ONGOING_CALL_STATUSES = ['Ringing', 'In Progress'] +END_CALL_STATUSES = ["No Answer", "Completed", "Busy", "Failed"] +ONGOING_CALL_STATUSES = ["Ringing", "In Progress"] class CallLog(Document): @@ -20,18 +20,17 @@ class CallLog(Document): deduplicate_dynamic_links(self) def before_insert(self): - """Add lead(third party person) links to the document. - """ - lead_number = self.get('from') if self.is_incoming_call() else self.get('to') + """Add lead(third party person) links to the document.""" + lead_number = self.get("from") if self.is_incoming_call() else self.get("to") lead_number = strip_number(lead_number) contact = get_contact_with_phone_number(strip_number(lead_number)) if contact: - self.add_link(link_type='Contact', link_name=contact) + self.add_link(link_type="Contact", link_name=contact) lead = get_lead_with_phone_number(lead_number) if lead: - self.add_link(link_type='Lead', link_name=lead) + self.add_link(link_type="Lead", link_name=lead) def after_insert(self): self.trigger_call_popup() @@ -39,29 +38,29 @@ class CallLog(Document): def on_update(self): def _is_call_missed(doc_before_save, doc_after_save): # FIXME: This works for Exotel but not for all telepony providers - return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES + return ( + doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES + ) def _is_call_ended(doc_before_save, doc_after_save): return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES doc_before_save = self.get_doc_before_save() - if not doc_before_save: return + if not doc_before_save: + return if _is_call_missed(doc_before_save, self): - frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self) + frappe.publish_realtime("call_{id}_missed".format(id=self.id), self) self.trigger_call_popup() if _is_call_ended(doc_before_save, self): - frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self) + frappe.publish_realtime("call_{id}_ended".format(id=self.id), self) def is_incoming_call(self): - return self.type == 'Incoming' + return self.type == "Incoming" def add_link(self, link_type, link_name): - self.append('links', { - 'link_doctype': link_type, - 'link_name': link_name - }) + self.append("links", {"link_doctype": link_type, "link_name": link_name}) def trigger_call_popup(self): if self.is_incoming_call(): @@ -72,53 +71,63 @@ class CallLog(Document): emails = set(scheduled_employees).intersection(employee_emails) if frappe.conf.developer_mode: - self.add_comment(text=f""" + self.add_comment( + text=f""" Scheduled Employees: {scheduled_employees} Matching Employee: {employee_emails} Show Popup To: {emails} - """) + """ + ) if employee_emails and not emails: self.add_comment(text=_("No employee was scheduled for call popup")) for email in emails: - frappe.publish_realtime('show_call_popup', self, user=email) + frappe.publish_realtime("show_call_popup", self, user=email) @frappe.whitelist() def add_call_summary(call_log, summary): - doc = frappe.get_doc('Call Log', call_log) - doc.add_comment('Comment', frappe.bold(_('Call Summary')) + '

    ' + summary) + doc = frappe.get_doc("Call Log", call_log) + doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "

    " + summary) + def get_employees_with_number(number): number = strip_number(number) - if not number: return [] + if not number: + return [] - employee_emails = frappe.cache().hget('employees_with_number', number) - if employee_emails: return employee_emails + employee_emails = frappe.cache().hget("employees_with_number", number) + if employee_emails: + return employee_emails - employees = frappe.get_all('Employee', filters={ - 'cell_number': ['like', '%{}%'.format(number)], - 'user_id': ['!=', ''] - }, fields=['user_id']) + employees = frappe.get_all( + "Employee", + filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]}, + fields=["user_id"], + ) employee_emails = [employee.user_id for employee in employees] - frappe.cache().hset('employees_with_number', number, employee_emails) + frappe.cache().hset("employees_with_number", number, employee_emails) return employee_emails + def link_existing_conversations(doc, state): """ Called from hooks on creation of Contact or Lead to link all the existing conversations. """ - if doc.doctype != 'Contact': return + if doc.doctype != "Contact": + return try: numbers = [d.phone for d in doc.phone_nos] for number in numbers: number = strip_number(number) - if not number: continue - logs = frappe.db.sql_list(""" + if not number: + continue + logs = frappe.db.sql_list( + """ SELECT cl.name FROM `tabCall Log` cl LEFT JOIN `tabDynamic Link` dl ON cl.name = dl.parent @@ -131,44 +140,42 @@ def link_existing_conversations(doc, state): ELSE 0 END )=0 - """, dict( - phone_number='%{}'.format(number), - docname=doc.name, - doctype = doc.doctype - ) + """, + dict(phone_number="%{}".format(number), docname=doc.name, doctype=doc.doctype), ) for log in logs: - call_log = frappe.get_doc('Call Log', log) + call_log = frappe.get_doc("Call Log", log) call_log.add_link(link_type=doc.doctype, link_name=doc.name) call_log.save(ignore_permissions=True) frappe.db.commit() except Exception: - frappe.log_error(title=_('Error during caller information update')) + frappe.log_error(title=_("Error during caller information update")) + def get_linked_call_logs(doctype, docname): # content will be shown in timeline - logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={ - 'parenttype': 'Call Log', - 'link_doctype': doctype, - 'link_name': docname - }) + logs = frappe.get_all( + "Dynamic Link", + fields=["parent"], + filters={"parenttype": "Call Log", "link_doctype": doctype, "link_name": docname}, + ) logs = set([log.parent for log in logs]) - logs = frappe.get_all('Call Log', fields=['*'], filters={ - 'name': ['in', logs] - }) + logs = frappe.get_all("Call Log", fields=["*"], filters={"name": ["in", logs]}) timeline_contents = [] for log in logs: log.show_call_button = 0 - timeline_contents.append({ - 'icon': 'call', - 'is_card': True, - 'creation': log.creation, - 'template': 'call_link', - 'template_data': log - }) + timeline_contents.append( + { + "icon": "call", + "is_card": True, + "creation": log.creation, + "template": "call_link", + "template_data": log, + } + ) return timeline_contents diff --git a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py index 08e244d8897..5edf81df736 100644 --- a/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py +++ b/erpnext/telephony/doctype/incoming_call_settings/incoming_call_settings.py @@ -20,35 +20,38 @@ class IncomingCallSettings(Document): self.validate_call_schedule_overlaps(self.call_handling_schedule) def validate_call_schedule_timeslot(self, schedule: list): - """ Make sure that to time slot is ahead of from time slot. - """ + """Make sure that to time slot is ahead of from time slot.""" errors = [] for record in schedule: from_time = self.time_to_seconds(record.from_time) to_time = self.time_to_seconds(record.to_time) if from_time >= to_time: errors.append( - _('Call Schedule Row {0}: To time slot should always be ahead of From time slot.').format(record.idx) + _("Call Schedule Row {0}: To time slot should always be ahead of From time slot.").format( + record.idx + ) ) if errors: - frappe.throw('
    '.join(errors)) + frappe.throw("
    ".join(errors)) def validate_call_schedule_overlaps(self, schedule: list): - """Check if any time slots are overlapped in a day schedule. - """ + """Check if any time slots are overlapped in a day schedule.""" week_days = set([each.day_of_week for each in schedule]) for day in week_days: - timeslots = [(record.from_time, record.to_time) for record in schedule if record.day_of_week==day] + timeslots = [ + (record.from_time, record.to_time) for record in schedule if record.day_of_week == day + ] # convert time in timeslot into an integer represents number of seconds timeslots = sorted(map(lambda seq: tuple(map(self.time_to_seconds, seq)), timeslots)) - if len(timeslots) < 2: continue + if len(timeslots) < 2: + continue for i in range(1, len(timeslots)): - if self.check_timeslots_overlap(timeslots[i-1], timeslots[i]): - frappe.throw(_('Please fix overlapping time slots for {0}.').format(day)) + if self.check_timeslots_overlap(timeslots[i - 1], timeslots[i]): + frappe.throw(_("Please fix overlapping time slots for {0}.").format(day)) @staticmethod def check_timeslots_overlap(ts1: Tuple[int, int], ts2: Tuple[int, int]) -> bool: @@ -58,7 +61,6 @@ class IncomingCallSettings(Document): @staticmethod def time_to_seconds(time: str) -> int: - """Convert time string of format HH:MM:SS into seconds - """ + """Convert time string of format HH:MM:SS into seconds""" date_time = datetime.strptime(time, "%H:%M:%S") return date_time - datetime(1900, 1, 1) diff --git a/erpnext/templates/pages/cart.py b/erpnext/templates/pages/cart.py index 0d0e6ce4f8f..25adb00b01c 100644 --- a/erpnext/templates/pages/cart.py +++ b/erpnext/templates/pages/cart.py @@ -5,6 +5,7 @@ from erpnext.e_commerce.shopping_cart.cart import get_cart_quotation no_cache = 1 + def get_context(context): context.body_class = "product-page" context.update(get_cart_quotation()) diff --git a/erpnext/templates/pages/courses.py b/erpnext/templates/pages/courses.py index 6051e60aa30..fb1af387d22 100644 --- a/erpnext/templates/pages/courses.py +++ b/erpnext/templates/pages/courses.py @@ -6,13 +6,13 @@ import frappe def get_context(context): - course = frappe.get_doc('Course', frappe.form_dict.course) + course = frappe.get_doc("Course", frappe.form_dict.course) sidebar_title = course.name context.no_cache = 1 context.show_sidebar = True - course = frappe.get_doc('Course', frappe.form_dict.course) - course.has_permission('read') + course = frappe.get_doc("Course", frappe.form_dict.course) + course.has_permission("read") context.doc = course context.sidebar_title = sidebar_title context.intro = course.course_intro diff --git a/erpnext/templates/pages/help.py b/erpnext/templates/pages/help.py index 25e7c2623db..19993ee9b13 100644 --- a/erpnext/templates/pages/help.py +++ b/erpnext/templates/pages/help.py @@ -1,4 +1,3 @@ - import json import frappe @@ -26,21 +25,19 @@ def get_context(context): else: context.issues = [] + def get_forum_posts(s): - response = requests.get(s.forum_url + '/' + s.get_latest_query) + response = requests.get(s.forum_url + "/" + s.get_latest_query) response.raise_for_status() response_json = response.json() - topics_data = {} # it will actually be an array - key_list = s.response_key_list.split(',') + topics_data = {} # it will actually be an array + key_list = s.response_key_list.split(",") for key in key_list: topics_data = response_json.get(key) if not topics_data else topics_data.get(key) for topic in topics_data: - topic["link"] = s.forum_url + '/' + s.post_route_string + '/' + str(topic.get(s.post_route_key)) + topic["link"] = s.forum_url + "/" + s.post_route_string + "/" + str(topic.get(s.post_route_key)) - post_params = { - "title": s.post_title_key, - "description": s.post_description_key - } + post_params = {"title": s.post_title_key, "description": s.post_description_key} return topics_data, post_params diff --git a/erpnext/templates/pages/home.py b/erpnext/templates/pages/home.py index d08e81b9e62..bca3e560536 100644 --- a/erpnext/templates/pages/home.py +++ b/erpnext/templates/pages/home.py @@ -6,46 +6,51 @@ import frappe no_cache = 1 + def get_context(context): - homepage = frappe.get_doc('Homepage') + homepage = frappe.get_doc("Homepage") for item in homepage.products: - route = frappe.db.get_value('Website Item', {"item_code": item.item_code}, 'route') + route = frappe.db.get_value("Website Item", {"item_code": item.item_code}, "route") if route: - item.route = '/' + route + item.route = "/" + route homepage.title = homepage.title or homepage.company context.title = homepage.title context.homepage = homepage - if homepage.hero_section_based_on == 'Homepage Section' and homepage.hero_section: - homepage.hero_section_doc = frappe.get_doc('Homepage Section', homepage.hero_section) + if homepage.hero_section_based_on == "Homepage Section" and homepage.hero_section: + homepage.hero_section_doc = frappe.get_doc("Homepage Section", homepage.hero_section) if homepage.slideshow: - doc = frappe.get_doc('Website Slideshow', homepage.slideshow) + doc = frappe.get_doc("Website Slideshow", homepage.slideshow) context.slideshow = homepage.slideshow context.slideshow_header = doc.header context.slides = doc.slideshow_items - context.blogs = frappe.get_all('Blog Post', - fields=['title', 'blogger', 'blog_intro', 'route'], - filters={ - 'published': 1 - }, - order_by='modified desc', - limit=3 + context.blogs = frappe.get_all( + "Blog Post", + fields=["title", "blogger", "blog_intro", "route"], + filters={"published": 1}, + order_by="modified desc", + limit=3, ) # filter out homepage section which is used as hero section - homepage_hero_section = homepage.hero_section_based_on == 'Homepage Section' and homepage.hero_section - homepage_sections = frappe.get_all('Homepage Section', - filters=[['name', '!=', homepage_hero_section]] if homepage_hero_section else None, - order_by='section_order asc' + homepage_hero_section = ( + homepage.hero_section_based_on == "Homepage Section" and homepage.hero_section ) - context.homepage_sections = [frappe.get_doc('Homepage Section', name) for name in homepage_sections] + homepage_sections = frappe.get_all( + "Homepage Section", + filters=[["name", "!=", homepage_hero_section]] if homepage_hero_section else None, + order_by="section_order asc", + ) + context.homepage_sections = [ + frappe.get_doc("Homepage Section", name) for name in homepage_sections + ] context.metatags = context.metatags or frappe._dict({}) context.metatags.image = homepage.hero_image or None context.metatags.description = homepage.description or None - context.explore_link = '/all-products' + context.explore_link = "/all-products" diff --git a/erpnext/templates/pages/integrations/gocardless_checkout.py b/erpnext/templates/pages/integrations/gocardless_checkout.py index bbdbf1ddf97..280f67f16b9 100644 --- a/erpnext/templates/pages/integrations/gocardless_checkout.py +++ b/erpnext/templates/pages/integrations/gocardless_checkout.py @@ -14,8 +14,18 @@ from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_setting no_cache = 1 -expected_keys = ('amount', 'title', 'description', 'reference_doctype', 'reference_docname', - 'payer_name', 'payer_email', 'order_id', 'currency') +expected_keys = ( + "amount", + "title", + "description", + "reference_doctype", + "reference_docname", + "payer_name", + "payer_email", + "order_id", + "currency", +) + def get_context(context): context.no_cache = 1 @@ -25,17 +35,22 @@ def get_context(context): for key in expected_keys: context[key] = frappe.form_dict[key] - context['amount'] = flt(context['amount']) + context["amount"] = flt(context["amount"]) gateway_controller = get_gateway_controller(context.reference_docname) - context['header_img'] = frappe.db.get_value("GoCardless Settings", gateway_controller, "header_img") + context["header_img"] = frappe.db.get_value( + "GoCardless Settings", gateway_controller, "header_img" + ) else: - frappe.redirect_to_message(_('Some information is missing'), - _('Looks like someone sent you to an incomplete URL. Please ask them to look into it.')) + frappe.redirect_to_message( + _("Some information is missing"), + _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), + ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect + @frappe.whitelist(allow_guest=True) def check_mandate(data, reference_doctype, reference_docname): data = json.loads(data) @@ -59,23 +74,27 @@ def check_mandate(data, reference_doctype, reference_docname): prefilled_customer.update({"email": frappe.session.user}) else: - prefilled_customer = { - "company_name": payer.name, - "email": frappe.session.user - } + prefilled_customer = {"company_name": payer.name, "email": frappe.session.user} - success_url = get_url("./integrations/gocardless_confirmation?reference_doctype=" + reference_doctype + "&reference_docname=" + reference_docname) + success_url = get_url( + "./integrations/gocardless_confirmation?reference_doctype=" + + reference_doctype + + "&reference_docname=" + + reference_docname + ) try: - redirect_flow = client.redirect_flows.create(params={ - "description": _("Pay {0} {1}").format(data['amount'], data['currency']), - "session_token": frappe.session.user, - "success_redirect_url": success_url, - "prefilled_customer": prefilled_customer - }) + redirect_flow = client.redirect_flows.create( + params={ + "description": _("Pay {0} {1}").format(data["amount"], data["currency"]), + "session_token": frappe.session.user, + "success_redirect_url": success_url, + "prefilled_customer": prefilled_customer, + } + ) return {"redirect_to": redirect_flow.redirect_url} except Exception as e: frappe.log_error(e, "GoCardless Payment Error") - return {"redirect_to": '/integrations/payment-failed'} + return {"redirect_to": "/integrations/payment-failed"} diff --git a/erpnext/templates/pages/integrations/gocardless_confirmation.py b/erpnext/templates/pages/integrations/gocardless_confirmation.py index a6c3e714947..cab532a5303 100644 --- a/erpnext/templates/pages/integrations/gocardless_confirmation.py +++ b/erpnext/templates/pages/integrations/gocardless_confirmation.py @@ -11,7 +11,8 @@ from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_setting no_cache = 1 -expected_keys = ('redirect_flow_id', 'reference_doctype', 'reference_docname') +expected_keys = ("redirect_flow_id", "reference_doctype", "reference_docname") + def get_context(context): context.no_cache = 1 @@ -22,11 +23,14 @@ def get_context(context): context[key] = frappe.form_dict[key] else: - frappe.redirect_to_message(_('Some information is missing'), - _('Looks like someone sent you to an incomplete URL. Please ask them to look into it.')) + frappe.redirect_to_message( + _("Some information is missing"), + _("Looks like someone sent you to an incomplete URL. Please ask them to look into it."), + ) frappe.local.flags.redirect_location = frappe.local.response.location raise frappe.Redirect + @frappe.whitelist(allow_guest=True) def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): @@ -34,15 +38,15 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): try: redirect_flow = client.redirect_flows.complete( - redirect_flow_id, - params={ - "session_token": frappe.session.user - }) + redirect_flow_id, params={"session_token": frappe.session.user} + ) confirmation_url = redirect_flow.confirmation_url - gocardless_success_page = frappe.get_hooks('gocardless_success_page') + gocardless_success_page = frappe.get_hooks("gocardless_success_page") if gocardless_success_page: - confirmation_url = frappe.get_attr(gocardless_success_page[-1])(reference_doctype, reference_docname) + confirmation_url = frappe.get_attr(gocardless_success_page[-1])( + reference_doctype, reference_docname + ) data = { "mandate": redirect_flow.links.mandate, @@ -50,7 +54,7 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): "redirect_to": confirmation_url, "redirect_message": "Mandate successfully created", "reference_doctype": reference_doctype, - "reference_docname": reference_docname + "reference_docname": reference_docname, } try: @@ -65,29 +69,38 @@ def confirm_payment(redirect_flow_id, reference_doctype, reference_docname): except Exception as e: frappe.log_error(e, "GoCardless Payment Error") - return {"redirect_to": '/integrations/payment-failed'} + return {"redirect_to": "/integrations/payment-failed"} def create_mandate(data): data = frappe._dict(data) frappe.logger().debug(data) - mandate = data.get('mandate') + mandate = data.get("mandate") if frappe.db.exists("GoCardless Mandate", mandate): return else: - reference_doc = frappe.db.get_value(data.get('reference_doctype'), data.get('reference_docname'), ["reference_doctype", "reference_name"], as_dict=1) - erpnext_customer = frappe.db.get_value(reference_doc.reference_doctype, reference_doc.reference_name, ["customer_name"], as_dict=1) + reference_doc = frappe.db.get_value( + data.get("reference_doctype"), + data.get("reference_docname"), + ["reference_doctype", "reference_name"], + as_dict=1, + ) + erpnext_customer = frappe.db.get_value( + reference_doc.reference_doctype, reference_doc.reference_name, ["customer_name"], as_dict=1 + ) try: - frappe.get_doc({ - "doctype": "GoCardless Mandate", - "mandate": mandate, - "customer": erpnext_customer.customer_name, - "gocardless_customer": data.get('customer') - }).insert(ignore_permissions=True) + frappe.get_doc( + { + "doctype": "GoCardless Mandate", + "mandate": mandate, + "customer": erpnext_customer.customer_name, + "gocardless_customer": data.get("customer"), + } + ).insert(ignore_permissions=True) except Exception: frappe.log_error(frappe.get_traceback()) diff --git a/erpnext/templates/pages/material_request_info.py b/erpnext/templates/pages/material_request_info.py index 65d4427e118..301ca01cfce 100644 --- a/erpnext/templates/pages/material_request_info.py +++ b/erpnext/templates/pages/material_request_info.py @@ -20,17 +20,23 @@ def get_context(context): if not frappe.has_website_permission(context.doc): frappe.throw(_("Not Permitted"), frappe.PermissionError) - default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value") + default_print_format = frappe.db.get_value( + "Property Setter", + dict(property="default_print_format", doc_type=frappe.form_dict.doctype), + "value", + ) if default_print_format: context.print_format = default_print_format else: context.print_format = "Standard" context.doc.items = get_more_items_info(context.doc.items, context.doc.name) + def get_more_items_info(items, material_request): for item in items: - item.customer_provided = frappe.get_value('Item', item.item_code, 'is_customer_provided_item') - item.work_orders = frappe.db.sql(""" + item.customer_provided = frappe.get_value("Item", item.item_code, "is_customer_provided_item") + item.work_orders = frappe.db.sql( + """ select wo.name, wo.status, wo_item.consumed_qty from @@ -41,9 +47,16 @@ def get_more_items_info(items, material_request): and wo_item.parent=wo.name and wo.status not in ('Completed', 'Cancelled', 'Stopped') order by - wo.name asc""", item.item_code, as_dict=1) - item.delivered_qty = flt(frappe.db.sql("""select sum(transfer_qty) + wo.name asc""", + item.item_code, + as_dict=1, + ) + item.delivered_qty = flt( + frappe.db.sql( + """select sum(transfer_qty) from `tabStock Entry Detail` where material_request = %s and item_code = %s and docstatus = 1""", - (material_request, item.item_code))[0][0]) + (material_request, item.item_code), + )[0][0] + ) return items diff --git a/erpnext/templates/pages/non_profit/join_chapter.py b/erpnext/templates/pages/non_profit/join_chapter.py index a5d03478866..a451c87e9f1 100644 --- a/erpnext/templates/pages/non_profit/join_chapter.py +++ b/erpnext/templates/pages/non_profit/join_chapter.py @@ -1,23 +1,25 @@ - import frappe def get_context(context): context.no_cache = True - chapter = frappe.get_doc('Chapter', frappe.form_dict.name) - if frappe.session.user!='Guest': + chapter = frappe.get_doc("Chapter", frappe.form_dict.name) + if frappe.session.user != "Guest": if frappe.session.user in [d.user for d in chapter.members if d.enabled == 1]: context.already_member = True else: - if frappe.request.method=='GET': + if frappe.request.method == "GET": pass - elif frappe.request.method=='POST': - chapter.append('members', dict( - user=frappe.session.user, - introduction=frappe.form_dict.introduction, - website_url=frappe.form_dict.website_url, - enabled=1 - )) + elif frappe.request.method == "POST": + chapter.append( + "members", + dict( + user=frappe.session.user, + introduction=frappe.form_dict.introduction, + website_url=frappe.form_dict.website_url, + enabled=1, + ), + ) chapter.save(ignore_permissions=1) frappe.db.commit() diff --git a/erpnext/templates/pages/non_profit/leave_chapter.py b/erpnext/templates/pages/non_profit/leave_chapter.py index 6aa875876a2..e3fd33ea15e 100644 --- a/erpnext/templates/pages/non_profit/leave_chapter.py +++ b/erpnext/templates/pages/non_profit/leave_chapter.py @@ -1,9 +1,8 @@ - import frappe def get_context(context): context.no_cache = True - chapter = frappe.get_doc('Chapter', frappe.form_dict.name) + chapter = frappe.get_doc("Chapter", frappe.form_dict.name) context.member_deleted = True context.chapter = chapter diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 4f83d6c57d9..69f9f56aa4a 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -20,12 +20,17 @@ def get_context(context): context.parents = frappe.form_dict.parents context.title = frappe.form_dict.name - context.payment_ref = frappe.db.get_value("Payment Request", - {"reference_name": frappe.form_dict.name}, "name") + context.payment_ref = frappe.db.get_value( + "Payment Request", {"reference_name": frappe.form_dict.name}, "name" + ) context.enabled_checkout = frappe.get_doc("E Commerce Settings").enable_checkout - default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value") + default_print_format = frappe.db.get_value( + "Property Setter", + dict(property="default_print_format", doc_type=frappe.form_dict.doctype), + "value", + ) if default_print_format: context.print_format = default_print_format else: @@ -35,15 +40,23 @@ def get_context(context): frappe.throw(_("Not Permitted"), frappe.PermissionError) # check for the loyalty program of the customer - customer_loyalty_program = frappe.db.get_value("Customer", context.doc.customer, "loyalty_program") + customer_loyalty_program = frappe.db.get_value( + "Customer", context.doc.customer, "loyalty_program" + ) if customer_loyalty_program: from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, ) - loyalty_program_details = get_loyalty_program_details_with_points(context.doc.customer, customer_loyalty_program) + + loyalty_program_details = get_loyalty_program_details_with_points( + context.doc.customer, customer_loyalty_program + ) context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) + def get_attachments(dt, dn): - return frappe.get_all("File", - fields=["name", "file_name", "file_url", "is_private"], - filters = {"attached_to_name": dn, "attached_to_doctype": dt, "is_private":0}) + return frappe.get_all( + "File", + fields=["name", "file_name", "file_url", "is_private"], + filters={"attached_to_name": dn, "attached_to_doctype": dt, "is_private": 0}, + ) diff --git a/erpnext/templates/pages/partners.py b/erpnext/templates/pages/partners.py index e4043ea8b96..8a49504ff04 100644 --- a/erpnext/templates/pages/partners.py +++ b/erpnext/templates/pages/partners.py @@ -6,11 +6,12 @@ import frappe page_title = "Partners" -def get_context(context): - partners = frappe.db.sql("""select * from `tabSales Partner` - where show_in_website=1 order by name asc""", as_dict=True) - return { - "partners": partners, - "title": page_title - } +def get_context(context): + partners = frappe.db.sql( + """select * from `tabSales Partner` + where show_in_website=1 order by name asc""", + as_dict=True, + ) + + return {"partners": partners, "title": page_title} diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index a2351a71804..e5e00ef5a1e 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -17,9 +17,11 @@ from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_htm no_cache = 1 + def get_context(context): context.show_search = True + @frappe.whitelist(allow_guest=True) def get_product_list(search=None, start=0, limit=12): data = get_product_data(search, start, limit) @@ -29,6 +31,7 @@ def get_product_list(search=None, start=0, limit=12): return [get_item_for_list_in_html(r) for r in data] + def get_product_data(search=None, start=0, limit=12): # limit = 12 because we show 12 items in the grid view # base query @@ -53,9 +56,8 @@ def get_product_data(search=None, start=0, limit=12): # order by query += """ ORDER BY ranking desc, modified desc limit %s, %s""" % (cint(start), cint(limit)) - return frappe.db.sql(query, { - "search": search - }, as_dict=1) + return frappe.db.sql(query, {"search": search}, as_dict=1) + @frappe.whitelist(allow_guest=True) def search(query): @@ -64,9 +66,10 @@ def search(query): return { "product_results": product_results.get("results") or [], - "category_results": category_results.get("results") or [] + "category_results": category_results.get("results") or [], } + @frappe.whitelist(allow_guest=True) def product_search(query, limit=10, fuzzy_search=True): search_results = {"from_redisearch": True, "results": []} @@ -86,9 +89,7 @@ def product_search(query, limit=10, fuzzy_search=True): ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = ac.get_suggestions( - query, - num=limit, - fuzzy= fuzzy_search and len(query) > 3 # Fuzzy on length < 3 can be real slow + query, num=limit, fuzzy=fuzzy_search and len(query) > 3 # Fuzzy on length < 3 can be real slow ) # Build a query @@ -100,17 +101,22 @@ def product_search(query, limit=10, fuzzy_search=True): q = Query(query_string) results = client.search(q) - search_results['results'] = list(map(convert_to_dict, results.docs)) - search_results['results'] = sorted(search_results['results'], key=lambda k: frappe.utils.cint(k['ranking']), reverse=True) + search_results["results"] = list(map(convert_to_dict, results.docs)) + search_results["results"] = sorted( + search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True + ) return search_results + def clean_up_query(query): - return ''.join(c for c in query if c.isalnum() or c.isspace()) + return "".join(c for c in query if c.isalnum() or c.isspace()) + def convert_to_dict(redis_search_doc): return redis_search_doc.__dict__ + @frappe.whitelist(allow_guest=True) def get_category_suggestions(query): search_results = {"results": []} @@ -119,13 +125,10 @@ def get_category_suggestions(query): # Redisearch module not loaded, query db categories = frappe.db.get_all( "Item Group", - filters={ - "name": ["like", "%{0}%".format(query)], - "show_in_website": 1 - }, - fields=["name", "route"] + filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1}, + fields=["name", "route"], ) - search_results['results'] = categories + search_results["results"] = categories return search_results if not query: @@ -134,6 +137,6 @@ def get_category_suggestions(query): ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) suggestions = ac.get_suggestions(query, num=10) - search_results['results'] = [s.string for s in suggestions] + search_results["results"] = [s.string for s in suggestions] return search_results diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index da3641eeb10..7cab288b98b 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -6,21 +6,28 @@ import frappe def get_context(context): - project_user = frappe.db.get_value("Project User", {"parent": frappe.form_dict.project, "user": frappe.session.user} , ["user", "view_attachments"], as_dict= True) - if frappe.session.user != 'Administrator' and (not project_user or frappe.session.user == 'Guest'): + project_user = frappe.db.get_value( + "Project User", + {"parent": frappe.form_dict.project, "user": frappe.session.user}, + ["user", "view_attachments"], + as_dict=True, + ) + if frappe.session.user != "Administrator" and ( + not project_user or frappe.session.user == "Guest" + ): raise frappe.PermissionError context.no_cache = 1 context.show_sidebar = True - project = frappe.get_doc('Project', frappe.form_dict.project) + project = frappe.get_doc("Project", frappe.form_dict.project) - project.has_permission('read') + project.has_permission("read") - project.tasks = get_tasks(project.name, start=0, item_status='open', - search=frappe.form_dict.get("search")) + project.tasks = get_tasks( + project.name, start=0, item_status="open", search=frappe.form_dict.get("search") + ) - project.timesheets = get_timesheets(project.name, start=0, - search=frappe.form_dict.get("search")) + project.timesheets = get_timesheets(project.name, start=0, search=frappe.form_dict.get("search")) if project_user and project_user.view_attachments: project.attachments = get_attachments(project.name) @@ -33,10 +40,23 @@ def get_tasks(project, start=0, search=None, item_status=None): if search: filters["subject"] = ("like", "%{0}%".format(search)) # if item_status: -# filters["status"] = item_status - tasks = frappe.get_all("Task", filters=filters, - fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"], - limit_start=start, limit_page_length=10) + # filters["status"] = item_status + tasks = frappe.get_all( + "Task", + filters=filters, + fields=[ + "name", + "subject", + "status", + "modified", + "_assign", + "exp_end_date", + "is_group", + "parent_task", + ], + limit_start=start, + limit_page_length=10, + ) task_nest = [] for task in tasks: if task.is_group: @@ -46,37 +66,60 @@ def get_tasks(project, start=0, search=None, item_status=None): task_nest.append(task) return list(filter(lambda x: not x.parent_task, tasks)) + @frappe.whitelist() def get_task_html(project, start=0, item_status=None): - return frappe.render_template("erpnext/templates/includes/projects/project_tasks.html", - {"doc": { - "name": project, - "project_name": project, - "tasks": get_tasks(project, start, item_status=item_status)} - }, is_path=True) + return frappe.render_template( + "erpnext/templates/includes/projects/project_tasks.html", + { + "doc": { + "name": project, + "project_name": project, + "tasks": get_tasks(project, start, item_status=item_status), + } + }, + is_path=True, + ) + def get_timesheets(project, start=0, search=None): filters = {"project": project} if search: filters["activity_type"] = ("like", "%{0}%".format(search)) - timesheets = frappe.get_all('Timesheet Detail', filters=filters, - fields=['project','activity_type','from_time','to_time','parent'], - limit_start=start, limit_page_length=10) + timesheets = frappe.get_all( + "Timesheet Detail", + filters=filters, + fields=["project", "activity_type", "from_time", "to_time", "parent"], + limit_start=start, + limit_page_length=10, + ) for timesheet in timesheets: - info = frappe.get_all('Timesheet', filters={"name": timesheet.parent}, - fields=['name','status','modified','modified_by'], - limit_start=start, limit_page_length=10) + info = frappe.get_all( + "Timesheet", + filters={"name": timesheet.parent}, + fields=["name", "status", "modified", "modified_by"], + limit_start=start, + limit_page_length=10, + ) if len(info): timesheet.update(info[0]) return timesheets + @frappe.whitelist() def get_timesheet_html(project, start=0): - return frappe.render_template("erpnext/templates/includes/projects/project_timesheets.html", - {"doc": {"timesheets": get_timesheets(project, start)}}, is_path=True) + return frappe.render_template( + "erpnext/templates/includes/projects/project_timesheets.html", + {"doc": {"timesheets": get_timesheets(project, start)}}, + is_path=True, + ) + def get_attachments(project): - return frappe.get_all('File', filters= {"attached_to_name": project, "attached_to_doctype": 'Project', "is_private":0}, - fields=['file_name','file_url', 'file_size']) + return frappe.get_all( + "File", + filters={"attached_to_name": project, "attached_to_doctype": "Project", "is_private": 0}, + fields=["file_name", "file_url", "file_size"], + ) diff --git a/erpnext/templates/pages/regional/india/update_gstin.py b/erpnext/templates/pages/regional/india/update_gstin.py index b65f0a6e739..f682b1904f2 100644 --- a/erpnext/templates/pages/regional/india/update_gstin.py +++ b/erpnext/templates/pages/regional/india/update_gstin.py @@ -1,4 +1,3 @@ - import frappe from six import iteritems @@ -13,12 +12,12 @@ def get_context(context): except frappe.ValidationError: context.invalid_gstin = 1 - party_type = 'Customer' - party_name = frappe.db.get_value('Customer', party) + party_type = "Customer" + party_name = frappe.db.get_value("Customer", party) if not party_name: - party_type = 'Supplier' - party_name = frappe.db.get_value('Supplier', party) + party_type = "Supplier" + party_name = frappe.db.get_value("Supplier", party) if not party_name: context.not_found = 1 @@ -31,10 +30,10 @@ def get_context(context): def update_gstin(context): dirty = False for key, value in iteritems(frappe.form_dict): - if key != 'party': - address_name = frappe.get_value('Address', key) + if key != "party": + address_name = frappe.get_value("Address", key) if address_name: - address = frappe.get_doc('Address', address_name) + address = frappe.get_doc("Address", address_name) address.gstin = value.upper() address.save(ignore_permissions=True) dirty = True diff --git a/erpnext/templates/pages/rfq.py b/erpnext/templates/pages/rfq.py index 0afd46cac90..4b836424911 100644 --- a/erpnext/templates/pages/rfq.py +++ b/erpnext/templates/pages/rfq.py @@ -20,40 +20,59 @@ def get_context(context): update_supplier_details(context) context["title"] = frappe.form_dict.name + def get_supplier(): doctype = frappe.form_dict.doctype - parties_doctype = 'Request for Quotation Supplier' if doctype == 'Request for Quotation' else doctype + parties_doctype = ( + "Request for Quotation Supplier" if doctype == "Request for Quotation" else doctype + ) customers, suppliers = get_customers_suppliers(parties_doctype, frappe.session.user) - return suppliers[0] if suppliers else '' + return suppliers[0] if suppliers else "" + def check_supplier_has_docname_access(supplier): status = True - if frappe.form_dict.name not in frappe.db.sql_list("""select parent from `tabRequest for Quotation Supplier` - where supplier = %s""", (supplier,)): + if frappe.form_dict.name not in frappe.db.sql_list( + """select parent from `tabRequest for Quotation Supplier` + where supplier = %s""", + (supplier,), + ): status = False return status + def unauthorized_user(supplier): status = check_supplier_has_docname_access(supplier) or False if status == False: frappe.throw(_("Not Permitted"), frappe.PermissionError) + def update_supplier_details(context): supplier_doc = frappe.get_doc("Supplier", context.doc.supplier) - context.doc.currency = supplier_doc.default_currency or frappe.get_cached_value('Company', context.doc.company, "default_currency") - context.doc.currency_symbol = frappe.db.get_value("Currency", context.doc.currency, "symbol", cache=True) - context.doc.number_format = frappe.db.get_value("Currency", context.doc.currency, "number_format", cache=True) - context.doc.buying_price_list = supplier_doc.default_price_list or '' + context.doc.currency = supplier_doc.default_currency or frappe.get_cached_value( + "Company", context.doc.company, "default_currency" + ) + context.doc.currency_symbol = frappe.db.get_value( + "Currency", context.doc.currency, "symbol", cache=True + ) + context.doc.number_format = frappe.db.get_value( + "Currency", context.doc.currency, "number_format", cache=True + ) + context.doc.buying_price_list = supplier_doc.default_price_list or "" + def get_link_quotation(supplier, rfq): - quotation = frappe.db.sql(""" select distinct `tabSupplier Quotation Item`.parent as name, + quotation = frappe.db.sql( + """ select distinct `tabSupplier Quotation Item`.parent as name, `tabSupplier Quotation`.status, `tabSupplier Quotation`.transaction_date from `tabSupplier Quotation Item`, `tabSupplier Quotation` where `tabSupplier Quotation`.docstatus < 2 and `tabSupplier Quotation Item`.request_for_quotation =%(name)s and `tabSupplier Quotation Item`.parent = `tabSupplier Quotation`.name and `tabSupplier Quotation`.supplier = %(supplier)s order by `tabSupplier Quotation`.creation desc""", - {'name': rfq, 'supplier': supplier}, as_dict=1) + {"name": rfq, "supplier": supplier}, + as_dict=1, + ) for data in quotation: data.transaction_date = formatdate(data.transaction_date) diff --git a/erpnext/templates/pages/search_help.py b/erpnext/templates/pages/search_help.py index c8854d74901..b1b71f38a20 100644 --- a/erpnext/templates/pages/search_help.py +++ b/erpnext/templates/pages/search_help.py @@ -1,4 +1,3 @@ - import frappe import requests from frappe import _ @@ -13,17 +12,18 @@ def get_context(context): context.no_cache = 1 if frappe.form_dict.q: query = str(utils.escape(sanitize_html(frappe.form_dict.q))) - context.title = _('Help Results for') + context.title = _("Help Results for") context.query = query - context.route = '/search_help' + context.route = "/search_help" d = frappe._dict() d.results_sections = get_help_results_sections(query) context.update(d) else: - context.title = _('Docs Search') + context.title = _("Docs Search") -@frappe.whitelist(allow_guest = True) + +@frappe.whitelist(allow_guest=True) def get_help_results_sections(text): out = [] settings = frappe.get_doc("Support Settings", "Support Settings") @@ -42,63 +42,72 @@ def get_help_results_sections(text): if results: # Add section - out.append({ - "title": api.source_name, - "results": results - }) + out.append({"title": api.source_name, "results": results}) return out + def get_response(api, text): - response = requests.get(api.base_url + '/' + api.query_route, data={ - api.search_term_param_name: text - }) + response = requests.get( + api.base_url + "/" + api.query_route, data={api.search_term_param_name: text} + ) response.raise_for_status() return response.json() + def get_topics_data(api, response_json): if not response_json: response_json = {} - topics_data = {} # it will actually be an array - key_list = api.response_result_key_path.split(',') + topics_data = {} # it will actually be an array + key_list = api.response_result_key_path.split(",") for key in key_list: topics_data = response_json.get(key) if not topics_data else topics_data.get(key) return topics_data or [] + def prepare_api_results(api, topics_data): if not topics_data: topics_data = [] results = [] for topic in topics_data: - route = api.base_url + '/' + (api.post_route + '/' if api.post_route else "") - for key in api.post_route_key_list.split(','): + route = api.base_url + "/" + (api.post_route + "/" if api.post_route else "") + for key in api.post_route_key_list.split(","): route += text_type(topic[key]) - results.append(frappe._dict({ - 'title': topic[api.post_title_key], - 'preview': html2text(topic[api.post_description_key]), - 'route': route - })) + results.append( + frappe._dict( + { + "title": topic[api.post_title_key], + "preview": html2text(topic[api.post_description_key]), + "route": route, + } + ) + ) return results[:5] + def prepare_doctype_results(api, raw): results = [] for r in raw: prepared_result = {} - parts = r["content"].split(' ||| ') + parts = r["content"].split(" ||| ") for part in parts: - pair = part.split(' : ', 1) + pair = part.split(" : ", 1) prepared_result[pair[0]] = pair[1] - results.append(frappe._dict({ - 'title': prepared_result[api.result_title_field], - 'preview': prepared_result[api.result_preview_field], - 'route': prepared_result[api.result_route_field] - })) + results.append( + frappe._dict( + { + "title": prepared_result[api.result_title_field], + "preview": prepared_result[api.result_preview_field], + "route": prepared_result[api.result_route_field], + } + ) + ) return results diff --git a/erpnext/templates/pages/task_info.py b/erpnext/templates/pages/task_info.py index 1d809c4bd72..66b775a9178 100644 --- a/erpnext/templates/pages/task_info.py +++ b/erpnext/templates/pages/task_info.py @@ -1,13 +1,15 @@ - import frappe def get_context(context): context.no_cache = 1 - task = frappe.get_doc('Task', frappe.form_dict.task) + task = frappe.get_doc("Task", frappe.form_dict.task) - context.comments = frappe.get_all('Communication', filters={'reference_name': task.name, 'comment_type': 'comment'}, - fields=['subject', 'sender_full_name', 'communication_date']) + context.comments = frappe.get_all( + "Communication", + filters={"reference_name": task.name, "comment_type": "comment"}, + fields=["subject", "sender_full_name", "communication_date"], + ) context.doc = task diff --git a/erpnext/templates/pages/timelog_info.py b/erpnext/templates/pages/timelog_info.py index 3bc74013a9e..3f0ec3738b2 100644 --- a/erpnext/templates/pages/timelog_info.py +++ b/erpnext/templates/pages/timelog_info.py @@ -1,10 +1,9 @@ - import frappe def get_context(context): context.no_cache = 1 - timelog = frappe.get_doc('Time Log', frappe.form_dict.timelog) + timelog = frappe.get_doc("Time Log", frappe.form_dict.timelog) context.doc = timelog diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py index 72ee34e157e..d70f27c9d9d 100644 --- a/erpnext/templates/pages/wishlist.py +++ b/erpnext/templates/pages/wishlist.py @@ -23,31 +23,33 @@ def get_context(context): context.settings = settings context.no_cache = 1 + def get_stock_availability(item_code, warehouse): stock_qty = frappe.utils.flt( - frappe.db.get_value("Bin", - { - "item_code": item_code, - "warehouse": warehouse - }, - "actual_qty") + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") ) return bool(stock_qty) + def get_wishlist_items(): if not frappe.db.exists("Wishlist", frappe.session.user): return [] return frappe.db.get_all( "Wishlist Item", - filters={ - "parent": frappe.session.user - }, + filters={"parent": frappe.session.user}, fields=[ - "web_item_name", "item_code", "item_name", - "website_item", "warehouse", - "image", "item_group", "route" - ]) + "web_item_name", + "item_code", + "item_name", + "website_item", + "warehouse", + "image", + "item_group", + "route", + ], + ) + def set_stock_price_details(items, settings, selling_price_list): for item in items: @@ -55,17 +57,15 @@ def set_stock_price_details(items, settings, selling_price_list): item.available = get_stock_availability(item.item_code, item.get("warehouse")) price_details = get_price( - item.item_code, - selling_price_list, - settings.default_customer_group, - settings.company + item.item_code, selling_price_list, settings.default_customer_group, settings.company ) if price_details: - item.formatted_price = price_details.get('formatted_price') - item.formatted_mrp = price_details.get('formatted_mrp') + item.formatted_price = price_details.get("formatted_price") + item.formatted_mrp = price_details.get("formatted_mrp") if item.formatted_mrp: - item.discount = price_details.get('formatted_discount_percent') or \ - price_details.get('formatted_discount_rate') + item.discount = price_details.get("formatted_discount_percent") or price_details.get( + "formatted_discount_rate" + ) - return items \ No newline at end of file + return items diff --git a/erpnext/templates/utils.py b/erpnext/templates/utils.py index 9f46e6a99ed..4295188dc0b 100644 --- a/erpnext/templates/utils.py +++ b/erpnext/templates/utils.py @@ -8,31 +8,35 @@ import frappe @frappe.whitelist(allow_guest=True) def send_message(subject="Website Query", message="", sender="", status="Open"): from frappe.www.contact import send_message as website_send_message + lead = customer = None website_send_message(subject, message, sender) - customer = frappe.db.sql("""select distinct dl.link_name from `tabDynamic Link` dl + customer = frappe.db.sql( + """select distinct dl.link_name from `tabDynamic Link` dl left join `tabContact` c on dl.parent=c.name where dl.link_doctype='Customer' - and c.email_id = %s""", sender) + and c.email_id = %s""", + sender, + ) if not customer: - lead = frappe.db.get_value('Lead', dict(email_id=sender)) + lead = frappe.db.get_value("Lead", dict(email_id=sender)) if not lead: - new_lead = frappe.get_doc(dict( - doctype='Lead', - email_id = sender, - lead_name = sender.split('@')[0].title() - )).insert(ignore_permissions=True) + new_lead = frappe.get_doc( + dict(doctype="Lead", email_id=sender, lead_name=sender.split("@")[0].title()) + ).insert(ignore_permissions=True) - opportunity = frappe.get_doc(dict( - doctype ='Opportunity', - opportunity_from = 'Customer' if customer else 'Lead', - status = 'Open', - title = subject, - contact_email = sender, - to_discuss = message - )) + opportunity = frappe.get_doc( + dict( + doctype="Opportunity", + opportunity_from="Customer" if customer else "Lead", + status="Open", + title=subject, + contact_email=sender, + to_discuss=message, + ) + ) if customer: opportunity.party_name = customer[0][0] @@ -43,15 +47,17 @@ def send_message(subject="Website Query", message="", sender="", status="Open"): opportunity.insert(ignore_permissions=True) - comm = frappe.get_doc({ - "doctype":"Communication", - "subject": subject, - "content": message, - "sender": sender, - "sent_or_received": "Received", - 'reference_doctype': 'Opportunity', - 'reference_name': opportunity.name - }) + comm = frappe.get_doc( + { + "doctype": "Communication", + "subject": subject, + "content": message, + "sender": sender, + "sent_or_received": "Received", + "reference_doctype": "Opportunity", + "reference_name": opportunity.name, + } + ) comm.insert(ignore_permissions=True) return "okay" diff --git a/erpnext/tests/__init__.py b/erpnext/tests/__init__.py index a504340d409..dc37472c4c6 100644 --- a/erpnext/tests/__init__.py +++ b/erpnext/tests/__init__.py @@ -1 +1 @@ -global_test_dependencies = ['User', 'Company', 'Item'] +global_test_dependencies = ["User", "Company", "Item"] diff --git a/erpnext/tests/test_init.py b/erpnext/tests/test_init.py index 89093ae8efd..8bb31ae41f1 100644 --- a/erpnext/tests/test_init.py +++ b/erpnext/tests/test_init.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -6,7 +5,8 @@ from six.moves import range from erpnext import encode_company_abbr -test_records = frappe.get_test_records('Company') +test_records = frappe.get_test_records("Company") + class TestInit(unittest.TestCase): def test_encode_company_abbr(self): @@ -14,23 +14,30 @@ class TestInit(unittest.TestCase): abbr = "NFECT" names = [ - "Warehouse Name", "ERPNext Foundation India", "Gold - Member - {a}".format(a=abbr), - " - {a}".format(a=abbr), "ERPNext - Foundation - India", + "Warehouse Name", + "ERPNext Foundation India", + "Gold - Member - {a}".format(a=abbr), + " - {a}".format(a=abbr), + "ERPNext - Foundation - India", "ERPNext Foundation India - {a}".format(a=abbr), - "No-Space-{a}".format(a=abbr), "- Warehouse" + "No-Space-{a}".format(a=abbr), + "- Warehouse", ] expected_names = [ - "Warehouse Name - {a}".format(a=abbr), "ERPNext Foundation India - {a}".format(a=abbr), - "Gold - Member - {a}".format(a=abbr), " - {a}".format(a=abbr), + "Warehouse Name - {a}".format(a=abbr), + "ERPNext Foundation India - {a}".format(a=abbr), + "Gold - Member - {a}".format(a=abbr), + " - {a}".format(a=abbr), "ERPNext - Foundation - India - {a}".format(a=abbr), - "ERPNext Foundation India - {a}".format(a=abbr), "No-Space-{a} - {a}".format(a=abbr), - "- Warehouse - {a}".format(a=abbr) + "ERPNext Foundation India - {a}".format(a=abbr), + "No-Space-{a} - {a}".format(a=abbr), + "- Warehouse - {a}".format(a=abbr), ] for i in range(len(names)): enc_name = encode_company_abbr(names[i], abbr=abbr) self.assertTrue( enc_name == expected_names[i], - "{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]) + "{enc} is not same as {exp}".format(enc=enc_name, exp=expected_names[i]), ) diff --git a/erpnext/tests/test_notifications.py b/erpnext/tests/test_notifications.py index 669bf6f3d92..0f391956309 100644 --- a/erpnext/tests/test_notifications.py +++ b/erpnext/tests/test_notifications.py @@ -10,9 +10,9 @@ from frappe.desk import notifications class TestNotifications(unittest.TestCase): def test_get_notifications_for_targets(self): - ''' - Test notification config entries for targets as percentages - ''' + """ + Test notification config entries for targets as percentages + """ company = frappe.get_all("Company")[0] frappe.db.set_value("Company", company.name, "monthly_sales_target", 10000) @@ -21,7 +21,7 @@ class TestNotifications(unittest.TestCase): config = notifications.get_notification_config() doc_target_percents = notifications.get_notifications_for_targets(config, {}) - self.assertEqual(doc_target_percents['Company'][company.name], 10) + self.assertEqual(doc_target_percents["Company"][company.name], 10) frappe.db.set_value("Company", company.name, "monthly_sales_target", 2000) frappe.db.set_value("Company", company.name, "total_monthly_sales", 0) @@ -29,4 +29,4 @@ class TestNotifications(unittest.TestCase): config = notifications.get_notification_config() doc_target_percents = notifications.get_notifications_for_targets(config, {}) - self.assertEqual(doc_target_percents['Company'][company.name], 0) + self.assertEqual(doc_target_percents["Company"][company.name], 0) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index 38f2c16d939..7267d4a1d5e 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -14,11 +14,11 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestPointOfSale(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - frappe.db.savepoint('before_test_point_of_sale') + frappe.db.savepoint("before_test_point_of_sale") @classmethod def tearDownClass(cls) -> None: - frappe.db.rollback(save_point='before_test_point_of_sale') + frappe.db.rollback(save_point="before_test_point_of_sale") def test_item_search(self): """ diff --git a/erpnext/tests/test_regional.py b/erpnext/tests/test_regional.py index b232d69dbb3..abeecee4e74 100644 --- a/erpnext/tests/test_regional.py +++ b/erpnext/tests/test_regional.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -8,15 +7,16 @@ import erpnext @erpnext.allow_regional def test_method(): - return 'original' + return "original" + class TestInit(unittest.TestCase): def test_regional_overrides(self): - frappe.flags.country = 'India' - self.assertEqual(test_method(), 'overridden') + frappe.flags.country = "India" + self.assertEqual(test_method(), "overridden") - frappe.flags.country = 'Maldives' - self.assertEqual(test_method(), 'original') + frappe.flags.country = "Maldives" + self.assertEqual(test_method(), "original") - frappe.flags.country = 'France' - self.assertEqual(test_method(), 'overridden') + frappe.flags.country = "France" + self.assertEqual(test_method(), "overridden") diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py index 3594cfee335..ffe9a5ae541 100644 --- a/erpnext/tests/test_search.py +++ b/erpnext/tests/test_search.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -9,12 +8,11 @@ class TestSearch(unittest.TestCase): # Search for the word "cond", part of the word "conduire" (Lead) in french. def test_contact_search_in_foreign_language(self): try: - frappe.local.lang = 'fr' - output = filter_dynamic_link_doctypes("DocType", "cond", "name", 0, 20, { - 'fieldtype': 'HTML', - 'fieldname': 'contact_html' - }) - result = [['found' for x in y if x=="Lead"] for y in output] - self.assertTrue(['found'] in result) + frappe.local.lang = "fr" + output = filter_dynamic_link_doctypes( + "DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"} + ) + result = [["found" for x in y if x == "Lead"] for y in output] + self.assertTrue(["found"] in result) finally: - frappe.local.lang = 'en' + frappe.local.lang = "en" diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index 170daa96733..07291e851b5 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -1,4 +1,3 @@ - import copy import unittest from collections import defaultdict @@ -26,35 +25,43 @@ class TestSubcontracting(unittest.TestCase): make_bom_for_subcontracted_items() def test_po_with_bom(self): - ''' - - Set backflush based on BOM - - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times. - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Create purchase receipt against the PO and check serial nos and batch no. - ''' + """ + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Create purchase receipt against the PO and check serial nos and batch no. + """ - set_backflush_based_on('BOM') - item_code = 'Subcontracted Item SA1' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 5, 'rate': 100}, - {'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 6, 'rate': 100}] + set_backflush_based_on("BOM") + item_code = "Subcontracted Item SA1" + items = [ + {"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 5, "rate": 100}, + {"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 6, "rate": 100}, + ] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 5}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 5}, - {'item_code': 'Subcontracted SRM Item 1', 'qty': 6}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 6}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 6} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 5}, + {"item_code": "Subcontracted SRM Item 2", "qty": 5}, + {"item_code": "Subcontracted SRM Item 3", "qty": 5}, + {"item_code": "Subcontracted SRM Item 1", "qty": 6}, + {"item_code": "Subcontracted SRM Item 2", "qty": 6}, + {"item_code": "Subcontracted SRM Item 3", "qty": 6}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.submit() @@ -62,43 +69,58 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) - for field in ['qty', 'serial_no', 'batch_no']: + for field in ["qty", "serial_no", "batch_no"]: if value.get(field): transfer, consumed = (transferred_detais.get(field), value.get(field)) - if field == 'serial_no': + if field == "serial_no": transfer, consumed = (sorted(transfer), sorted(consumed)) self.assertEqual(transfer, consumed) def test_po_with_material_transfer(self): - ''' - - Set backflush based on Material Transfer - - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5. - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. - - Create partial purchase receipt against the PO and check serial nos and batch no. - ''' + """ + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. + - Create partial purchase receipt against the PO and check serial nos and batch no. + """ - set_backflush_based_on('Material Transferred for Subcontract') - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA1', 'qty': 5, 'rate': 100}, - {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA5', 'qty': 6, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA1", + "qty": 5, + "rate": 100, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA5", + "qty": 6, + "rate": 100, + }, + ] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'}, - {'item_code': 'Subcontracted SRM Item 5', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'}, - {'item_code': 'Subcontracted SRM Item 4', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 2", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 3", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 5", "qty": 6, "main_item_code": "Subcontracted Item SA5"}, + {"item_code": "Subcontracted SRM Item 4", "qty": 6, "main_item_code": "Subcontracted Item SA5"}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name - make_stock_transfer_entry(po_no = po.name, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details) + ) pr1 = make_purchase_receipt(po.name) pr1.remove(pr1.items[1]) @@ -107,7 +129,7 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) - for field in ['qty', 'serial_no', 'batch_no']: + for field in ["qty", "serial_no", "batch_no"]: if value.get(field): self.assertEqual(value.get(field), transferred_detais.get(field)) @@ -117,36 +139,51 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr2).items(): transferred_detais = itemwise_details.get(key) - for field in ['qty', 'serial_no', 'batch_no']: + for field in ["qty", "serial_no", "batch_no"]: if value.get(field): self.assertEqual(value.get(field), transferred_detais.get(field)) def test_subcontract_with_same_components_different_fg(self): - ''' - - Set backflush based on Material Transfer - - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3. - - Transfer the components from Stores to Supplier warehouse with serial nos. - - Transfer extra qty of components for the item Subcontracted Item SA2. - - Create partial purchase receipt against the PO and check serial nos and batch no. - ''' + """ + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of components for the item Subcontracted Item SA2. + - Create partial purchase receipt against the PO and check serial nos and batch no. + """ - set_backflush_based_on('Material Transferred for Subcontract') - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}, - {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA3', 'qty': 6, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA2", + "qty": 5, + "rate": 100, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA3", + "qty": 6, + "rate": 100, + }, + ] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA3'} + rm_items = [ + {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA2"}, + {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA3"}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name + d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name - make_stock_transfer_entry(po_no = po.name, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details) + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 3 @@ -156,7 +193,7 @@ class TestSubcontracting(unittest.TestCase): for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 4) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:4])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:4])) pr2 = make_purchase_receipt(po.name) pr2.items[0].qty = 2 @@ -167,7 +204,7 @@ class TestSubcontracting(unittest.TestCase): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 2) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[4:6])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[4:6])) pr3 = make_purchase_receipt(po.name) pr3.submit() @@ -175,85 +212,104 @@ class TestSubcontracting(unittest.TestCase): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 6) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[6:12])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12])) def test_return_non_consumed_materials(self): - ''' - - Set backflush based on Material Transfer - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the components from Stores to Supplier warehouse with serial nos. - - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. - - Create purchase receipt for full qty against the PO and change the qty of raw material. - - After that return the non consumed material back to the store from supplier's warehouse. - ''' + """ + - Set backflush based on Material Transfer + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. + - Create purchase receipt for full qty against the PO and change the qty of raw material. + - After that return the non consumed material back to the store from supplier's warehouse. + """ - set_backflush_based_on('Material Transferred for Subcontract') - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}] + set_backflush_based_on("Material Transferred for Subcontract") + items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Item SA2", + "qty": 5, + "rate": 100, + } + ] + rm_items = [ + {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA2"} + ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details) + ) pr1 = make_purchase_receipt(po.name) pr1.save() pr1.supplied_items[0].consumed_qty = 5 - pr1.supplied_items[0].serial_no = '\n'.join(sorted( - itemwise_details.get('Subcontracted SRM Item 2').get('serial_no')[0:5] - )) + pr1.supplied_items[0].serial_no = "\n".join( + sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5]) + ) pr1.submit() for key, value in get_supplied_items(pr1).items(): transferred_detais = itemwise_details.get(key) self.assertEqual(value.qty, 5) - self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:5])) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5])) po.load_from_db() self.assertEqual(po.supplied_items[0].consumed_qty, 5) doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items]) self.assertEqual(doc.items[0].qty, 1) - self.assertEqual(doc.items[0].s_warehouse, '_Test Warehouse 1 - _TC') - self.assertEqual(doc.items[0].t_warehouse, '_Test Warehouse - _TC') - self.assertEqual(get_serial_nos(doc.items[0].serial_no), - itemwise_details.get(doc.items[0].item_code)['serial_no'][5:6]) + self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC") + self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC") + self.assertEqual( + get_serial_nos(doc.items[0].serial_no), + itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6], + ) def test_item_with_batch_based_on_bom(self): - ''' - - Set backflush based on BOM - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - ''' + """ + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + """ - set_backflush_based_on('BOM') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("BOM") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 1}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 2 @@ -282,37 +338,43 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, 2) def test_item_with_batch_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches with extra 2 qty for the batched item. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 2 @@ -321,7 +383,7 @@ class TestSubcontracting(unittest.TestCase): pr1.submit() for key, value in get_supplied_items(pr1).items(): - qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + qty = 4 if key != "Subcontracted SRM Item 3" else 6 self.assertEqual(value.qty, qty) pr1 = make_purchase_receipt(po.name) @@ -342,30 +404,35 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, 2) def test_partial_transfer_serial_no_components_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the partial components from Stores to Supplier warehouse with serial nos. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with serial nos. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA2' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA2" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 5 @@ -378,7 +445,9 @@ class TestSubcontracting(unittest.TestCase): pr1.load_from_db() pr1.supplied_items[0].consumed_qty = 5 - pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.supplied_items[0].serial_no = "\n".join( + itemwise_details[pr1.supplied_items[0].rm_item_code]["serial_no"] + ) pr1.save() pr1.submit() @@ -389,10 +458,14 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.submit() @@ -403,67 +476,77 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) def test_incorrect_serial_no_components_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the serialized componenets to the supplier. - - Create purchase receipt and change the serial no which is not transferred. - - System should throw the error and not allowed to save the purchase receipt. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the serialized componenets to the supplier. + - Create purchase receipt and change the serial no which is not transferred. + - System should throw the error and not allowed to save the purchase receipt. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA2' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA2" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 10}] + rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 10}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.save() - pr1.supplied_items[0].serial_no = 'ABCD' + pr1.supplied_items[0].serial_no = "ABCD" self.assertRaises(frappe.ValidationError, pr1.save) pr1.delete() def test_partial_transfer_batch_based_on_material_transfer(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA6. - - Transfer the partial components from Stores to Supplier warehouse with batch. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with batch. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA6' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA6" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 3", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.items[0].qty = 5 pr1.save() - transferred_batch_no = '' + transferred_batch_no = "" for key, value in get_supplied_items(pr1).items(): details = itemwise_details.get(key) self.assertEqual(value.qty, 3) @@ -483,10 +566,14 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_receipt(po.name) pr1.submit() @@ -496,55 +583,60 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.qty, details.qty) self.assertEqual(value.batch_no, details.batch_no) - def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches with extra 2 qty for the batched item. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + - In the first purchase receipt the batched raw materials will be consumed 2 extra qty. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" add_second_row_in_pr(pr1) pr1.save() pr1.submit() for key, value in get_supplied_items(pr1).items(): - qty = 4 if key != 'Subcontracted SRM Item 3' else 6 + qty = 4 if key != "Subcontracted SRM Item 3" else 6 self.assertEqual(value.qty, qty) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.items[0].qty = 2 add_second_row_in_pr(pr1) pr1.save() @@ -556,43 +648,50 @@ class TestSubcontracting(unittest.TestCase): pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() pr1.submit() for key, value in get_supplied_items(pr1).items(): self.assertEqual(value.qty, 2) - def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA2. - - Transfer the partial components from Stores to Supplier warehouse with serial nos. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with serial nos. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice( + self, + ): + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA2' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA2" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 5 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() for key, value in get_supplied_items(pr1).items(): @@ -602,7 +701,9 @@ class TestSubcontracting(unittest.TestCase): pr1.load_from_db() pr1.supplied_items[0].consumed_qty = 5 - pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no']) + pr1.supplied_items[0].serial_no = "\n".join( + itemwise_details[pr1.supplied_items[0].rm_item_code]["serial_no"] + ) pr1.save() pr1.submit() @@ -613,14 +714,18 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.submit() for key, value in get_supplied_items(pr1).items(): @@ -629,38 +734,43 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self): - ''' - - Set backflush based on Material Transferred for Subcontract - - Create subcontracted PO for the item Subcontracted Item SA6. - - Transfer the partial components from Stores to Supplier warehouse with batch. - - Create partial purchase receipt against the PO and change the qty manually. - - Transfer the remaining components from Stores to Supplier warehouse with batch. - - Create purchase receipt for remaining qty against the PO and change the qty manually. - ''' + """ + - Set backflush based on Material Transferred for Subcontract + - Create subcontracted PO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial purchase receipt against the PO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create purchase receipt for remaining qty against the PO and change the qty manually. + """ - set_backflush_based_on('Material Transferred for Subcontract') - item_code = 'Subcontracted Item SA6' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "Subcontracted Item SA6" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}] + rm_items = [{"item_code": "Subcontracted SRM Item 3", "qty": 5}] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 5 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() - transferred_batch_no = '' + transferred_batch_no = "" for key, value in get_supplied_items(pr1).items(): details = itemwise_details.get(key) self.assertEqual(value.qty, 3) @@ -680,14 +790,18 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.submit() for key, value in get_supplied_items(pr1).items(): @@ -696,41 +810,47 @@ class TestSubcontracting(unittest.TestCase): self.assertEqual(value.batch_no, details.batch_no) def test_item_with_batch_based_on_bom_for_purchase_invoice(self): - ''' - - Set backflush based on BOM - - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). - - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. - - Transfer the components in multiple batches. - - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. - - Keep the qty as 2 for Subcontracted Item in the purchase receipt. - ''' + """ + - Set backflush based on BOM + - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the purchase receipt. + """ - set_backflush_based_on('BOM') - item_code = 'Subcontracted Item SA4' - items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}] + set_backflush_based_on("BOM") + item_code = "Subcontracted Item SA4" + items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}] - rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 2', 'qty': 10}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}, - {'item_code': 'Subcontracted SRM Item 3', 'qty': 1} + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 10}, + {"item_code": "Subcontracted SRM Item 2", "qty": 10}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 3}, + {"item_code": "Subcontracted SRM Item 3", "qty": 1}, ] itemwise_details = make_stock_in_entry(rm_items=rm_items) - po = create_purchase_order(rm_items = items, is_subcontracted="Yes", - supplier_warehouse="_Test Warehouse 1 - _TC") + po = create_purchase_order( + rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + ) for d in rm_items: - d['po_detail'] = po.items[0].name + d["po_detail"] = po.items[0].name - make_stock_transfer_entry(po_no = po.name, main_item_code=item_code, - rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)) + make_stock_transfer_entry( + po_no=po.name, + main_item_code=item_code, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" add_second_row_in_pr(pr1) pr1.save() pr1.submit() @@ -741,7 +861,7 @@ class TestSubcontracting(unittest.TestCase): pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" add_second_row_in_pr(pr1) pr1.save() pr1.submit() @@ -752,34 +872,50 @@ class TestSubcontracting(unittest.TestCase): pr1 = make_purchase_invoice(po.name) pr1.update_stock = 1 pr1.items[0].qty = 2 - pr1.items[0].expense_account = 'Stock Adjustment - _TC' + pr1.items[0].expense_account = "Stock Adjustment - _TC" pr1.save() pr1.submit() for key, value in get_supplied_items(pr1).items(): self.assertEqual(value.qty, 2) + def add_second_row_in_pr(pr): item_dict = {} - for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom', - 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate', 'expense_account', 'po_detail']: + for column in [ + "item_code", + "item_name", + "qty", + "uom", + "warehouse", + "stock_uom", + "purchase_order", + "purchase_order_item", + "conversion_factor", + "rate", + "expense_account", + "po_detail", + ]: item_dict[column] = pr.items[0].get(column) - pr.append('items', item_dict) + pr.append("items", item_dict) pr.set_missing_values() + def get_supplied_items(pr_doc): supplied_items = {} - for row in pr_doc.get('supplied_items'): + for row in pr_doc.get("supplied_items"): if row.rm_item_code not in supplied_items: - supplied_items.setdefault(row.rm_item_code, - frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + supplied_items.setdefault( + row.rm_item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) details = supplied_items[row.rm_item_code] update_item_details(row, details) return supplied_items + def make_stock_in_entry(**args): args = frappe._dict(args) @@ -787,11 +923,17 @@ def make_stock_in_entry(**args): for row in args.rm_items: row = frappe._dict(row) - doc = make_stock_entry(target=row.warehouse or '_Test Warehouse - _TC', - item_code=row.item_code, qty=row.qty or 1, basic_rate=row.rate or 100) + doc = make_stock_entry( + target=row.warehouse or "_Test Warehouse - _TC", + item_code=row.item_code, + qty=row.qty or 1, + basic_rate=row.rate or 100, + ) if row.item_code not in items: - items.setdefault(row.item_code, frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)})) + items.setdefault( + row.item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) child_row = doc.items[0] details = items[child_row.item_code] @@ -799,15 +941,20 @@ def make_stock_in_entry(**args): return items + def update_item_details(child_row, details): - details.qty += (child_row.get('qty') if child_row.doctype == 'Stock Entry Detail' - else child_row.get('consumed_qty')) + details.qty += ( + child_row.get("qty") + if child_row.doctype == "Stock Entry Detail" + else child_row.get("consumed_qty") + ) if child_row.serial_no: details.serial_no.extend(get_serial_nos(child_row.serial_no)) if child_row.batch_no: - details.batch_no[child_row.batch_no] += (child_row.get('qty') or child_row.get('consumed_qty')) + details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") + def make_stock_transfer_entry(**args): args = frappe._dict(args) @@ -816,21 +963,27 @@ def make_stock_transfer_entry(**args): for row in args.rm_items: row = frappe._dict(row) - item = {'item_code': row.main_item_code or args.main_item_code, 'rm_item_code': row.item_code, - 'qty': row.qty or 1, 'item_name': row.item_code, 'rate': row.rate or 100, - 'stock_uom': row.stock_uom or 'Nos', 'warehouse': row.warehuose or '_Test Warehouse - _TC'} + item = { + "item_code": row.main_item_code or args.main_item_code, + "rm_item_code": row.item_code, + "qty": row.qty or 1, + "item_name": row.item_code, + "rate": row.rate or 100, + "stock_uom": row.stock_uom or "Nos", + "warehouse": row.warehuose or "_Test Warehouse - _TC", + } item_details = args.itemwise_details.get(row.item_code) if item_details and item_details.serial_no: - serial_nos = item_details.serial_no[0:cint(row.qty)] - item['serial_no'] = '\n'.join(serial_nos) + serial_nos = item_details.serial_no[0 : cint(row.qty)] + item["serial_no"] = "\n".join(serial_nos) item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos)) if item_details and item_details.batch_no: for batch_no, batch_qty in item_details.batch_no.items(): if batch_qty >= row.qty: - item['batch_no'] = batch_no + item["batch_no"] = batch_no item_details.batch_no[batch_no] -= row.qty break @@ -843,42 +996,70 @@ def make_stock_transfer_entry(**args): return doc + def make_subcontract_items(): - sub_contracted_items = {'Subcontracted Item SA1': {}, 'Subcontracted Item SA2': {}, 'Subcontracted Item SA3': {}, - 'Subcontracted Item SA4': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'SBAT.####'}, - 'Subcontracted Item SA5': {}, 'Subcontracted Item SA6': {}} + sub_contracted_items = { + "Subcontracted Item SA1": {}, + "Subcontracted Item SA2": {}, + "Subcontracted Item SA3": {}, + "Subcontracted Item SA4": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SBAT.####", + }, + "Subcontracted Item SA5": {}, + "Subcontracted Item SA6": {}, + } for item, properties in sub_contracted_items.items(): - if not frappe.db.exists('Item', item): - properties.update({'is_stock_item': 1, 'is_sub_contracted_item': 1}) + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1, "is_sub_contracted_item": 1}) make_item(item, properties) + def make_raw_materials(): - raw_materials = {'Subcontracted SRM Item 1': {}, - 'Subcontracted SRM Item 2': {'has_serial_no': 1, 'serial_no_series': 'SRI.####'}, - 'Subcontracted SRM Item 3': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'BAT.####'}, - 'Subcontracted SRM Item 4': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}, - 'Subcontracted SRM Item 5': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}} + raw_materials = { + "Subcontracted SRM Item 1": {}, + "Subcontracted SRM Item 2": {"has_serial_no": 1, "serial_no_series": "SRI.####"}, + "Subcontracted SRM Item 3": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT.####", + }, + "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + } for item, properties in raw_materials.items(): - if not frappe.db.exists('Item', item): - properties.update({'is_stock_item': 1}) + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1}) make_item(item, properties) + def make_bom_for_subcontracted_items(): boms = { - 'Subcontracted Item SA1': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], - 'Subcontracted Item SA2': ['Subcontracted SRM Item 2'], - 'Subcontracted Item SA3': ['Subcontracted SRM Item 2'], - 'Subcontracted Item SA4': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'], - 'Subcontracted Item SA5': ['Subcontracted SRM Item 5'], - 'Subcontracted Item SA6': ['Subcontracted SRM Item 3'] + "Subcontracted Item SA1": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA2": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA3": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA4": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA5": ["Subcontracted SRM Item 5"], + "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], } for item_code, raw_materials in boms.items(): - if not frappe.db.exists('BOM', {'item': item_code}): + if not frappe.db.exists("BOM", {"item": item_code}): make_bom(item=item_code, raw_materials=raw_materials, rate=100) + def set_backflush_based_on(based_on): - frappe.db.set_value('Buying Settings', None, - 'backflush_raw_materials_of_subcontract_based_on', based_on) + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on + ) diff --git a/erpnext/tests/test_webform.py b/erpnext/tests/test_webform.py index 19255db33c5..202467b5450 100644 --- a/erpnext/tests/test_webform.py +++ b/erpnext/tests/test_webform.py @@ -7,132 +7,143 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur class TestWebsite(unittest.TestCase): def test_permission_for_custom_doctype(self): - create_user('Supplier 1', 'supplier1@gmail.com') - create_user('Supplier 2', 'supplier2@gmail.com') - create_supplier_with_contact('Supplier1', 'All Supplier Groups', 'Supplier 1', 'supplier1@gmail.com') - create_supplier_with_contact('Supplier2', 'All Supplier Groups', 'Supplier 2', 'supplier2@gmail.com') - po1 = create_purchase_order(supplier='Supplier1') - po2 = create_purchase_order(supplier='Supplier2') + create_user("Supplier 1", "supplier1@gmail.com") + create_user("Supplier 2", "supplier2@gmail.com") + create_supplier_with_contact( + "Supplier1", "All Supplier Groups", "Supplier 1", "supplier1@gmail.com" + ) + create_supplier_with_contact( + "Supplier2", "All Supplier Groups", "Supplier 2", "supplier2@gmail.com" + ) + po1 = create_purchase_order(supplier="Supplier1") + po2 = create_purchase_order(supplier="Supplier2") create_custom_doctype() create_webform() - create_order_assignment(supplier='Supplier1', po = po1.name) - create_order_assignment(supplier='Supplier2', po = po2.name) + create_order_assignment(supplier="Supplier1", po=po1.name) + create_order_assignment(supplier="Supplier2", po=po2.name) frappe.set_user("Administrator") # checking if data consist of all order assignment of Supplier1 and Supplier2 - self.assertTrue('Supplier1' and 'Supplier2' in [data.supplier for data in get_data()]) + self.assertTrue("Supplier1" and "Supplier2" in [data.supplier for data in get_data()]) frappe.set_user("supplier1@gmail.com") # checking if data only consist of order assignment of Supplier1 - self.assertTrue('Supplier1' in [data.supplier for data in get_data()]) - self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier1']) + self.assertTrue("Supplier1" in [data.supplier for data in get_data()]) + self.assertFalse([data.supplier for data in get_data() if data.supplier != "Supplier1"]) frappe.set_user("supplier2@gmail.com") # checking if data only consist of order assignment of Supplier2 - self.assertTrue('Supplier2' in [data.supplier for data in get_data()]) - self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier2']) + self.assertTrue("Supplier2" in [data.supplier for data in get_data()]) + self.assertFalse([data.supplier for data in get_data() if data.supplier != "Supplier2"]) frappe.set_user("Administrator") + def get_data(): - webform_list_contexts = frappe.get_hooks('webform_list_context') + webform_list_contexts = frappe.get_hooks("webform_list_context") if webform_list_contexts: - context = frappe._dict(frappe.get_attr(webform_list_contexts[0])('Buying') or {}) - kwargs = dict(doctype='Order Assignment', order_by = 'modified desc') + context = frappe._dict(frappe.get_attr(webform_list_contexts[0])("Buying") or {}) + kwargs = dict(doctype="Order Assignment", order_by="modified desc") return context.get_list(**kwargs) + def create_user(name, email): - frappe.get_doc({ - 'doctype': 'User', - 'send_welcome_email': 0, - 'user_type': 'Website User', - 'first_name': name, - 'email': email, - 'roles': [{"doctype": "Has Role", "role": "Supplier"}] - }).insert(ignore_if_duplicate = True) + frappe.get_doc( + { + "doctype": "User", + "send_welcome_email": 0, + "user_type": "Website User", + "first_name": name, + "email": email, + "roles": [{"doctype": "Has Role", "role": "Supplier"}], + } + ).insert(ignore_if_duplicate=True) + def create_supplier_with_contact(name, group, contact_name, contact_email): - supplier = frappe.get_doc({ - 'doctype': 'Supplier', - 'supplier_name': name, - 'supplier_group': group - }).insert(ignore_if_duplicate = True) + supplier = frappe.get_doc( + {"doctype": "Supplier", "supplier_name": name, "supplier_group": group} + ).insert(ignore_if_duplicate=True) - if not frappe.db.exists('Contact', contact_name+'-1-'+name): + if not frappe.db.exists("Contact", contact_name + "-1-" + name): new_contact = frappe.new_doc("Contact") new_contact.first_name = contact_name - new_contact.is_primary_contact = True, - new_contact.append('links', { - "link_doctype": "Supplier", - "link_name": supplier.name - }) - new_contact.append('email_ids', { - "email_id": contact_email, - "is_primary": 1 - }) + new_contact.is_primary_contact = (True,) + new_contact.append("links", {"link_doctype": "Supplier", "link_name": supplier.name}) + new_contact.append("email_ids", {"email_id": contact_email, "is_primary": 1}) new_contact.insert(ignore_mandatory=True) + def create_custom_doctype(): - frappe.get_doc({ - 'doctype': 'DocType', - 'name': 'Order Assignment', - 'module': 'Buying', - 'custom': 1, - 'autoname': 'field:po', - 'fields': [ - {'label': 'PO', 'fieldname': 'po', 'fieldtype': 'Link', 'options': 'Purchase Order'}, - {'label': 'Supplier', 'fieldname': 'supplier', 'fieldtype': 'Data', "fetch_from": "po.supplier"} - ], - 'permissions': [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "read": 1, - "role": "Supplier" - } - ] - }).insert(ignore_if_duplicate = True) + frappe.get_doc( + { + "doctype": "DocType", + "name": "Order Assignment", + "module": "Buying", + "custom": 1, + "autoname": "field:po", + "fields": [ + {"label": "PO", "fieldname": "po", "fieldtype": "Link", "options": "Purchase Order"}, + { + "label": "Supplier", + "fieldname": "supplier", + "fieldtype": "Data", + "fetch_from": "po.supplier", + }, + ], + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1, + }, + {"read": 1, "role": "Supplier"}, + ], + } + ).insert(ignore_if_duplicate=True) + def create_webform(): - frappe.get_doc({ - 'doctype': 'Web Form', - 'module': 'Buying', - 'title': 'SO Schedule', - 'route': 'so-schedule', - 'doc_type': 'Order Assignment', - 'web_form_fields': [ - { - 'doctype': 'Web Form Field', - 'fieldname': 'po', - 'fieldtype': 'Link', - 'options': 'Purchase Order', - 'label': 'PO' - }, - { - 'doctype': 'Web Form Field', - 'fieldname': 'supplier', - 'fieldtype': 'Data', - 'label': 'Supplier' - } - ] + frappe.get_doc( + { + "doctype": "Web Form", + "module": "Buying", + "title": "SO Schedule", + "route": "so-schedule", + "doc_type": "Order Assignment", + "web_form_fields": [ + { + "doctype": "Web Form Field", + "fieldname": "po", + "fieldtype": "Link", + "options": "Purchase Order", + "label": "PO", + }, + { + "doctype": "Web Form Field", + "fieldname": "supplier", + "fieldtype": "Data", + "label": "Supplier", + }, + ], + } + ).insert(ignore_if_duplicate=True) - }).insert(ignore_if_duplicate = True) def create_order_assignment(supplier, po): - frappe.get_doc({ - 'doctype': 'Order Assignment', - 'po': po, - 'supplier': supplier, - }).insert(ignore_if_duplicate = True) \ No newline at end of file + frappe.get_doc( + { + "doctype": "Order Assignment", + "po": po, + "supplier": supplier, + } + ).insert(ignore_if_duplicate=True) diff --git a/erpnext/tests/test_woocommerce.py b/erpnext/tests/test_woocommerce.py index 59a9a313dbe..663464b6b78 100644 --- a/erpnext/tests/test_woocommerce.py +++ b/erpnext/tests/test_woocommerce.py @@ -1,4 +1,3 @@ - import os import time import unittest @@ -26,32 +25,126 @@ class TestWoocommerce(unittest.TestCase): woo_settings.save(ignore_permissions=True) def test_sales_order_for_woocommerce(self): - frappe.flags.woocomm_test_order_data = {"id":75,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":False,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"_Test Company","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":"","date_paid_gmt":"","date_completed":"","date_completed_gmt":"","cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]} + frappe.flags.woocomm_test_order_data = { + "id": 75, + "parent_id": 0, + "number": "74", + "order_key": "wc_order_5aa1281c2dacb", + "created_via": "checkout", + "version": "3.3.3", + "status": "processing", + "currency": "INR", + "date_created": "2018-03-08T12:10:04", + "date_created_gmt": "2018-03-08T12:10:04", + "date_modified": "2018-03-08T12:10:04", + "date_modified_gmt": "2018-03-08T12:10:04", + "discount_total": "0.00", + "discount_tax": "0.00", + "shipping_total": "150.00", + "shipping_tax": "0.00", + "cart_tax": "0.00", + "total": "649.00", + "total_tax": "0.00", + "prices_include_tax": False, + "customer_id": 12, + "customer_ip_address": "103.54.99.5", + "customer_user_agent": "mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36", + "customer_note": "", + "billing": { + "first_name": "Tony", + "last_name": "Stark", + "company": "_Test Company", + "address_1": "Mumbai", + "address_2": "", + "city": "Dadar", + "state": "MH", + "postcode": "123", + "country": "IN", + "email": "tony@gmail.com", + "phone": "123457890", + }, + "shipping": { + "first_name": "Tony", + "last_name": "Stark", + "company": "", + "address_1": "Mumbai", + "address_2": "", + "city": "Dadar", + "state": "MH", + "postcode": "123", + "country": "IN", + }, + "payment_method": "cod", + "payment_method_title": "Cash on delivery", + "transaction_id": "", + "date_paid": "", + "date_paid_gmt": "", + "date_completed": "", + "date_completed_gmt": "", + "cart_hash": "8e76b020d5790066496f244860c4703f", + "meta_data": [], + "line_items": [ + { + "id": 80, + "name": "Marvel", + "product_id": 56, + "variation_id": 0, + "quantity": 1, + "tax_class": "", + "subtotal": "499.00", + "subtotal_tax": "0.00", + "total": "499.00", + "total_tax": "0.00", + "taxes": [], + "meta_data": [], + "sku": "", + "price": 499, + } + ], + "tax_lines": [], + "shipping_lines": [ + { + "id": 81, + "method_title": "Flat rate", + "method_id": "flat_rate:1", + "total": "150.00", + "total_tax": "0.00", + "taxes": [], + "meta_data": [{"id": 623, "key": "Items", "value": "Marvel × 1"}], + } + ], + "fee_lines": [], + "coupon_lines": [], + "refunds": [], + } order() - self.assertTrue(frappe.get_value("Customer",{"woocommerce_email":"tony@gmail.com"})) - self.assertTrue(frappe.get_value("Item",{"woocommerce_id": 56})) - self.assertTrue(frappe.get_value("Sales Order",{"woocommerce_id":75})) + self.assertTrue(frappe.get_value("Customer", {"woocommerce_email": "tony@gmail.com"})) + self.assertTrue(frappe.get_value("Item", {"woocommerce_id": 56})) + self.assertTrue(frappe.get_value("Sales Order", {"woocommerce_id": 75})) frappe.flags.woocomm_test_order_data = {} + def emulate_request(): # Emulate Woocommerce Request headers = { - "X-Wc-Webhook-Event":"created", - "X-Wc-Webhook-Signature":"h1SjzQMPwd68MF5bficeFq20/RkQeRLsb9AVCUz/rLs=" + "X-Wc-Webhook-Event": "created", + "X-Wc-Webhook-Signature": "h1SjzQMPwd68MF5bficeFq20/RkQeRLsb9AVCUz/rLs=", } # Emulate Request Data data = """{"id":74,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":false,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"Woocommerce","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":null,"date_paid_gmt":null,"date_completed":null,"date_completed_gmt":null,"cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}""" # Build URL - port = frappe.get_site_config().webserver_port or '8000' + port = frappe.get_site_config().webserver_port or "8000" - if os.environ.get('CI'): - host = 'localhost' + if os.environ.get("CI"): + host = "localhost" else: host = frappe.local.site - url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order".format(site=host, port=port) + url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order".format( + site=host, port=port + ) r = requests.post(url=url, headers=headers, data=data) diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py index 9c8c371e051..44834c8a77c 100644 --- a/erpnext/tests/ui_test_helpers.py +++ b/erpnext/tests/ui_test_helpers.py @@ -9,54 +9,67 @@ def create_employee_records(): frappe.db.sql("DELETE FROM tabEmployee WHERE company='Test Org Chart'") - emp1 = create_employee('Test Employee 1', 'CEO') - emp2 = create_employee('Test Employee 2', 'CTO') - emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1) - emp4 = create_employee('Test Employee 4', 'Project Manager', emp2) - emp5 = create_employee('Test Employee 5', 'Engineer', emp2) - emp6 = create_employee('Test Employee 6', 'Analyst', emp3) - emp7 = create_employee('Test Employee 7', 'Software Developer', emp4) + emp1 = create_employee("Test Employee 1", "CEO") + emp2 = create_employee("Test Employee 2", "CTO") + emp3 = create_employee("Test Employee 3", "Head of Marketing and Sales", emp1) + emp4 = create_employee("Test Employee 4", "Project Manager", emp2) + emp5 = create_employee("Test Employee 5", "Engineer", emp2) + emp6 = create_employee("Test Employee 6", "Analyst", emp3) + emp7 = create_employee("Test Employee 7", "Software Developer", emp4) employees = [emp1, emp2, emp3, emp4, emp5, emp6, emp7] return employees + @frappe.whitelist() def get_employee_records(): - return frappe.db.get_list('Employee', filters={ - 'company': 'Test Org Chart' - }, pluck='name', order_by='name') + return frappe.db.get_list( + "Employee", filters={"company": "Test Org Chart"}, pluck="name", order_by="name" + ) + def create_company(): - company = frappe.db.exists('Company', 'Test Org Chart') + company = frappe.db.exists("Company", "Test Org Chart") if not company: - company = frappe.get_doc({ - 'doctype': 'Company', - 'company_name': 'Test Org Chart', - 'country': 'India', - 'default_currency': 'INR' - }).insert().name + company = ( + frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test Org Chart", + "country": "India", + "default_currency": "INR", + } + ) + .insert() + .name + ) return company + def create_employee(first_name, designation, reports_to=None): - employee = frappe.db.exists('Employee', {'first_name': first_name, 'designation': designation}) + employee = frappe.db.exists("Employee", {"first_name": first_name, "designation": designation}) if not employee: - employee = frappe.get_doc({ - 'doctype': 'Employee', - 'first_name': first_name, - 'company': 'Test Org Chart', - 'gender': 'Female', - 'date_of_birth': getdate('08-12-1998'), - 'date_of_joining': getdate('01-01-2021'), - 'designation': designation, - 'reports_to': reports_to - }).insert().name + employee = ( + frappe.get_doc( + { + "doctype": "Employee", + "first_name": first_name, + "company": "Test Org Chart", + "gender": "Female", + "date_of_birth": getdate("08-12-1998"), + "date_of_joining": getdate("01-01-2021"), + "designation": designation, + "reports_to": reports_to, + } + ) + .insert() + .name + ) return employee + def create_missing_designation(): - if not frappe.db.exists('Designation', 'CTO'): - frappe.get_doc({ - 'doctype': 'Designation', - 'designation_name': 'CTO' - }).insert() + if not frappe.db.exists("Designation", "CTO"): + frappe.get_doc({"doctype": "Designation", "designation_name": "CTO"}).insert() diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py index 0a9a5f84e11..66641f35518 100644 --- a/erpnext/tests/utils.py +++ b/erpnext/tests/utils.py @@ -9,82 +9,76 @@ from frappe.core.doctype.report.report import get_report_module_dotted_path ReportFilters = Dict[str, Any] ReportName = NewType("ReportName", str) + def create_test_contact_and_address(): - frappe.db.sql('delete from tabContact') - frappe.db.sql('delete from `tabContact Email`') - frappe.db.sql('delete from `tabContact Phone`') - frappe.db.sql('delete from tabAddress') - frappe.db.sql('delete from `tabDynamic Link`') + frappe.db.sql("delete from tabContact") + frappe.db.sql("delete from `tabContact Email`") + frappe.db.sql("delete from `tabContact Phone`") + frappe.db.sql("delete from tabAddress") + frappe.db.sql("delete from `tabDynamic Link`") - frappe.get_doc({ - "doctype": "Address", - "address_title": "_Test Address for Customer", - "address_type": "Office", - "address_line1": "Station Road", - "city": "_Test City", - "state": "Test State", - "country": "India", - "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer" - } - ] - }).insert() + frappe.get_doc( + { + "doctype": "Address", + "address_title": "_Test Address for Customer", + "address_type": "Office", + "address_line1": "Station Road", + "city": "_Test City", + "state": "Test State", + "country": "India", + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}], + } + ).insert() - contact = frappe.get_doc({ - "doctype": 'Contact', - "first_name": "_Test Contact for _Test Customer", - "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer" - } - ] - }) + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": "_Test Contact for _Test Customer", + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}], + } + ) contact.add_email("test_contact_customer@example.com", is_primary=True) contact.add_phone("+91 0000000000", is_primary_phone=True) contact.insert() - contact_two = frappe.get_doc({ - "doctype": 'Contact', - "first_name": "_Test Contact 2 for _Test Customer", - "links": [ - { - "link_doctype": "Customer", - "link_name": "_Test Customer" - } - ] - }) + contact_two = frappe.get_doc( + { + "doctype": "Contact", + "first_name": "_Test Contact 2 for _Test Customer", + "links": [{"link_doctype": "Customer", "link_name": "_Test Customer"}], + } + ) contact_two.add_email("test_contact_two_customer@example.com", is_primary=True) contact_two.add_phone("+92 0000000000", is_primary_phone=True) contact_two.insert() def execute_script_report( - report_name: ReportName, - module: str, - filters: ReportFilters, - default_filters: Optional[ReportFilters] = None, - optional_filters: Optional[ReportFilters] = None - ): + report_name: ReportName, + module: str, + filters: ReportFilters, + default_filters: Optional[ReportFilters] = None, + optional_filters: Optional[ReportFilters] = None, +): """Util for testing execution of a report with specified filters. Tests the execution of report with default_filters + filters. Tests the execution using optional_filters one at a time. Args: - report_name: Human readable name of report (unscrubbed) - module: module to which report belongs to - filters: specific values for filters - default_filters: default values for filters such as company name. - optional_filters: filters which should be tested one at a time in addition to default filters. + report_name: Human readable name of report (unscrubbed) + module: module to which report belongs to + filters: specific values for filters + default_filters: default values for filters such as company name. + optional_filters: filters which should be tested one at a time in addition to default filters. """ if default_filters is None: default_filters = {} - report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute") + report_execute_fn = frappe.get_attr( + get_report_module_dotted_path(module, report_name) + ".execute" + ) report_filters = frappe._dict(default_filters).copy().update(filters) report_data = report_execute_fn(report_filters) diff --git a/erpnext/utilities/__init__.py b/erpnext/utilities/__init__.py index 3749cdeaa88..c2b4229f171 100644 --- a/erpnext/utilities/__init__.py +++ b/erpnext/utilities/__init__.py @@ -7,9 +7,12 @@ from erpnext.utilities.activation import get_level def update_doctypes(): - for d in frappe.db.sql("""select df.parent, df.fieldname + for d in frappe.db.sql( + """select df.parent, df.fieldname from tabDocField df, tabDocType dt where df.fieldname - like "%description%" and df.parent = dt.name and dt.istable = 1""", as_dict=1): + like "%description%" and df.parent = dt.name and dt.istable = 1""", + as_dict=1, + ): dt = frappe.get_doc("DocType", d.parent) for f in dt.fields: @@ -18,20 +21,17 @@ def update_doctypes(): dt.save() break + def get_site_info(site_info): # called via hook - company = frappe.db.get_single_value('Global Defaults', 'default_company') + company = frappe.db.get_single_value("Global Defaults", "default_company") domain = None if not company: - company = frappe.db.sql('select name from `tabCompany` order by creation asc') + company = frappe.db.sql("select name from `tabCompany` order by creation asc") company = company[0][0] if company else None if company: - domain = frappe.get_cached_value('Company', cstr(company), 'domain') + domain = frappe.get_cached_value("Company", cstr(company), "domain") - return { - 'company': company, - 'domain': domain, - 'activation': get_level() - } + return {"company": company, "domain": domain, "activation": get_level()} diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index 8ccda411179..a447f9d88fc 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -42,7 +42,7 @@ def get_level(): "Supplier": 5, "Task": 5, "User": 5, - "Work Order": 5 + "Work Order": 5, } for doctype, min_count in iteritems(doctypes): @@ -51,111 +51,118 @@ def get_level(): activation_level += 1 sales_data.append({doctype: count}) - if frappe.db.get_single_value('System Settings', 'setup_complete'): + if frappe.db.get_single_value("System Settings", "setup_complete"): activation_level += 1 - communication_number = frappe.db.count('Communication', dict(communication_medium='Email')) + communication_number = frappe.db.count("Communication", dict(communication_medium="Email")) if communication_number > 10: activation_level += 1 sales_data.append({"Communication": communication_number}) # recent login - if frappe.db.sql('select name from tabUser where last_login > date_sub(now(), interval 2 day) limit 1'): + if frappe.db.sql( + "select name from tabUser where last_login > date_sub(now(), interval 2 day) limit 1" + ): activation_level += 1 level = {"activation_level": activation_level, "sales_data": sales_data} return level + def get_help_messages(): - '''Returns help messages to be shown on Desktop''' + """Returns help messages to be shown on Desktop""" if get_level() > 6: return [] - domain = frappe.get_cached_value('Company', erpnext.get_default_company(), 'domain') + domain = frappe.get_cached_value("Company", erpnext.get_default_company(), "domain") messages = [] message_settings = [ frappe._dict( - doctype='Lead', - title=_('Create Leads'), - description=_('Leads help you get business, add all your contacts and more as your leads'), - action=_('Create Lead'), - route='List/Lead', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Lead", + title=_("Create Leads"), + description=_("Leads help you get business, add all your contacts and more as your leads"), + action=_("Create Lead"), + route="List/Lead", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Quotation', - title=_('Create customer quotes'), - description=_('Quotations are proposals, bids you have sent to your customers'), - action=_('Create Quotation'), - route='List/Quotation', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Quotation", + title=_("Create customer quotes"), + description=_("Quotations are proposals, bids you have sent to your customers"), + action=_("Create Quotation"), + route="List/Quotation", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Sales Order', - title=_('Manage your orders'), - description=_('Create Sales Orders to help you plan your work and deliver on-time'), - action=_('Create Sales Order'), - route='List/Sales Order', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Sales Order", + title=_("Manage your orders"), + description=_("Create Sales Orders to help you plan your work and deliver on-time"), + action=_("Create Sales Order"), + route="List/Sales Order", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Purchase Order', - title=_('Create Purchase Orders'), - description=_('Purchase orders help you plan and follow up on your purchases'), - action=_('Create Purchase Order'), - route='List/Purchase Order', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="Purchase Order", + title=_("Create Purchase Orders"), + description=_("Purchase orders help you plan and follow up on your purchases"), + action=_("Create Purchase Order"), + route="List/Purchase Order", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='User', - title=_('Create Users'), - description=_('Add the rest of your organization as your users. You can also add invite Customers to your portal by adding them from Contacts'), - action=_('Create User'), - route='List/User', - domain=('Manufacturing', 'Retail', 'Services', 'Distribution'), - target=3 + doctype="User", + title=_("Create Users"), + description=_( + "Add the rest of your organization as your users. You can also add invite Customers to your portal by adding them from Contacts" + ), + action=_("Create User"), + route="List/User", + domain=("Manufacturing", "Retail", "Services", "Distribution"), + target=3, ), frappe._dict( - doctype='Timesheet', - title=_('Add Timesheets'), - description=_('Timesheets help keep track of time, cost and billing for activites done by your team'), - action=_('Create Timesheet'), - route='List/Timesheet', - domain=('Services',), - target=5 + doctype="Timesheet", + title=_("Add Timesheets"), + description=_( + "Timesheets help keep track of time, cost and billing for activites done by your team" + ), + action=_("Create Timesheet"), + route="List/Timesheet", + domain=("Services",), + target=5, ), frappe._dict( - doctype='Student', - title=_('Add Students'), - description=_('Students are at the heart of the system, add all your students'), - action=_('Create Student'), - route='List/Student', - domain=('Education',), - target=5 + doctype="Student", + title=_("Add Students"), + description=_("Students are at the heart of the system, add all your students"), + action=_("Create Student"), + route="List/Student", + domain=("Education",), + target=5, ), frappe._dict( - doctype='Student Batch', - title=_('Group your students in batches'), - description=_('Student Batches help you track attendance, assessments and fees for students'), - action=_('Create Student Batch'), - route='List/Student Batch', - domain=('Education',), - target=3 + doctype="Student Batch", + title=_("Group your students in batches"), + description=_("Student Batches help you track attendance, assessments and fees for students"), + action=_("Create Student Batch"), + route="List/Student Batch", + domain=("Education",), + target=3, ), frappe._dict( - doctype='Employee', - title=_('Create Employee Records'), - description=_('Create Employee records to manage leaves, expense claims and payroll'), - action=_('Create Employee'), - route='List/Employee', - target=3 - ) + doctype="Employee", + title=_("Create Employee Records"), + description=_("Create Employee records to manage leaves, expense claims and payroll"), + action=_("Create Employee"), + route="List/Employee", + target=3, + ), ] for m in message_settings: diff --git a/erpnext/utilities/bot.py b/erpnext/utilities/bot.py index 87a350864f6..5c2e576dd20 100644 --- a/erpnext/utilities/bot.py +++ b/erpnext/utilities/bot.py @@ -9,13 +9,16 @@ from frappe.utils.bot import BotParser class FindItemBot(BotParser): def get_reply(self): - if self.startswith('where is', 'find item', 'locate'): - if not frappe.has_permission('Warehouse'): + if self.startswith("where is", "find item", "locate"): + if not frappe.has_permission("Warehouse"): raise frappe.PermissionError - item = '%{0}%'.format(self.strip_words(self.query, 'where is', 'find item', 'locate')) - items = frappe.db.sql('''select name from `tabItem` where item_code like %(txt)s - or item_name like %(txt)s or description like %(txt)s''', dict(txt=item)) + item = "%{0}%".format(self.strip_words(self.query, "where is", "find item", "locate")) + items = frappe.db.sql( + """select name from `tabItem` where item_code like %(txt)s + or item_name like %(txt)s or description like %(txt)s""", + dict(txt=item), + ) if items: out = [] @@ -23,14 +26,19 @@ class FindItemBot(BotParser): for item in items: found = False for warehouse in warehouses: - qty = frappe.db.get_value("Bin", {'item_code': item[0], 'warehouse': warehouse.name}, 'actual_qty') + qty = frappe.db.get_value( + "Bin", {"item_code": item[0], "warehouse": warehouse.name}, "actual_qty" + ) if qty: - out.append(_('{0} units of [{1}](/app/Form/Item/{1}) found in [{2}](/app/Form/Warehouse/{2})').format(qty, - item[0], warehouse.name)) + out.append( + _("{0} units of [{1}](/app/Form/Item/{1}) found in [{2}](/app/Form/Warehouse/{2})").format( + qty, item[0], warehouse.name + ) + ) found = True if not found: - out.append(_('[{0}](/app/Form/Item/{0}) is out of stock').format(item[0])) + out.append(_("[{0}](/app/Form/Item/{0}) is out of stock").format(item[0])) return "\n\n".join(out) diff --git a/erpnext/utilities/doctype/rename_tool/rename_tool.py b/erpnext/utilities/doctype/rename_tool/rename_tool.py index 74de54ac310..b31574cdc85 100644 --- a/erpnext/utilities/doctype/rename_tool/rename_tool.py +++ b/erpnext/utilities/doctype/rename_tool/rename_tool.py @@ -12,14 +12,19 @@ from frappe.model.rename_doc import bulk_rename class RenameTool(Document): pass + @frappe.whitelist() def get_doctypes(): - return frappe.db.sql_list("""select name from tabDocType - where allow_rename=1 and module!='Core' order by name""") + return frappe.db.sql_list( + """select name from tabDocType + where allow_rename=1 and module!='Core' order by name""" + ) + @frappe.whitelist() def upload(select_doctype=None, rows=None): from frappe.utils.csvutils import read_csv_content_from_attached_file + if not select_doctype: select_doctype = frappe.form_dict.select_doctype diff --git a/erpnext/utilities/doctype/sms_log/test_sms_log.py b/erpnext/utilities/doctype/sms_log/test_sms_log.py index 5f7abdc1a86..3ff02023882 100644 --- a/erpnext/utilities/doctype/sms_log/test_sms_log.py +++ b/erpnext/utilities/doctype/sms_log/test_sms_log.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('SMS Log') + class TestSMSLog(unittest.TestCase): pass diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 4e605134c4b..12ceb312af2 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -29,16 +29,17 @@ class Video(Document): try: video = api.get_video_by_id(video_id=self.youtube_video_id) - video_stats = video.items[0].to_dict().get('statistics') + video_stats = video.items[0].to_dict().get("statistics") - self.like_count = video_stats.get('likeCount') - self.view_count = video_stats.get('viewCount') - self.dislike_count = video_stats.get('dislikeCount') - self.comment_count = video_stats.get('commentCount') + self.like_count = video_stats.get("likeCount") + self.view_count = video_stats.get("viewCount") + self.dislike_count = video_stats.get("dislikeCount") + self.comment_count = video_stats.get("commentCount") except Exception: title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name) - frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + def is_tracking_enabled(): return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") @@ -55,7 +56,9 @@ def get_frequency(value): def update_youtube_data(): # Called every 30 minutes via hooks - enable_youtube_tracking, frequency = frappe.db.get_value("Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"]) + enable_youtube_tracking, frequency = frappe.db.get_value( + "Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"] + ) if not enable_youtube_tracking: return @@ -78,19 +81,21 @@ def get_formatted_ids(video_list): for video in video_list: ids.append(video.youtube_video_id) - return ','.join(ids) + return ",".join(ids) @frappe.whitelist() def get_id_from_url(url): """ - Returns video id from url - :param youtube url: String URL + Returns video id from url + :param youtube url: String URL """ if not isinstance(url, string_types): frappe.throw(_("URL can only be a string"), title=_("Invalid URL")) - pattern = re.compile(r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?') + pattern = re.compile( + r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?' + ) id = pattern.match(url) return id.groups()[-1] @@ -106,7 +111,7 @@ def batch_update_youtube_data(): return video_stats except Exception: title = "Failed to Update YouTube Statistics" - frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) def prepare_and_set_data(video_list): video_ids = get_formatted_ids(video_list) @@ -115,24 +120,27 @@ def batch_update_youtube_data(): def set_youtube_data(entries): for entry in entries: - video_stats = entry.to_dict().get('statistics') - video_id = entry.to_dict().get('id') + video_stats = entry.to_dict().get("statistics") + video_id = entry.to_dict().get("id") stats = { - 'like_count' : video_stats.get('likeCount'), - 'view_count' : video_stats.get('viewCount'), - 'dislike_count' : video_stats.get('dislikeCount'), - 'comment_count' : video_stats.get('commentCount'), - 'video_id': video_id + "like_count": video_stats.get("likeCount"), + "view_count": video_stats.get("viewCount"), + "dislike_count": video_stats.get("dislikeCount"), + "comment_count": video_stats.get("commentCount"), + "video_id": video_id, } - frappe.db.sql(""" + frappe.db.sql( + """ UPDATE `tabVideo` SET like_count = %(like_count)s, view_count = %(view_count)s, dislike_count = %(dislike_count)s, comment_count = %(comment_count)s - WHERE youtube_video_id = %(video_id)s""", stats) + WHERE youtube_video_id = %(video_id)s""", + stats, + ) video_list = frappe.get_all("Video", fields=["youtube_video_id"]) if len(video_list) > 50: diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py index 6f1e2bba167..97fbc41934b 100644 --- a/erpnext/utilities/doctype/video_settings/video_settings.py +++ b/erpnext/utilities/doctype/video_settings/video_settings.py @@ -18,5 +18,5 @@ class VideoSettings(Document): build("youtube", "v3", developerKey=self.api_key) except Exception: title = _("Failed to Authenticate the API key.") - frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials")) diff --git a/erpnext/utilities/hierarchy_chart.py b/erpnext/utilities/hierarchy_chart.py index c18ce107add..4bf4353cdfc 100644 --- a/erpnext/utilities/hierarchy_chart.py +++ b/erpnext/utilities/hierarchy_chart.py @@ -8,11 +8,11 @@ from frappe import _ @frappe.whitelist() def get_all_nodes(method, company): - '''Recursively gets all data from nodes''' + """Recursively gets all data from nodes""" method = frappe.get_attr(method) if method not in frappe.whitelisted: - frappe.throw(_('Not Permitted'), frappe.PermissionError) + frappe.throw(_("Not Permitted"), frappe.PermissionError) root_nodes = method(company=company) result = [] @@ -21,14 +21,16 @@ def get_all_nodes(method, company): for root in root_nodes: data = method(root.id, company) result.append(dict(parent=root.id, parent_name=root.name, data=data)) - nodes_to_expand.extend([{'id': d.get('id'), 'name': d.get('name')} for d in data if d.get('expandable')]) + nodes_to_expand.extend( + [{"id": d.get("id"), "name": d.get("name")} for d in data if d.get("expandable")] + ) while nodes_to_expand: parent = nodes_to_expand.pop(0) - data = method(parent.get('id'), company) - result.append(dict(parent=parent.get('id'), parent_name=parent.get('name'), data=data)) + data = method(parent.get("id"), company) + result.append(dict(parent=parent.get("id"), parent_name=parent.get("name"), data=data)) for d in data: - if d.get('expandable'): - nodes_to_expand.append({'id': d.get('id'), 'name': d.get('name')}) + if d.get("expandable"): + nodes_to_expand.append({"id": d.get("id"), "name": d.get("name")}) return result diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 1cae8d9dd3a..dca1b361f45 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -10,32 +10,41 @@ from erpnext.stock.doctype.batch.batch import get_batch_qty def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): - in_stock, stock_qty = 0, '' - template_item_code, is_stock_item = frappe.db.get_value("Item", item_code, ["variant_of", "is_stock_item"]) + in_stock, stock_qty = 0, "" + template_item_code, is_stock_item = frappe.db.get_value( + "Item", item_code, ["variant_of", "is_stock_item"] + ) if not warehouse: warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field) if not warehouse and template_item_code and template_item_code != item_code: - warehouse = frappe.db.get_value("Website Item", {"item_code": template_item_code}, item_warehouse_field) + warehouse = frappe.db.get_value( + "Website Item", {"item_code": template_item_code}, item_warehouse_field + ) if warehouse: - stock_qty = frappe.db.sql(""" + stock_qty = frappe.db.sql( + """ select GREATEST(S.actual_qty - S.reserved_qty - S.reserved_qty_for_production - S.reserved_qty_for_sub_contract, 0) / IFNULL(C.conversion_factor, 1) from tabBin S inner join `tabItem` I on S.item_code = I.Item_code left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code - where S.item_code=%s and S.warehouse=%s""", (item_code, warehouse)) + where S.item_code=%s and S.warehouse=%s""", + (item_code, warehouse), + ) if stock_qty: stock_qty = adjust_qty_for_expired_items(item_code, stock_qty, warehouse) in_stock = stock_qty[0][0] > 0 and 1 or 0 - return frappe._dict({"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item}) + return frappe._dict( + {"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item} + ) def adjust_qty_for_expired_items(item_code, stock_qty, warehouse): - batches = frappe.get_all('Batch', filters=[{'item': item_code}], fields=['expiry_date', 'name']) + batches = frappe.get_all("Batch", filters=[{"item": item_code}], fields=["expiry_date", "name"]) expired_batches = get_expired_batches(batches) stock_qty = [list(item) for item in stock_qty] @@ -68,33 +77,42 @@ def qty_from_all_warehouses(batch_info): return qty + def get_price(item_code, price_list, customer_group, company, qty=1): from erpnext.e_commerce.shopping_cart.cart import get_party template_item_code = frappe.db.get_value("Item", item_code, "variant_of") if price_list: - price = frappe.get_all("Item Price", fields=["price_list_rate", "currency"], - filters={"price_list": price_list, "item_code": item_code}) + price = frappe.get_all( + "Item Price", + fields=["price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": item_code}, + ) if template_item_code and not price: - price = frappe.get_all("Item Price", fields=["price_list_rate", "currency"], - filters={"price_list": price_list, "item_code": template_item_code}) + price = frappe.get_all( + "Item Price", + fields=["price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": template_item_code}, + ) if price: party = get_party() - pricing_rule_dict = frappe._dict({ - "item_code": item_code, - "qty": qty, - "stock_qty": qty, - "transaction_type": "selling", - "price_list": price_list, - "customer_group": customer_group, - "company": company, - "conversion_rate": 1, - "for_shopping_cart": True, - "currency": frappe.db.get_value("Price List", price_list, "currency") - }) + pricing_rule_dict = frappe._dict( + { + "item_code": item_code, + "qty": qty, + "stock_qty": qty, + "transaction_type": "selling", + "price_list": price_list, + "customer_group": customer_group, + "company": company, + "conversion_rate": 1, + "for_shopping_cart": True, + "currency": frappe.db.get_value("Price List", price_list, "currency"), + } + ) if party and party.doctype == "Customer": pricing_rule_dict.update({"customer": party.name}) @@ -109,7 +127,9 @@ def get_price(item_code, price_list, customer_group, company, qty=1): if pricing_rule.pricing_rule_for == "Discount Percentage": price_obj.discount_percent = pricing_rule.discount_percentage price_obj.formatted_discount_percent = str(flt(pricing_rule.discount_percentage, 0)) + "%" - price_obj.price_list_rate = flt(price_obj.price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0))) + price_obj.price_list_rate = flt( + price_obj.price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0)) + ) if pricing_rule.pricing_rule_for == "Rate": rate_discount = flt(mrp) - flt(pricing_rule.price_list_rate) @@ -118,21 +138,33 @@ def get_price(item_code, price_list, customer_group, company, qty=1): price_obj.price_list_rate = pricing_rule.price_list_rate or 0 if price_obj: - price_obj["formatted_price"] = fmt_money(price_obj["price_list_rate"], currency=price_obj["currency"]) + price_obj["formatted_price"] = fmt_money( + price_obj["price_list_rate"], currency=price_obj["currency"] + ) if mrp != price_obj["price_list_rate"]: price_obj["formatted_mrp"] = fmt_money(mrp, currency=price_obj["currency"]) - price_obj["currency_symbol"] = not cint(frappe.db.get_default("hide_currency_symbol")) \ - and (frappe.db.get_value("Currency", price_obj.currency, "symbol", cache=True) or price_obj.currency) \ + price_obj["currency_symbol"] = ( + not cint(frappe.db.get_default("hide_currency_symbol")) + and ( + frappe.db.get_value("Currency", price_obj.currency, "symbol", cache=True) + or price_obj.currency + ) or "" + ) - uom_conversion_factor = frappe.db.sql("""select C.conversion_factor + uom_conversion_factor = frappe.db.sql( + """select C.conversion_factor from `tabUOM Conversion Detail` C inner join `tabItem` I on C.parent = I.name and C.uom = I.sales_uom - where I.name = %s""", item_code) + where I.name = %s""", + item_code, + ) uom_conversion_factor = uom_conversion_factor[0][0] if uom_conversion_factor else 1 - price_obj["formatted_price_sales_uom"] = fmt_money(price_obj["price_list_rate"] * uom_conversion_factor, currency=price_obj["currency"]) + price_obj["formatted_price_sales_uom"] = fmt_money( + price_obj["price_list_rate"] * uom_conversion_factor, currency=price_obj["currency"] + ) if not price_obj["price_list_rate"]: price_obj["price_list_rate"] = 0 @@ -145,11 +177,17 @@ def get_price(item_code, price_list, customer_group, company, qty=1): return price_obj + def get_non_stock_item_status(item_code, item_warehouse_field): # if item is a product bundle, check if its bundle items are in stock if frappe.db.exists("Product Bundle", item_code): items = frappe.get_doc("Product Bundle", item_code).get_all_children() - bundle_warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field) - return all(get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock for d in items) + bundle_warehouse = frappe.db.get_value( + "Website Item", {"item_code": item_code}, item_warehouse_field + ) + return all( + get_web_item_qty_in_stock(d.item_code, item_warehouse_field, bundle_warehouse).in_stock + for d in items + ) else: return 1 diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py index a185a7012c6..a65a75f3621 100644 --- a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py @@ -16,91 +16,51 @@ def execute(filters=None): chart_data, summary = get_chart_summary_data(data) return columns, data, None, chart_data, summary + def get_columns(): return [ - { - "label": _("Published Date"), - "fieldname": "publish_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Title"), - "fieldname": "title", - "fieldtype": "Data", - "width": 200 - }, - { - "label": _("Duration"), - "fieldname": "duration", - "fieldtype": "Duration", - "width": 100 - }, - { - "label": _("Views"), - "fieldname": "view_count", - "fieldtype": "Float", - "width": 200 - }, - { - "label": _("Likes"), - "fieldname": "like_count", - "fieldtype": "Float", - "width": 200 - }, - { - "label": _("Dislikes"), - "fieldname": "dislike_count", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Comments"), - "fieldname": "comment_count", - "fieldtype": "Float", - "width": 100 - } + {"label": _("Published Date"), "fieldname": "publish_date", "fieldtype": "Date", "width": 100}, + {"label": _("Title"), "fieldname": "title", "fieldtype": "Data", "width": 200}, + {"label": _("Duration"), "fieldname": "duration", "fieldtype": "Duration", "width": 100}, + {"label": _("Views"), "fieldname": "view_count", "fieldtype": "Float", "width": 200}, + {"label": _("Likes"), "fieldname": "like_count", "fieldtype": "Float", "width": 200}, + {"label": _("Dislikes"), "fieldname": "dislike_count", "fieldtype": "Float", "width": 100}, + {"label": _("Comments"), "fieldname": "comment_count", "fieldtype": "Float", "width": 100}, ] + def get_data(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT publish_date, title, provider, duration, view_count, like_count, dislike_count, comment_count FROM `tabVideo` WHERE view_count is not null and publish_date between %(from_date)s and %(to_date)s - ORDER BY view_count desc""", filters, as_dict=1) + ORDER BY view_count desc""", + filters, + as_dict=1, + ) + def get_chart_summary_data(data): labels, likes, views = [], [], [] total_views = 0 for row in data: - labels.append(row.get('title')) - likes.append(row.get('like_count')) - views.append(row.get('view_count')) - total_views += flt(row.get('view_count')) - + labels.append(row.get("title")) + likes.append(row.get("like_count")) + views.append(row.get("view_count")) + total_views += flt(row.get("view_count")) chart_data = { - "data" : { - "labels" : labels, - "datasets" : [ - { - "name" : "Likes", - "values" : likes - }, - { - "name" : "Views", - "values" : views - } - ] + "data": { + "labels": labels, + "datasets": [{"name": "Likes", "values": likes}, {"name": "Views", "values": views}], }, "type": "bar", - "barOptions": { - "stacked": 1 - }, + "barOptions": {"stacked": 1}, } summary = [ diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 1c11c6aba6e..78ac29c76fd 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -11,7 +11,9 @@ from six import string_types from erpnext.controllers.status_updater import StatusUpdater -class UOMMustBeIntegerError(frappe.ValidationError): pass +class UOMMustBeIntegerError(frappe.ValidationError): + pass + class TransactionBase(StatusUpdater): def validate_posting_time(self): @@ -19,69 +21,79 @@ class TransactionBase(StatusUpdater): if frappe.flags.in_import and self.posting_date: self.set_posting_time = 1 - if not getattr(self, 'set_posting_time', None): + if not getattr(self, "set_posting_time", None): now = now_datetime() - self.posting_date = now.strftime('%Y-%m-%d') - self.posting_time = now.strftime('%H:%M:%S.%f') + self.posting_date = now.strftime("%Y-%m-%d") + self.posting_time = now.strftime("%H:%M:%S.%f") elif self.posting_time: try: get_time(self.posting_time) except ValueError: - frappe.throw(_('Invalid Posting Time')) + frappe.throw(_("Invalid Posting Time")) def add_calendar_event(self, opts, force=False): - if cstr(self.contact_by) != cstr(self._prev.contact_by) or \ - cstr(self.contact_date) != cstr(self._prev.contact_date) or force or \ - (hasattr(self, "ends_on") and cstr(self.ends_on) != cstr(self._prev.ends_on)): + if ( + cstr(self.contact_by) != cstr(self._prev.contact_by) + or cstr(self.contact_date) != cstr(self._prev.contact_date) + or force + or (hasattr(self, "ends_on") and cstr(self.ends_on) != cstr(self._prev.ends_on)) + ): self.delete_events() self._add_calendar_event(opts) def delete_events(self): - participations = frappe.get_all("Event Participants", filters={"reference_doctype": self.doctype, "reference_docname": self.name, - "parenttype": "Event"}, fields=["name", "parent"]) + participations = frappe.get_all( + "Event Participants", + filters={ + "reference_doctype": self.doctype, + "reference_docname": self.name, + "parenttype": "Event", + }, + fields=["name", "parent"], + ) if participations: for participation in participations: - total_participants = frappe.get_all("Event Participants", filters={"parenttype": "Event", "parent": participation.parent}) + total_participants = frappe.get_all( + "Event Participants", filters={"parenttype": "Event", "parent": participation.parent} + ) if len(total_participants) <= 1: frappe.db.sql("delete from `tabEvent` where name='%s'" % participation.parent) frappe.db.sql("delete from `tabEvent Participants` where name='%s'" % participation.name) - def _add_calendar_event(self, opts): opts = frappe._dict(opts) if self.contact_date: - event = frappe.get_doc({ - "doctype": "Event", - "owner": opts.owner or self.owner, - "subject": opts.subject, - "description": opts.description, - "starts_on": self.contact_date, - "ends_on": opts.ends_on, - "event_type": "Private" - }) - - event.append('event_participants', { - "reference_doctype": self.doctype, - "reference_docname": self.name + event = frappe.get_doc( + { + "doctype": "Event", + "owner": opts.owner or self.owner, + "subject": opts.subject, + "description": opts.description, + "starts_on": self.contact_date, + "ends_on": opts.ends_on, + "event_type": "Private", } ) + event.append( + "event_participants", {"reference_doctype": self.doctype, "reference_docname": self.name} + ) + event.insert(ignore_permissions=True) if frappe.db.exists("User", self.contact_by): - frappe.share.add("Event", event.name, self.contact_by, - flags={"ignore_share_permission": True}) + frappe.share.add("Event", event.name, self.contact_by, flags={"ignore_share_permission": True}) def validate_uom_is_integer(self, uom_field, qty_fields): validate_uom_is_integer(self, uom_field, qty_fields) def validate_with_previous_doc(self, ref): - self.exclude_fields = ["conversion_factor", "uom"] if self.get('is_return') else [] + self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else [] for key, val in ref.items(): is_child = val.get("is_child_table") @@ -106,8 +118,9 @@ class TransactionBase(StatusUpdater): def compare_values(self, ref_doc, fields, doc=None): for reference_doctype, ref_dn_list in ref_doc.items(): for reference_name in ref_dn_list: - prevdoc_values = frappe.db.get_value(reference_doctype, reference_name, - [d[0] for d in fields], as_dict=1) + prevdoc_values = frappe.db.get_value( + reference_doctype, reference_name, [d[0] for d in fields], as_dict=1 + ) if not prevdoc_values: frappe.throw(_("Invalid reference {0} {1}").format(reference_doctype, reference_name)) @@ -116,7 +129,6 @@ class TransactionBase(StatusUpdater): if prevdoc_values[field] is not None and field not in self.exclude_fields: self.validate_value(field, condition, prevdoc_values[field], doc) - def validate_rate_with_reference_doc(self, ref_details): buying_doctypes = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"] @@ -132,17 +144,26 @@ class TransactionBase(StatusUpdater): if d.get(ref_link_field): ref_rate = frappe.db.get_value(ref_dt + " Item", d.get(ref_link_field), "rate") - if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= .01: + if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01: if action == "Stop": - role_allowed_to_override = frappe.db.get_single_value(settings_doc, 'role_to_override_stop_action') + role_allowed_to_override = frappe.db.get_single_value( + settings_doc, "role_to_override_stop_action" + ) if role_allowed_to_override not in frappe.get_roles(): - frappe.throw(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( - d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate)) + frappe.throw( + _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( + d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate + ) + ) else: - frappe.msgprint(_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( - d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate), title=_("Warning"), indicator="orange") - + frappe.msgprint( + _("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format( + d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate + ), + title=_("Warning"), + indicator="orange", + ) def get_link_filters(self, for_doctype): if hasattr(self, "prev_link_mapper") and self.prev_link_mapper.get(for_doctype): @@ -151,11 +172,7 @@ class TransactionBase(StatusUpdater): values = filter(None, tuple(item.as_dict()[fieldname] for item in self.items)) if values: - ret = { - for_doctype : { - "filters": [[for_doctype, "name", "in", values]] - } - } + ret = {for_doctype: {"filters": [[for_doctype, "name", "in", values]]}} else: ret = None else: @@ -164,17 +181,17 @@ 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. + """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. + 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") - """ + doc.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") + """ child_table_values = set() for row in self.get(child_table): @@ -183,8 +200,11 @@ class TransactionBase(StatusUpdater): if len(child_table_values) > 1: self.set(default_field, None) + def delete_events(ref_type, ref_name): - events = frappe.db.sql_list(""" SELECT + events = ( + frappe.db.sql_list( + """ SELECT distinct `tabEvent`.name from `tabEvent`, `tabEvent Participants` @@ -192,18 +212,27 @@ def delete_events(ref_type, ref_name): `tabEvent`.name = `tabEvent Participants`.parent and `tabEvent Participants`.reference_doctype = %s and `tabEvent Participants`.reference_docname = %s - """, (ref_type, ref_name)) or [] + """, + (ref_type, ref_name), + ) + or [] + ) if events: frappe.delete_doc("Event", events, for_reload=True) + def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, string_types): qty_fields = [qty_fields] distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) - integer_uoms = list(filter(lambda uom: frappe.db.get_value("UOM", uom, - "must_be_whole_number", cache=True) or None, distinct_uoms)) + integer_uoms = list( + filter( + lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, + distinct_uoms, + ) + ) if not integer_uoms: return @@ -214,6 +243,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): qty = d.get(f) if qty: if abs(cint(qty) - flt(qty)) > 0.0000001: - frappe.throw(_("Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.") \ - .format(qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))), - UOMMustBeIntegerError) + frappe.throw( + _( + "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." + ).format( + qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field)) + ), + UOMMustBeIntegerError, + ) diff --git a/erpnext/utilities/web_form/addresses/addresses.py b/erpnext/utilities/web_form/addresses/addresses.py index 8024f875797..db325529e43 100644 --- a/erpnext/utilities/web_form/addresses/addresses.py +++ b/erpnext/utilities/web_form/addresses/addresses.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here context.show_sidebar = True diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py index 17e0cd86ca7..fbf0dce0590 100644 --- a/erpnext/www/all-products/index.py +++ b/erpnext/www/all-products/index.py @@ -5,15 +5,18 @@ from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder sitemap = 1 + def get_context(context): # Add homepage as parent context.body_class = "product-page" - context.parents = [{"name": frappe._("Home"), "route":"/"}] + context.parents = [{"name": frappe._("Home"), "route": "/"}] filter_engine = ProductFiltersBuilder() context.field_filters = filter_engine.get_field_filters() context.attribute_filters = filter_engine.get_attribute_filters() - context.page_length = cint(frappe.db.get_single_value('E Commerce Settings', 'products_per_page'))or 20 + context.page_length = ( + cint(frappe.db.get_single_value("E Commerce Settings", "products_per_page")) or 20 + ) context.no_cache = 1 diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 8cda3c1b90d..06e99da3f94 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -11,38 +11,46 @@ no_cache = 1 def get_context(context): - is_enabled = frappe.db.get_single_value('Appointment Booking Settings', 'enable_scheduling') + is_enabled = frappe.db.get_single_value("Appointment Booking Settings", "enable_scheduling") if is_enabled: return context else: - frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"), - http_status_code=302, indicator_color="red") + frappe.redirect_to_message( + _("Appointment Scheduling Disabled"), + _("Appointment Scheduling has been disabled for this site"), + http_status_code=302, + indicator_color="red", + ) raise frappe.Redirect + @frappe.whitelist(allow_guest=True) def get_appointment_settings(): - settings = frappe.get_doc('Appointment Booking Settings') - settings.holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) + settings = frappe.get_doc("Appointment Booking Settings") + settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) return settings + @frappe.whitelist(allow_guest=True) def get_timezones(): import pytz + return pytz.all_timezones + @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): # Convert query to local timezones - format_string = '%Y-%m-%d %H:%M:%S' - query_start_time = datetime.datetime.strptime(date + ' 00:00:00', format_string) - query_end_time = datetime.datetime.strptime(date + ' 23:59:59', format_string) + format_string = "%Y-%m-%d %H:%M:%S" + query_start_time = datetime.datetime.strptime(date + " 00:00:00", format_string) + query_end_time = datetime.datetime.strptime(date + " 23:59:59", format_string) query_start_time = convert_to_system_timezone(timezone, query_start_time) query_end_time = convert_to_system_timezone(timezone, query_end_time) now = convert_to_guest_timezone(timezone, datetime.datetime.now()) # Database queries - settings = frappe.get_doc('Appointment Booking Settings') - holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) + settings = frappe.get_doc("Appointment Booking Settings") + holiday_list = frappe.get_doc("Holiday List", settings.holiday_list) timeslots = get_available_slots_between(query_start_time, query_end_time, settings) # Filter and convert timeslots @@ -58,15 +66,15 @@ def get_appointment_slots(date, timezone): converted_timeslots.append(dict(time=converted_timeslot, availability=True)) else: converted_timeslots.append(dict(time=converted_timeslot, availability=False)) - date_required = datetime.datetime.strptime(date + ' 00:00:00', format_string).date() + date_required = datetime.datetime.strptime(date + " 00:00:00", format_string).date() converted_timeslots = filter_timeslots(date_required, converted_timeslots) return converted_timeslots + def get_available_slots_between(query_start_time, query_end_time, settings): records = _get_records(query_start_time, query_end_time, settings) timeslots = [] - appointment_duration = datetime.timedelta( - minutes=settings.appointment_duration) + appointment_duration = datetime.timedelta(minutes=settings.appointment_duration) for record in records: if record.day_of_week == WEEKDAYS[query_start_time.weekday()]: current_time = _deltatime_to_datetime(query_start_time, record.from_time) @@ -82,33 +90,35 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date, time, tz, contact): - format_string = '%Y-%m-%d %H:%M:%S' + format_string = "%Y-%m-%d %H:%M:%S" scheduled_time = datetime.datetime.strptime(date + " " + time, format_string) # Strip tzinfo from datetime objects since it's handled by the doctype - scheduled_time = scheduled_time.replace(tzinfo = None) + scheduled_time = scheduled_time.replace(tzinfo=None) scheduled_time = convert_to_system_timezone(tz, scheduled_time) - scheduled_time = scheduled_time.replace(tzinfo = None) + scheduled_time = scheduled_time.replace(tzinfo=None) # Create a appointment document from form - appointment = frappe.new_doc('Appointment') + appointment = frappe.new_doc("Appointment") appointment.scheduled_time = scheduled_time contact = json.loads(contact) - appointment.customer_name = contact.get('name', None) - appointment.customer_phone_number = contact.get('number', None) - appointment.customer_skype = contact.get('skype', None) - appointment.customer_details = contact.get('notes', None) - appointment.customer_email = contact.get('email', None) - appointment.status = 'Open' + appointment.customer_name = contact.get("name", None) + appointment.customer_phone_number = contact.get("number", None) + appointment.customer_skype = contact.get("skype", None) + appointment.customer_details = contact.get("notes", None) + appointment.customer_email = contact.get("email", None) + appointment.status = "Open" appointment.insert() return appointment + # Helper Functions def filter_timeslots(date, timeslots): filtered_timeslots = [] for timeslot in timeslots: - if(timeslot['time'].date() == date): + if timeslot["time"].date() == date: filtered_timeslots.append(timeslot) return filtered_timeslots + def convert_to_guest_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) local_timezone = pytz.timezone(frappe.utils.get_time_zone()) @@ -116,15 +126,18 @@ def convert_to_guest_timezone(guest_tz, datetimeobject): datetimeobject = datetimeobject.astimezone(guest_tz) return datetimeobject -def convert_to_system_timezone(guest_tz,datetimeobject): + +def convert_to_system_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) datetimeobject = guest_tz.localize(datetimeobject) system_tz = pytz.timezone(frappe.utils.get_time_zone()) datetimeobject = datetimeobject.astimezone(system_tz) return datetimeobject + def check_availabilty(timeslot, settings): - return frappe.db.count('Appointment', {'scheduled_time': timeslot}) < settings.number_of_agents + return frappe.db.count("Appointment", {"scheduled_time": timeslot}) < settings.number_of_agents + def _is_holiday(date, holiday_list): for holiday in holiday_list.holidays: @@ -136,7 +149,10 @@ def _is_holiday(date, holiday_list): def _get_records(start_time, end_time, settings): records = [] for record in settings.availability_of_slots: - if record.day_of_week == WEEKDAYS[start_time.weekday()] or record.day_of_week == WEEKDAYS[end_time.weekday()]: + if ( + record.day_of_week == WEEKDAYS[start_time.weekday()] + or record.day_of_week == WEEKDAYS[end_time.weekday()] + ): records.append(record) return records @@ -148,4 +164,4 @@ def _deltatime_to_datetime(date, deltatime): def _datetime_to_deltatime(date_time): midnight = datetime.datetime.combine(date_time.date(), datetime.time.min) - return (date_time-midnight) + return date_time - midnight diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py index dc36f4fc975..1a5ba9de7e5 100644 --- a/erpnext/www/book_appointment/verify/index.py +++ b/erpnext/www/book_appointment/verify/index.py @@ -8,11 +8,11 @@ def get_context(context): context.success = False return context - email = frappe.form_dict['email'] - appointment_name = frappe.form_dict['appointment'] + email = frappe.form_dict["email"] + appointment_name = frappe.form_dict["appointment"] if email and appointment_name: - appointment = frappe.get_doc('Appointment',appointment_name) + appointment = frappe.get_doc("Appointment", appointment_name) appointment.set_verified(email) context.success = True return context diff --git a/erpnext/www/lms/content.py b/erpnext/www/lms/content.py index b4d9793266b..99462ceeee5 100644 --- a/erpnext/www/lms/content.py +++ b/erpnext/www/lms/content.py @@ -1,32 +1,30 @@ - import frappe import erpnext.education.utils as utils no_cache = 1 + def get_context(context): # Load Query Parameters try: - program = frappe.form_dict['program'] - content = frappe.form_dict['content'] - content_type = frappe.form_dict['type'] - course = frappe.form_dict['course'] - topic = frappe.form_dict['topic'] + program = frappe.form_dict["program"] + content = frappe.form_dict["content"] + content_type = frappe.form_dict["type"] + course = frappe.form_dict["course"] + topic = frappe.form_dict["topic"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect - # Check if user has access to the content has_program_access = utils.allowed_program_access(program) has_content_access = allowed_content_access(program, content, content_type) if frappe.session.user == "Guest" or not has_program_access or not has_content_access: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect - # Set context for content to be displayer context.content = frappe.get_doc(content_type, content).as_dict() context.content_type = content_type @@ -35,35 +33,43 @@ def get_context(context): context.topic = topic topic = frappe.get_doc("Topic", topic) - content_list = [{'content_type':item.content_type, 'content':item.content} for item in topic.topic_content] + content_list = [ + {"content_type": item.content_type, "content": item.content} for item in topic.topic_content + ] # Set context for progress numbers - context.position = content_list.index({'content': content, 'content_type': content_type}) + context.position = content_list.index({"content": content, "content_type": content_type}) context.length = len(content_list) # Set context for navigation context.previous = get_previous_content(content_list, context.position) context.next = get_next_content(content_list, context.position) + def get_next_content(content_list, current_index): try: return content_list[current_index + 1] except IndexError: return None + def get_previous_content(content_list, current_index): if current_index == 0: return None else: return content_list[current_index - 1] + def allowed_content_access(program, content, content_type): - contents_of_program = frappe.db.sql("""select `tabTopic Content`.content, `tabTopic Content`.content_type + contents_of_program = frappe.db.sql( + """select `tabTopic Content`.content, `tabTopic Content`.content_type from `tabCourse Topic`, `tabProgram Course`, `tabTopic Content` where `tabCourse Topic`.parent = `tabProgram Course`.course and `tabTopic Content`.parent = `tabCourse Topic`.topic - and `tabProgram Course`.parent = %(program)s""", {'program': program}) + and `tabProgram Course`.parent = %(program)s""", + {"program": program}, + ) return (content, content_type) in contents_of_program diff --git a/erpnext/www/lms/course.py b/erpnext/www/lms/course.py index 6f1f0f208e9..840beee3ad2 100644 --- a/erpnext/www/lms/course.py +++ b/erpnext/www/lms/course.py @@ -1,27 +1,28 @@ - import frappe import erpnext.education.utils as utils no_cache = 1 + def get_context(context): try: - program = frappe.form_dict['program'] - course_name = frappe.form_dict['name'] + program = frappe.form_dict["program"] + course_name = frappe.form_dict["name"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.education_settings = frappe.get_single("Education Settings") - course = frappe.get_doc('Course', course_name) + course = frappe.get_doc("Course", course_name) context.program = program context.course = course context.topics = course.get_topics() - context.has_access = utils.allowed_program_access(context.program) + context.has_access = utils.allowed_program_access(context.program) context.progress = get_topic_progress(context.topics, course, context.program) + def get_topic_progress(topics, course, program): progress = {topic.name: utils.get_topic_progress(topic, course.name, program) for topic in topics} return progress diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py index 97d474befd1..782ac481a06 100644 --- a/erpnext/www/lms/index.py +++ b/erpnext/www/lms/index.py @@ -1,14 +1,14 @@ - import frappe import erpnext.education.utils as utils no_cache = 1 + def get_context(context): context.education_settings = frappe.get_single("Education Settings") if not context.education_settings.enable_lms: - frappe.local.flags.redirect_location = '/' + frappe.local.flags.redirect_location = "/" raise frappe.Redirect context.featured_programs = get_featured_programs() diff --git a/erpnext/www/lms/profile.py b/erpnext/www/lms/profile.py index 5e51ddc6c16..c4c1cd78eb7 100644 --- a/erpnext/www/lms/profile.py +++ b/erpnext/www/lms/profile.py @@ -1,27 +1,37 @@ - import frappe import erpnext.education.utils as utils no_cache = 1 + def get_context(context): if frappe.session.user == "Guest": - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.student = utils.get_current_student() if not context.student: - context.student = frappe.get_doc('User', frappe.session.user) + context.student = frappe.get_doc("User", frappe.session.user) context.progress = get_program_progress(context.student.name) + def get_program_progress(student): - enrolled_programs = frappe.get_all("Program Enrollment", filters={'student':student}, fields=['program']) + enrolled_programs = frappe.get_all( + "Program Enrollment", filters={"student": student}, fields=["program"] + ) student_progress = [] for list_item in enrolled_programs: program = frappe.get_doc("Program", list_item.program) progress = utils.get_program_progress(program) completion = utils.get_program_completion(program) - student_progress.append({'program': program.program_name, 'name': program.name, 'progress':progress, 'completion': completion}) + student_progress.append( + { + "program": program.program_name, + "name": program.name, + "progress": progress, + "completion": completion, + } + ) return student_progress diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py index a7c713caed0..1df2aa5bacd 100644 --- a/erpnext/www/lms/program.py +++ b/erpnext/www/lms/program.py @@ -1,4 +1,3 @@ - import frappe from frappe import _ @@ -6,11 +5,12 @@ import erpnext.education.utils as utils no_cache = 1 + def get_context(context): try: - program = frappe.form_dict['program'] + program = frappe.form_dict["program"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.education_settings = frappe.get_single("Education Settings") @@ -19,12 +19,14 @@ def get_context(context): context.has_access = utils.allowed_program_access(program) context.progress = get_course_progress(context.courses, context.program) + def get_program(program_name): try: - return frappe.get_doc('Program', program_name) + return frappe.get_doc("Program", program_name) except frappe.DoesNotExistError: frappe.throw(_("Program {0} does not exist.").format(program_name)) + def get_course_progress(courses, program): progress = {course.name: utils.get_course_progress(course, program) for course in courses} return progress or {} diff --git a/erpnext/www/lms/topic.py b/erpnext/www/lms/topic.py index 684437cbf22..7783211a41b 100644 --- a/erpnext/www/lms/topic.py +++ b/erpnext/www/lms/topic.py @@ -1,24 +1,25 @@ - import frappe import erpnext.education.utils as utils no_cache = 1 + def get_context(context): try: - course = frappe.form_dict['course'] - program = frappe.form_dict['program'] - topic = frappe.form_dict['topic'] + course = frappe.form_dict["course"] + program = frappe.form_dict["program"] + topic = frappe.form_dict["topic"] except KeyError: - frappe.local.flags.redirect_location = '/lms' + frappe.local.flags.redirect_location = "/lms" raise frappe.Redirect context.program = program context.course = course context.topic = frappe.get_doc("Topic", topic) context.contents = get_contents(context.topic, course, program) - context.has_access = utils.allowed_program_access(program) + context.has_access = utils.allowed_program_access(program) + def get_contents(topic, course, program): student = utils.get_current_student() @@ -28,19 +29,29 @@ def get_contents(topic, course, program): progress = [] if contents: for content in contents: - if content.doctype in ('Article', 'Video'): + if content.doctype in ("Article", "Video"): if student: status = utils.check_content_completion(content.name, content.doctype, course_enrollment.name) else: status = True - progress.append({'content': content, 'content_type': content.doctype, 'completed': status}) - elif content.doctype == 'Quiz': + progress.append({"content": content, "content_type": content.doctype, "completed": status}) + elif content.doctype == "Quiz": if student: - status, score, result, time_taken = utils.check_quiz_completion(content, course_enrollment.name) + status, score, result, time_taken = utils.check_quiz_completion( + content, course_enrollment.name + ) else: status = False score = None result = None - progress.append({'content': content, 'content_type': content.doctype, 'completed': status, 'score': score, 'result': result}) + progress.append( + { + "content": content, + "content_type": content.doctype, + "completed": status, + "score": score, + "result": result, + } + ) return progress diff --git a/erpnext/www/payment_setup_certification.py b/erpnext/www/payment_setup_certification.py index c87f4687c8c..5d62d60f5eb 100644 --- a/erpnext/www/payment_setup_certification.py +++ b/erpnext/www/payment_setup_certification.py @@ -1,20 +1,24 @@ - import frappe no_cache = 1 + def get_context(context): - if frappe.session.user != 'Guest': + if frappe.session.user != "Guest": context.all_certifications = get_all_certifications_of_a_member() context.show_sidebar = True def get_all_certifications_of_a_member(): - '''Returns all certifications''' + """Returns all certifications""" all_certifications = [] - all_certifications = frappe.db.sql(""" select cc.name,cc.from_date,cc.to_date,ca.amount,ca.currency + all_certifications = frappe.db.sql( + """ select cc.name,cc.from_date,cc.to_date,ca.amount,ca.currency from `tabCertified Consultant` cc inner join `tabCertification Application` ca on cc.certification_application = ca.name - where paid = 1 and email = %(user)s order by cc.to_date desc""" ,{'user': frappe.session.user},as_dict=True) + where paid = 1 and email = %(user)s order by cc.to_date desc""", + {"user": frappe.session.user}, + as_dict=True, + ) return all_certifications diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py index fecc05b54d1..7b493c3c288 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -3,6 +3,7 @@ from frappe import _ sitemap = 1 + def get_context(context): context.body_class = "product-page" @@ -18,13 +19,9 @@ def get_context(context): context.no_cache = 1 + def get_slideshow(slideshow): - values = { - 'show_indicators': 1, - 'show_controls': 1, - 'rounded': 1, - 'slider_name': "Categories" - } + values = {"show_indicators": 1, "show_controls": 1, "rounded": 1, "slider_name": "Categories"} slideshow = frappe.get_cached_doc("Website Slideshow", slideshow) slides = slideshow.get({"doctype": "Website Slideshow Item"}) for index, slide in enumerate(slides, start=1): @@ -37,9 +34,10 @@ def get_slideshow(slideshow): return values + def get_tabs(categories): tab_values = { - 'title': _("Shop by Category"), + "title": _("Shop by Category"), } categorical_data = get_category_records(categories) @@ -48,15 +46,17 @@ def get_tabs(categories): # pre-render cards for each tab tab_values[f"tab_{index + 1}_content"] = frappe.render_template( "erpnext/www/shop-by-category/category_card_section.html", - {"data": categorical_data[tab], "type": tab} + {"data": categorical_data[tab], "type": tab}, ) return tab_values + def get_category_records(categories): categorical_data = {} for category in categories: if category == "item_group": - categorical_data["item_group"] = frappe.db.sql(""" + categorical_data["item_group"] = frappe.db.sql( + """ Select name, parent_item_group, is_group, image, route from @@ -65,7 +65,8 @@ def get_category_records(categories): parent_item_group = 'All Item Groups' and show_in_website = 1 """, - as_dict=1) + as_dict=1, + ) else: doctype = frappe.unscrub(category) fields = ["name"] @@ -79,7 +80,7 @@ def get_category_records(categories): from `tab{doctype}` """, - as_dict=1) + as_dict=1, + ) return categorical_data - diff --git a/erpnext/www/support/index.py b/erpnext/www/support/index.py index 1d198ed3ed1..aa00e928804 100644 --- a/erpnext/www/support/index.py +++ b/erpnext/www/support/index.py @@ -1,10 +1,9 @@ - import frappe def get_context(context): context.no_cache = 1 - context.align_greeting = '' + context.align_greeting = "" setting = frappe.get_doc("Support Settings") context.greeting_title = setting.greeting_title @@ -17,18 +16,22 @@ def get_context(context): if favorite_articles: for article in favorite_articles: name_list.append(article.name) - for record in (frappe.get_all("Help Article", + for record in frappe.get_all( + "Help Article", fields=["title", "content", "route", "category"], - filters={"name": ['not in', tuple(name_list)], "published": 1}, - order_by="creation desc", limit=(6-len(favorite_articles)))): + filters={"name": ["not in", tuple(name_list)], "published": 1}, + order_by="creation desc", + limit=(6 - len(favorite_articles)), + ): favorite_articles.append(record) context.favorite_article_list = get_favorite_articles(favorite_articles) context.help_article_list = get_help_article_list() + def get_favorite_articles_by_page_view(): return frappe.db.sql( - """ + """ SELECT t1.name as name, t1.title as title, @@ -44,32 +47,42 @@ def get_favorite_articles_by_page_view(): GROUP BY route ORDER BY count DESC LIMIT 6; - """, as_dict=True) + """, + as_dict=True, + ) + def get_favorite_articles(favorite_articles): - favorite_article_list=[] + favorite_article_list = [] for article in favorite_articles: description = frappe.utils.strip_html(article.content) if len(description) > 120: - description = description[:120] + '...' + description = description[:120] + "..." favorite_article_dict = { - 'title': article.title, - 'description': description, - 'route': article.route, - 'category': article.category, + "title": article.title, + "description": description, + "route": article.route, + "category": article.category, } favorite_article_list.append(favorite_article_dict) return favorite_article_list + def get_help_article_list(): - help_article_list=[] + help_article_list = [] category_list = frappe.get_all("Help Category", fields="name") for category in category_list: - help_articles = frappe.get_all("Help Article", fields="*", filters={"category": category.name, "published": 1}, order_by="modified desc", limit=5) + help_articles = frappe.get_all( + "Help Article", + fields="*", + filters={"category": category.name, "published": 1}, + order_by="modified desc", + limit=5, + ) if help_articles: help_aricles_per_caetgory = { - 'category': category, - 'articles': help_articles, + "category": category, + "articles": help_articles, } help_article_list.append(help_aricles_per_caetgory) return help_article_list diff --git a/setup.py b/setup.py index 35c15b4a00d..ce6ddeb153b 100644 --- a/setup.py +++ b/setup.py @@ -7,23 +7,22 @@ import re from setuptools import find_packages, setup # get version from __version__ variable in erpnext/__init__.py -_version_re = re.compile(r'__version__\s+=\s+(.*)') +_version_re = re.compile(r"__version__\s+=\s+(.*)") -with open('requirements.txt') as f: - install_requires = f.read().strip().split('\n') +with open("requirements.txt") as f: + install_requires = f.read().strip().split("\n") -with open('erpnext/__init__.py', 'rb') as f: - version = str(ast.literal_eval(_version_re.search( - f.read().decode('utf-8')).group(1))) +with open("erpnext/__init__.py", "rb") as f: + version = str(ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1))) setup( - name='erpnext', + name="erpnext", version=version, - description='Open Source ERP', - author='Frappe Technologies', - author_email='info@erpnext.com', + description="Open Source ERP", + author="Frappe Technologies", + author_email="info@erpnext.com", packages=find_packages(), zip_safe=False, include_package_data=True, - install_requires=install_requires + install_requires=install_requires, ) From 14c838604074d41814d1b1d6719f791dcb4e5972 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 29 Mar 2022 17:31:43 +0530 Subject: [PATCH 725/951] chore: ignore black formatting commit in blame --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b5e46fb8107..9b5ea2f58ab 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -17,3 +17,6 @@ f0bcb753fb7ebbb64bb0d6906d431d002f0f7d8f # imports cleanup 4b2be2999f2203493b49bf74c5b440d49e38b5e3 + +# formatting with black +c07713b860505211db2af685e2e950bf5dd7dd3a From 39ff7b0b06dcb7826b33668a556eb7529fcf8b44 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Mar 2022 18:18:13 +0530 Subject: [PATCH 726/951] fix: validate 0 transfer qty in stock entry (#30476) (#30479) (cherry picked from commit b80fac03afe99d3c0b9c0f09d6eb34573c5692cf) Co-authored-by: Ankush Menat --- .../stock/doctype/stock_entry/stock_entry.py | 6 ++- .../doctype/stock_entry/test_stock_entry.py | 48 ++++--------------- 2 files changed, 13 insertions(+), 41 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 60ce65eda11..bafb138b31d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -226,12 +226,16 @@ class StockEntry(StockController): def set_transfer_qty(self): for item in self.get("items"): if not flt(item.qty): - frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx)) + frappe.throw(_("Row {0}: Qty is mandatory").format(item.idx), title=_("Zero quantity")) if not flt(item.conversion_factor): frappe.throw(_("Row {0}: UOM Conversion Factor is mandatory").format(item.idx)) item.transfer_qty = flt( flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item) ) + if not flt(item.transfer_qty): + frappe.throw( + _("Row {0}: Qty in Stock UOM can not be zero.").format(item.idx), title=_("Zero quantity") + ) def update_cost_in_project(self): if self.work_order and not frappe.db.get_value( diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 9992b77c00e..323ab6af819 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -52,7 +52,6 @@ class TestStockEntry(FrappeTestCase): def tearDown(self): frappe.db.rollback() frappe.set_user("Administrator") - frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") def test_fifo(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) @@ -768,13 +767,12 @@ class TestStockEntry(FrappeTestCase): fg_cost, flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2) ) + @change_settings("Manufacturing Settings", {"material_consumption": 1}) def test_work_order_manufacture_with_material_consumption(self): from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as _make_stock_entry, ) - frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1") - bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item", "is_default": 1, "docstatus": 1}) work_order = frappe.new_doc("Work Order") @@ -984,43 +982,6 @@ class TestStockEntry(FrappeTestCase): repack.insert() self.assertRaises(frappe.ValidationError, repack.submit) - # def test_material_consumption(self): - # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") - # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") - - # from erpnext.manufacturing.doctype.work_order.work_order \ - # import make_stock_entry as _make_stock_entry - # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", - # "is_default": 1, "docstatus": 1}) - - # work_order = frappe.new_doc("Work Order") - # work_order.update({ - # "company": "_Test Company", - # "fg_warehouse": "_Test Warehouse 1 - _TC", - # "production_item": "_Test FG Item 2", - # "bom_no": bom_no, - # "qty": 4.0, - # "stock_uom": "_Test UOM", - # "wip_warehouse": "_Test Warehouse - _TC", - # "additional_operating_cost": 1000, - # "use_multi_level_bom": 1 - # }) - # work_order.insert() - # work_order.submit() - - # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100) - # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20) - - # item_quantity = { - # '_Test Item': 2.0, - # '_Test Item 2': 12.0, - # '_Test Serialized Item With Series': 6.0 - # } - - # stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2)) - # for d in stock_entry.get('items'): - # self.assertEqual(item_quantity.get(d.item_code), d.qty) - def test_customer_provided_parts_se(self): create_item( "CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0 @@ -1315,6 +1276,13 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.total_incoming_value, se.total_outgoing_value) + def test_transfer_qty_validation(self): + se = make_stock_entry(item_code="_Test Item", do_not_save=True, qty=0.001, rate=100) + se.items[0].uom = "Kg" + se.items[0].conversion_factor = 0.002 + + self.assertRaises(frappe.ValidationError, se.save) + def make_serialized_item(**args): args = frappe._dict(args) From c9cd3ee9aa645aa1155d6da3f783227bd86659be Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Mar 2022 13:36:00 +0530 Subject: [PATCH 727/951] fix: credit limit validation in delivery note (cherry picked from commit c122882884d8a4d2f504eb53bef9003333bd393c) --- erpnext/stock/doctype/delivery_note/delivery_note.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 304857dd0f8..7205758a8e4 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -280,8 +280,11 @@ class DeliveryNote(SellingController): ) if bypass_credit_limit_check_at_sales_order: - validate_against_credit_limit = True - extra_amount = self.base_grand_total + for d in self.get("items"): + if not d.against_sales_invoice: + validate_against_credit_limit = True + extra_amount = self.base_grand_total + break else: for d in self.get("items"): if not (d.against_sales_order or d.against_sales_invoice): From 46e6d16c498e36fa8bbca6cbbc81fb3322c86e48 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 12:05:26 +0530 Subject: [PATCH 728/951] fix: Dont set `idx` while adding WO items to Stock Entry (backport #30377) (#30485) * fix: Dont set `idx` while adding WO items to Stock Entry - `idx` must be computed by base document's `self.append()` function, so do not set it (cherry picked from commit a787ebb7325294eb95cca1c91b6257bd9cdab88f) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py * chore: Remove redundant idx query and value setting - idx can be removed from `select_columns` as it is already in the main query - setting idx to '' is not required as it is not used further (cherry picked from commit 639d380c1f333ba2162aacd62511d2593db156bc) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.py * test: idx mapping correctness (cherry picked from commit fa3b953cf7ee9d4c5fa8330bb56499efa9339113) * fix: Linter (cherry picked from commit b5ad626d23fc4246e5665b3df78d9eca181b0d4f) * fix: resolve conflicts Co-authored-by: marination Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/bom/bom.py | 4 +-- .../doctype/work_order/test_work_order.py | 30 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 5 ++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e12cd3131cd..8fd6050b4f9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1015,7 +1015,7 @@ def get_bom_items_as_dict( query = query.format( table="BOM Scrap Item", where_conditions="", - select_columns=", bom_item.idx, item.description, is_process_loss", + select_columns=", item.description, is_process_loss", is_stock_item=is_stock_item, qty_field="stock_qty", ) @@ -1028,7 +1028,7 @@ def get_bom_items_as_dict( is_stock_item=is_stock_item, qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, - bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, + bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier, bom_item.description, bom_item.base_rate as rate """, ) items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 3709c7db101..1e9b3ba1137 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1070,6 +1070,36 @@ class TestWorkOrder(FrappeTestCase): except frappe.MandatoryError: self.fail("Batch generation causing failing in Work Order") + @change_settings( + "Manufacturing Settings", + {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, + ) + def test_manufacture_entry_mapped_idx_with_exploded_bom(self): + """Test if WO containing BOM with partial exploded items and scrap items, maps idx correctly.""" + test_stock_entry.make_stock_entry( + item_code="_Test Item", + target="_Test Warehouse - _TC", + basic_rate=5000.0, + qty=2, + ) + test_stock_entry.make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + basic_rate=1000.0, + qty=2, + ) + + wo_order = make_wo_order_test_record( + qty=1, + use_multi_level_bom=1, + skip_transfer=1, + ) + + ste_manu = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1)) + + for index, row in enumerate(ste_manu.get("items"), start=1): + self.assertEqual(index, row.idx) + def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index bafb138b31d..cc7317a2cd8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1383,8 +1383,8 @@ class StockEntry(StockController): def set_scrap_items(self): if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) - for item in itervalues(scrap_item_dict): - item.idx = "" + + for item in scrap_item_dict.values(): if self.pro_doc and self.pro_doc.scrap_warehouse: item["to_warehouse"] = self.pro_doc.scrap_warehouse @@ -1900,7 +1900,6 @@ class StockEntry(StockController): se_child.is_process_loss = item_row.get("is_process_loss", 0) for field in [ - "idx", "po_detail", "original_item", "expense_account", From c7d8d60de6d754ea608437ddbaad505f89937c8c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 15:07:56 +0530 Subject: [PATCH 729/951] fix: explicitly check if additional salary is recurring while fetching components for payroll (backport #30489) (#30491) Co-authored-by: Rucha Mahabal --- .../additional_salary/additional_salary.py | 21 +++++- .../test_additional_salary.py | 65 +++++++++++++++---- 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index 74b780e9e9d..f57d9d37cf1 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -145,6 +145,8 @@ class AdditionalSalary(Document): @frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type): + from frappe.query_builder import Criterion + comp_type = "Earning" if component_type == "earnings" else "Deduction" additional_sal = frappe.qb.DocType("Additional Salary") @@ -168,8 +170,23 @@ def get_additional_salaries(employee, start_date, end_date, component_type): & (additional_sal.type == comp_type) ) .where( - additional_sal.payroll_date[start_date:end_date] - | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date)) + Criterion.any( + [ + Criterion.all( + [ # is recurring and additional salary dates fall within the payroll period + additional_sal.is_recurring == 1, + additional_sal.from_date <= end_date, + additional_sal.to_date >= end_date, + ] + ), + Criterion.all( + [ # is not recurring and additional salary's payroll date falls within the payroll period + additional_sal.is_recurring == 0, + additional_sal.payroll_date[start_date:end_date], + ] + ), + ] + ) ) .run(as_dict=True) ) diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index 7d5d9e02f34..bd739368a0a 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -4,7 +4,8 @@ import unittest import frappe -from frappe.utils import add_days, nowdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, add_months, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee @@ -16,19 +17,10 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure -class TestAdditionalSalary(unittest.TestCase): +class TestAdditionalSalary(FrappeTestCase): def setUp(self): setup_test() - def tearDown(self): - for dt in [ - "Salary Slip", - "Additional Salary", - "Salary Structure Assignment", - "Salary Structure", - ]: - frappe.db.sql("delete from `tab%s`" % dt) - def test_recurring_additional_salary(self): amount = 0 salary_component = None @@ -46,19 +38,66 @@ class TestAdditionalSalary(unittest.TestCase): if earning.salary_component == "Recurring Salary Component": amount = earning.amount salary_component = earning.salary_component + break self.assertEqual(amount, add_sal.amount) self.assertEqual(salary_component, add_sal.salary_component) + def test_non_recurring_additional_salary(self): + amount = 0 + salary_component = None + date = nowdate() -def get_additional_salary(emp_id): + emp_id = make_employee("test_additional@salary.com") + frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(date, 1800)) + salary_structure = make_salary_structure( + "Test Salary Structure Additional Salary", "Monthly", employee=emp_id + ) + add_sal = get_additional_salary(emp_id, recurring=False, payroll_date=date) + + ss = make_employee_salary_slip( + "test_additional@salary.com", "Monthly", salary_structure=salary_structure.name + ) + + amount, salary_component = None, None + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + break + + self.assertEqual(amount, add_sal.amount) + self.assertEqual(salary_component, add_sal.salary_component) + + # should not show up in next months + ss.posting_date = add_months(date, 1) + ss.start_date = ss.end_date = None + ss.earnings = [] + ss.deductions = [] + ss.save() + + amount, salary_component = None, None + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + break + + self.assertIsNone(amount) + self.assertIsNone(salary_component) + + +def get_additional_salary(emp_id, recurring=True, payroll_date=None): create_salary_component("Recurring Salary Component") add_sal = frappe.new_doc("Additional Salary") add_sal.employee = emp_id add_sal.salary_component = "Recurring Salary Component" - add_sal.is_recurring = 1 + + add_sal.is_recurring = 1 if recurring else 0 add_sal.from_date = add_days(nowdate(), -50) add_sal.to_date = add_days(nowdate(), 180) + add_sal.payroll_date = payroll_date + add_sal.amount = 5000 add_sal.currency = erpnext.get_default_currency() add_sal.save() From 3f3717952c94af428c3d4c919aceb6b8cbae7679 Mon Sep 17 00:00:00 2001 From: Anoop Date: Wed, 30 Mar 2022 16:36:15 +0530 Subject: [PATCH 730/951] fix: cast array slice index integer while splitting serial_nos array (#30468) --- erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index f2586825cc7..7ef39c26aa0 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1315,7 +1315,7 @@ def get_serial_nos_for_job_card(row, wo_doc): used_serial_nos.extend(get_serial_nos(d.serial_no)) serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos))) - row.serial_no = "\n".join(serial_nos[0 : row.job_card_qty]) + row.serial_no = "\n".join(serial_nos[0 : cint(row.job_card_qty)]) def validate_operation_data(row): From 22ec1a4996f9ab16f02111906ead4159c38dce0c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 16:37:00 +0530 Subject: [PATCH 731/951] fix: enable row deletion in reference table (#30492) (cherry picked from commit 500870b2b0eca0407f06198d497bc7031950bab9) Co-authored-by: rahib-hassan --- erpnext/accounts/doctype/payment_order/payment_order.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js index 9074defa577..7d85d89c452 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.js +++ b/erpnext/accounts/doctype/payment_order/payment_order.js @@ -12,7 +12,6 @@ frappe.ui.form.on('Payment Order', { }); frm.set_df_property('references', 'cannot_add_rows', true); - frm.set_df_property('references', 'cannot_delete_rows', true); }, refresh: function(frm) { if (frm.doc.docstatus == 0) { From 20ef6ab5bf56827975778d28110998deb35c5f77 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 31 Mar 2022 12:04:53 +0530 Subject: [PATCH 732/951] fix: review changes --- erpnext/crm/doctype/contract/contract.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 86eaf07f1b5..f186e87813e 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -18,12 +18,14 @@ class Contract(Document): name = self.party_name if self.contract_template: - name += " - {} Agreement".format(self.contract_template) + name = f"{name} - {self.contract_template} Agreement" # If identical, append contract name with the next number in the iteration if frappe.db.exists("Contract", name): - count = len(frappe.get_all("Contract", filters={"name": ["like", "%{}%".format(name)]})) - name = "{} - {}".format(name, count) + count = frappe.db.count('Contract', filters={ + 'name': ('like', f"%{name}%"), + }) + name = f"{name} - {count}" self.name = _(name) From 444625a0ca387a4752b867070e7a77c0a40bd863 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 31 Mar 2022 13:38:37 +0530 Subject: [PATCH 733/951] fix: linter issues --- erpnext/crm/doctype/contract/contract.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index f186e87813e..dd4b86dd8b6 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -22,9 +22,12 @@ class Contract(Document): # If identical, append contract name with the next number in the iteration if frappe.db.exists("Contract", name): - count = frappe.db.count('Contract', filters={ - 'name': ('like', f"%{name}%"), - }) + count = frappe.db.count( + "Contract", + filters={ + "name": ("like", f"%{name}%"), + }, + ) name = f"{name} - {count}" self.name = _(name) From 168cc353ca86578e751d68d072cdf52786449283 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Mar 2022 12:43:18 +0530 Subject: [PATCH 734/951] fix: Account currency validation (cherry picked from commit d4cc7c553eaec88edb128da3224cb94650cc12ed) --- erpnext/accounts/doctype/account/account.py | 4 +- .../accounts/doctype/account/test_account.py | 76 ++++--------------- 2 files changed, 19 insertions(+), 61 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 150f68b7bd3..c71ea3648b9 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -204,7 +204,9 @@ class Account(NestedSet): if not self.account_currency: self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency") - elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"): + gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency") + + if gl_currency and self.account_currency != gl_currency: if frappe.db.get_value("GL Entry", {"account": self.name}): frappe.throw(_("Currency can not be changed after making entries using some other currency")) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index efc063de56a..a6d44882eb6 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -241,71 +241,27 @@ class TestAccount(unittest.TestCase): for doc in to_delete: frappe.delete_doc("Account", doc) + def test_validate_account_currency(self): + from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry -def _make_test_records(verbose=None): - from frappe.test_runner import make_test_objects + if not frappe.db.get_value("Account", "Test Currency Account - _TC"): + acc = frappe.new_doc("Account") + acc.account_name = "Test Currency Account" + acc.parent_account = "Tax Assets - _TC" + acc.company = "_Test Company" + acc.insert() + else: + acc = frappe.get_doc("Account", "Test Currency Account - _TC") - accounts = [ - # [account_name, parent_account, is_group] - ["_Test Bank", "Bank Accounts", 0, "Bank", None], - ["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"], - ["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"], - ["_Test Cash", "Cash In Hand", 0, "Cash", None], - ["_Test Account Stock Expenses", "Direct Expenses", 1, None, None], - ["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], - ["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None], - ["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], - ["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None], - ["_Test Employee Advance", "Current Liabilities", 0, None, None], - ["_Test Account Tax Assets", "Current Assets", 1, None, None], - ["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None], - ["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None], - ["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None], - ["_Test Account Cost for Goods Sold", "Expenses", 0, None, None], - ["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None], - ["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None], - ["_Test Account S&H Education Cess", "_Test Account Tax Assets", 0, "Tax", None], - ["_Test Account CST", "Direct Expenses", 0, "Tax", None], - ["_Test Account Discount", "Direct Expenses", 0, None, None], - ["_Test Write Off", "Indirect Expenses", 0, None, None], - ["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None], - ["_Test Account Sales", "Direct Income", 0, None, None], - # related to Account Inventory Integration - ["_Test Account Stock In Hand", "Current Assets", 0, None, None], - # fixed asset depreciation - ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], - ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], - ["_Test Depreciations", "Expenses", 0, None, None], - ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], - # Receivable / Payable Account - ["_Test Receivable", "Current Assets", 0, "Receivable", None], - ["_Test Payable", "Current Liabilities", 0, "Payable", None], - ["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"], - ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"], - ] + self.assertEqual(acc.account_currency, "INR") - for company, abbr in [ - ["_Test Company", "_TC"], - ["_Test Company 1", "_TC1"], - ["_Test Company with perpetual inventory", "TCP1"], - ]: - test_objects = make_test_objects( - "Account", - [ - { - "doctype": "Account", - "account_name": account_name, - "parent_account": parent_account + " - " + abbr, - "company": company, - "is_group": is_group, - "account_type": account_type, - "account_currency": currency, - } - for account_name, parent_account, is_group, account_type, currency in accounts - ], + # Make a JV against this account + make_journal_entry( + "Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True ) - return test_objects + acc.account_currency = "USD" + self.assertRaises(frappe.ValidationError, acc.save) def get_inventory_account(company, warehouse=None): From ebb2e975cd198bdfe7654f66d6eadc4a79fd3485 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Mar 2022 21:07:56 +0530 Subject: [PATCH 735/951] fix: make test record (cherry picked from commit d93edbc859268fee20be208f84d66b7542bd52ee) --- .../accounts/doctype/account/test_account.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index a6d44882eb6..f9c9173af08 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -264,6 +264,72 @@ class TestAccount(unittest.TestCase): self.assertRaises(frappe.ValidationError, acc.save) +def _make_test_records(verbose=None): + from frappe.test_runner import make_test_objects + + accounts = [ + # [account_name, parent_account, is_group] + ["_Test Bank", "Bank Accounts", 0, "Bank", None], + ["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"], + ["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"], + ["_Test Cash", "Cash In Hand", 0, "Cash", None], + ["_Test Account Stock Expenses", "Direct Expenses", 1, None, None], + ["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], + ["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None], + ["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None], + ["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None], + ["_Test Employee Advance", "Current Liabilities", 0, None, None], + ["_Test Account Tax Assets", "Current Assets", 1, None, None], + ["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None], + ["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None], + ["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None], + ["_Test Account Cost for Goods Sold", "Expenses", 0, None, None], + ["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None], + ["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None], + ["_Test Account S&H Education Cess", "_Test Account Tax Assets", 0, "Tax", None], + ["_Test Account CST", "Direct Expenses", 0, "Tax", None], + ["_Test Account Discount", "Direct Expenses", 0, None, None], + ["_Test Write Off", "Indirect Expenses", 0, None, None], + ["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None], + ["_Test Account Sales", "Direct Income", 0, None, None], + # related to Account Inventory Integration + ["_Test Account Stock In Hand", "Current Assets", 0, None, None], + # fixed asset depreciation + ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], + ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], + ["_Test Depreciations", "Expenses", 0, None, None], + ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], + # Receivable / Payable Account + ["_Test Receivable", "Current Assets", 0, "Receivable", None], + ["_Test Payable", "Current Liabilities", 0, "Payable", None], + ["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"], + ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"], + ] + + for company, abbr in [ + ["_Test Company", "_TC"], + ["_Test Company 1", "_TC1"], + ["_Test Company with perpetual inventory", "TCP1"], + ]: + test_objects = make_test_objects( + "Account", + [ + { + "doctype": "Account", + "account_name": account_name, + "parent_account": parent_account + " - " + abbr, + "company": company, + "is_group": is_group, + "account_type": account_type, + "account_currency": currency, + } + for account_name, parent_account, is_group, account_type, currency in accounts + ], + ) + + return test_objects + + def get_inventory_account(company, warehouse=None): account = None if warehouse: From 00cb0d029464a713fdc1046c7be1c3c05a745cab Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Mar 2022 10:47:27 +0530 Subject: [PATCH 736/951] fix(asset): do not validate warehouse on asset purchase (cherry picked from commit 136466d255651ba29be16248e822c2a374114c67) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 49a01dcaa19..a026445831d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -245,6 +245,7 @@ class PurchaseInvoice(BuyingController): def validate_warehouse(self, for_validate=True): if self.update_stock and for_validate: +<<<<<<< HEAD for d in self.get("items"): if not d.warehouse: frappe.throw( @@ -252,6 +253,12 @@ class PurchaseInvoice(BuyingController): "Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}" ).format(d.idx, d.item_code, self.company) ) +======= + for d in self.get('items'): + if not d.warehouse and not d.is_fixed_asset: + frappe.throw(_("Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}"). + format(d.idx, d.item_code, self.company), exc=WarehouseMissingError) +>>>>>>> 136466d255 (fix(asset): do not validate warehouse on asset purchase) super(PurchaseInvoice, self).validate_warehouse() From ad91d57a419a1c0d7fd8b85e12aaf32e6725439a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Mar 2022 18:43:33 +0530 Subject: [PATCH 737/951] perf: skip warehouse validation for non-stock items (cherry picked from commit 6528218ac31001e04e6b5ebfa0f3d429e296742f) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 ++++++ erpnext/controllers/accounts_controller.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a026445831d..716bda0d33e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -245,9 +245,15 @@ class PurchaseInvoice(BuyingController): def validate_warehouse(self, for_validate=True): if self.update_stock and for_validate: +<<<<<<< HEAD <<<<<<< HEAD for d in self.get("items"): if not d.warehouse: +======= + stock_items = self.get_stock_items() + for d in self.get("items"): + if not d.warehouse and d.item_code in stock_items: +>>>>>>> 6528218ac3 (perf: skip warehouse validation for non-stock items) frappe.throw( _( "Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f0143962874..964d8fb06fe 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1265,6 +1265,9 @@ class AccountsController(TransactionBase): return get_company_default(self.company, fieldname, ignore_validation=ignore_validation) def get_stock_items(self): + if hasattr(self, "_stock_items") and self._stock_items: + return self._stock_items + stock_items = [] item_codes = list(set(item.item_code for item in self.get("items"))) if item_codes: @@ -1280,6 +1283,7 @@ class AccountsController(TransactionBase): ) ] + self._stock_items = stock_items return stock_items def set_total_advance_paid(self): From c36b5d9ab835793ccc5b084406024d45b3e0022a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 11:41:52 +0530 Subject: [PATCH 738/951] perf: skip warehouse validation for non-stock items (cherry picked from commit 199a6da960c0419a16db59e7c93b2d23405efdc4) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/controllers/accounts_controller.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 964d8fb06fe..f06ced36373 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1265,12 +1265,10 @@ class AccountsController(TransactionBase): return get_company_default(self.company, fieldname, ignore_validation=ignore_validation) def get_stock_items(self): - if hasattr(self, "_stock_items") and self._stock_items: - return self._stock_items - stock_items = [] item_codes = list(set(item.item_code for item in self.get("items"))) if item_codes: +<<<<<<< HEAD stock_items = [ r[0] for r in frappe.db.sql( @@ -1282,8 +1280,12 @@ class AccountsController(TransactionBase): item_codes, ) ] +======= + stock_items = frappe.db.get_values( + "Item", {"name": ["in", item_codes], "is_stock_item": 1}, pluck="name", cache=True + ) +>>>>>>> 199a6da960 (perf: skip warehouse validation for non-stock items) - self._stock_items = stock_items return stock_items def set_total_advance_paid(self): From b4a10d571f50e3f9e64d2011f148c4868161a82e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 12:48:00 +0530 Subject: [PATCH 739/951] fix(test): Item MacBook does not exist (cherry picked from commit 4623a1bc5777f8bb16a147eae52b9f8e695612af) --- erpnext/assets/doctype/asset/test_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fcb2ad2277b..79455bb1b4e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -68,7 +68,7 @@ class TestAsset(AssetSetup): def test_item_exists(self): asset = create_asset(item_code="MacBook", do_not_save=1) - self.assertRaises(frappe.DoesNotExistError, asset.save) + self.assertRaises(frappe.ValidationError, asset.save) def test_validate_item(self): asset = create_asset(item_code="MacBook Pro", do_not_save=1) From bd2061d6f398b28877ea1005a19a6d444b20b7bb Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 11:36:30 +0530 Subject: [PATCH 740/951] fix: prevent multiple save on applying coupon code (cherry picked from commit d5fd8e0ba6f2b6297339dd12bb7cbf1dc0c3155e) --- .../selling/page/point_of_sale/pos_payment.js | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 1e9f6d7d920..326ee59d11a 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -170,20 +170,24 @@ erpnext.PointOfSale.Payment = class { }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); - } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { - frappe.show_alert({ - message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), - indicator: "orange" - }); + if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { + if (!frm.doc.ignore_pricing_rule) { + frm.applying_pos_coupon_code = true + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc), + () => (frm.applying_pos_coupon_code = false) + ]); + } else if (frm.doc.ignore_pricing_rule) { + frappe.show_alert({ + message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), + indicator: "orange" + }); + } } }); From a3a7dc9ce6dfe1b0c5215e710be514097403d7e0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 12:47:35 +0530 Subject: [PATCH 741/951] fix(pos): customer group filter in customer selector (cherry picked from commit 2f82e237ef650ab49d08dfd3eeaf56f1f299c84a) --- .../selling/page/point_of_sale/point_of_sale.py | 15 ++++++++++++++- .../selling/page/point_of_sale/pos_controller.js | 13 +++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 4efb1a03f7e..26a80763c9b 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -8,7 +8,7 @@ import frappe from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability -from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups +from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups def search_by_term(search_term, warehouse, price_list): @@ -324,3 +324,16 @@ def set_customer_info(fieldname, customer, value=""): contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) frappe.db.set_value("Customer", customer, "mobile_no", value) contact_doc.save() + +@frappe.whitelist() +def get_pos_profile_data(pos_profile): + pos_profile = frappe.get_doc('POS Profile', pos_profile) + pos_profile = pos_profile.as_dict() + + _customer_groups_with_children = [] + for row in pos_profile.customer_groups: + children = get_child_nodes('Customer Group', row.customer_group) + _customer_groups_with_children.extend(children) + + pos_profile.customer_groups = _customer_groups_with_children + return pos_profile \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ea8459f970b..d66c6e46860 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -119,10 +119,15 @@ erpnext.PointOfSale.Controller = class { this.allow_negative_stock = flt(message.allow_negative_stock) || false; }); - frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => { - Object.assign(this.settings, profile); - this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group); - this.make_app(); + frappe.call({ + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data", + args: { "pos_profile": this.pos_profile }, + callback: (res) => { + const profile = res.message; + Object.assign(this.settings, profile); + this.settings.customer_groups = profile.customer_groups.map(group => group.name); + this.make_app(); + } }); } From 3b583c6c48ba559cef329a21340ac1ba20eac42c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 13:24:11 +0530 Subject: [PATCH 742/951] fix(pos): specific case when serialized item not removed (cherry picked from commit 4afb47e869b70c66a0fa51934a3c65de54f98b4e) --- .../selling/page/point_of_sale/pos_controller.js | 4 ++-- .../selling/page/point_of_sale/pos_item_details.js | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index d66c6e46860..49e85ecc7a0 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -560,7 +560,7 @@ erpnext.PointOfSale.Controller = class { if (this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); - if (this.check_serial_batch_selection_needed(item_row)) + if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible')) this.edit_item_details_of(item_row); } @@ -709,7 +709,7 @@ erpnext.PointOfSale.Controller = class { frappe.dom.freeze(); const { doctype, name, current_item } = this.item_details; - frappe.model.set_value(doctype, name, 'qty', 0) + return frappe.model.set_value(doctype, name, 'qty', 0) .then(() => { frappe.model.clear_doc(doctype, name); this.update_cart_html(current_item, true); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index a3ad0025943..1d720f7291a 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -60,12 +60,18 @@ erpnext.PointOfSale.ItemDetails = class { return item && item.name == this.current_item.name; } - toggle_item_details_section(item) { + async toggle_item_details_section(item) { const current_item_changed = !this.compare_with_current_item(item); // if item is null or highlighted cart item is clicked twice const hide_item_details = !Boolean(item) || !current_item_changed; + if ((!hide_item_details && current_item_changed) || hide_item_details) { + // if item details is being closed OR if item details is opened but item is changed + // in both cases, if the current item is a serialized item, then validate and remove the item + await this.validate_serial_batch_item(); + } + this.events.toggle_item_selector(!hide_item_details); this.toggle_component(!hide_item_details); @@ -83,7 +89,6 @@ erpnext.PointOfSale.ItemDetails = class { this.render_form(item); this.events.highlight_cart_item(item); } else { - this.validate_serial_batch_item(); this.current_item = {}; } } @@ -103,11 +108,11 @@ erpnext.PointOfSale.ItemDetails = class { (serialized && batched && (no_batch_selected || no_serial_selected))) { frappe.show_alert({ - message: __("Item will be removed since no serial / batch no selected."), + message: __("Item is removed since no serial / batch no selected."), indicator: 'orange' }); frappe.utils.play_sound("cancel"); - this.events.remove_item_from_cart(); + return this.events.remove_item_from_cart(); } } From 76f83ea559a417c0c830517eed80dde958df983e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Mar 2022 13:53:58 +0530 Subject: [PATCH 743/951] fix(pos): allow validating stock on save (cherry picked from commit aff74087755cfb6c2e9a582886c26267b6fc5667) # Conflicts: # erpnext/accounts/doctype/pos_invoice/pos_invoice.py --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 13 +++++++++++++ .../accounts/doctype/pos_profile/pos_profile.json | 13 +++++++++++-- .../selling/page/point_of_sale/pos_controller.js | 11 +++++++++-- erpnext/selling/page/point_of_sale/pos_payment.js | 2 +- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 9115ee88541..a9be620b993 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -212,11 +212,24 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_stock_availablility(self): +<<<<<<< HEAD if self.is_return or self.docstatus != 1: return allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") for d in self.get("items"): is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) +======= + if self.is_return: + return + + if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'): + return + + from erpnext.stock.stock_ledger import is_negative_stock_allowed + + for d in self.get('items'): + is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) +>>>>>>> aff7408775 (fix(pos): allow validating stock on save) if is_service_item: return if d.serial_no: diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 9c9f37bba27..11646a6517d 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -22,6 +22,7 @@ "hide_images", "hide_unavailable_items", "auto_add_item_to_cart", + "validate_stock_on_save", "column_break_16", "update_stock", "ignore_pricing_rule", @@ -351,6 +352,12 @@ { "fieldname": "column_break_25", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_stock_on_save", + "fieldtype": "Check", + "label": "Validate Stock on Save" } ], "icon": "icon-cog", @@ -378,10 +385,11 @@ "link_fieldname": "pos_profile" } ], - "modified": "2021-10-14 14:17:00.469298", + "modified": "2022-03-21 13:29:28.480533", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { @@ -404,5 +412,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 49e85ecc7a0..6974bed4f1f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -720,7 +720,14 @@ erpnext.PointOfSale.Controller = class { } async save_and_checkout() { - this.frm.is_dirty() && await this.frm.save(); - this.payment.checkout(); + if (this.frm.is_dirty()) { + // only move to payment section if save is successful + frappe.route_hooks.after_save = () => this.payment.checkout(); + return this.frm.save( + null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error + ); + } else { + this.payment.checkout(); + } } }; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 326ee59d11a..b4ece46e6e1 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -172,7 +172,7 @@ erpnext.PointOfSale.Payment = class { frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { if (!frm.doc.ignore_pricing_rule) { - frm.applying_pos_coupon_code = true + frm.applying_pos_coupon_code = true; frappe.run_serially([ () => frm.doc.ignore_pricing_rule=1, () => frm.trigger('ignore_pricing_rule'), From 36845a87e0fb86cf1d385a9e17c3d925c62898d2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Mar 2022 17:41:49 +0530 Subject: [PATCH 744/951] fix(pos): remove returned sr. nos. from pos reserved sr. nos. list (cherry picked from commit f2ae63cbfdc0262f45ccae5991927e49e5c38c4c) # Conflicts: # erpnext/accounts/doctype/pos_invoice/pos_invoice.py # erpnext/stock/doctype/serial_no/serial_no.py --- .../doctype/pos_invoice/pos_invoice.json | 4 +- .../doctype/pos_invoice/pos_invoice.py | 10 ++- .../doctype/pos_invoice/test_pos_invoice.py | 72 +++++++++++++++++++ erpnext/stock/doctype/serial_no/serial_no.py | 55 +++++++++++++- 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 0c6e7edeb02..b8500270d1a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -264,7 +264,6 @@ "print_hide": 1 }, { - "allow_on_submit": 1, "default": "0", "fieldname": "is_return", "fieldtype": "Check", @@ -1573,7 +1572,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:11:53.871828", + "modified": "2022-03-22 13:00:24.166684", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", @@ -1623,6 +1622,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index a9be620b993..8df2e3587df 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -17,7 +17,11 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_due_date, get_party_account from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty -from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivered_serial_nos, + get_pos_reserved_serial_nos, + get_serial_nos, +) class POSInvoice(SalesInvoice): @@ -179,12 +183,16 @@ class POSInvoice(SalesInvoice): ) def validate_delivered_serial_nos(self, item): +<<<<<<< HEAD serial_nos = get_serial_nos(item.serial_no) delivered_serial_nos = frappe.db.get_list( "Serial No", {"item_code": item.item_code, "name": ["in", serial_nos], "sales_invoice": ["is", "set"]}, pluck="name", ) +======= + delivered_serial_nos = get_delivered_serial_nos(item.serial_no) +>>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) if delivered_serial_nos: bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos)) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index ee35614cdce..0925614cbc7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -765,6 +765,78 @@ class TestPOSInvoice(unittest.TestCase): pos_inv.delete() pr.delete() + def test_delivered_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint('before_test_delivered_serial_no_case') + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + dn = create_delivery_note( + item_code="_Test Serialized Item With Series", serial_no=serial_no + ) + + delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") + self.assertEquals(delivery_document_no, dn.name) + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=True + ) + + self.assertRaises(frappe.ValidationError, pos_inv.submit) + + finally: + frappe.db.rollback(save_point='before_test_delivered_serial_no_case') + frappe.set_user("Administrator") + + def test_returned_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint('before_test_returned_serial_no_case') + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + ) + + pos_return = make_sales_return(pos_inv.name) + pos_return.flags.ignore_validate = True + pos_return.insert() + pos_return.submit() + + pos_reserved_serial_nos = get_pos_reserved_serial_nos({ + 'item_code': '_Test Serialized Item With Series', + 'warehouse': '_Test Warehouse - _TC' + }) + self.assertTrue(serial_no not in pos_reserved_serial_nos) + + finally: + frappe.db.rollback(save_point='before_test_returned_serial_no_case') + frappe.set_user("Administrator") def create_pos_invoice(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index bc30878789f..e9a5d53fdd7 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -820,11 +820,35 @@ def auto_fetch_serial_number( return sorted([d.get("name") for d in serial_numbers]) +def get_delivered_serial_nos(serial_nos): + ''' + Returns serial numbers that delivered from the list of serial numbers + ''' + from frappe.query_builder.functions import Coalesce + + SerialNo = frappe.qb.DocType("Serial No") + serial_nos = get_serial_nos(serial_nos) + query = ( + frappe.qb + .from_(SerialNo) + .select(SerialNo.name) + .where( + (SerialNo.name.isin(serial_nos)) + & (Coalesce(SerialNo.delivery_document_type, "") != "") + ) + ) + + result = query.run() + if result and len(result) > 0: + delivered_serial_nos = [row[0] for row in result] + return delivered_serial_nos + @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): filters = json.loads(filters) +<<<<<<< HEAD pos_transacted_sr_nos = frappe.db.sql( """select item.serial_no as serial_no from `tabPOS Invoice` p, `tabPOS Invoice Item` item @@ -839,10 +863,39 @@ def get_pos_reserved_serial_nos(filters): filters, as_dict=1, ) +======= + POSInvoice = frappe.qb.DocType("POS Invoice") + POSInvoiceItem = frappe.qb.DocType("POS Invoice Item") + query = frappe.qb.from_( + POSInvoice + ).from_( + POSInvoiceItem + ).select( + POSInvoice.is_return, + POSInvoiceItem.serial_no + ).where( + (POSInvoice.name == POSInvoiceItem.parent) + & (POSInvoice.docstatus == 1) + & (POSInvoiceItem.docstatus == 1) + & (POSInvoiceItem.item_code == filters.get('item_code')) + & (POSInvoiceItem.warehouse == filters.get('warehouse')) + & (POSInvoiceItem.serial_no.isnotnull()) + & (POSInvoiceItem.serial_no != '') + ) + + pos_transacted_sr_nos = query.run(as_dict=True) +>>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) reserved_sr_nos = [] + returned_sr_nos = [] for d in pos_transacted_sr_nos: - reserved_sr_nos += get_serial_nos(d.serial_no) + if d.is_return == 0: + reserved_sr_nos += get_serial_nos(d.serial_no) + elif d.is_return == 1: + returned_sr_nos += get_serial_nos(d.serial_no) + + for sr_no in returned_sr_nos: + reserved_sr_nos.remove(sr_no) return reserved_sr_nos From 3bb0716dffa3d72732d3e0ce70a870635e440502 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Mar 2022 16:20:37 +0530 Subject: [PATCH 745/951] fix(pos): cannot close the pos if sr. no. is sold & returned (cherry picked from commit cf51a0a1b8ec45bf653c9478bd57cee676b384d9) # Conflicts: # erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py # erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py --- .../pos_invoice_merge_log.py | 65 ++++++++++++++++++- .../test_pos_invoice_merge_log.py | 65 +++++++++++++++++++ .../pos_invoice_reference.json | 23 ++++++- 3 files changed, 149 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 89af6ad4e57..d97ce1cffc0 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -66,7 +66,7 @@ class POSInvoiceMergeLog(Document): frappe.throw(msg) def on_submit(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] returns = [d for d in pos_invoice_docs if d.get("is_return") == 1] sales = [d for d in pos_invoice_docs if d.get("is_return") == 0] @@ -83,7 +83,7 @@ class POSInvoiceMergeLog(Document): self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) def on_cancel(self): - pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] self.update_pos_invoices(pos_invoice_docs) self.cancel_linked_invoices() @@ -279,11 +279,16 @@ def get_all_unconsolidated_invoices(): "status": ["not in", ["Consolidated"]], "docstatus": 1, } +<<<<<<< HEAD pos_invoices = frappe.db.get_all( "POS Invoice", filters=filters, fields=["name as pos_invoice", "posting_date", "grand_total", "customer"], ) +======= + pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, + fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against']) +>>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) return pos_invoices @@ -326,6 +331,7 @@ def unconsolidate_pos_invoices(closing_entry): else: cancel_merge_logs(merge_logs, closing_entry) +<<<<<<< HEAD def create_merge_logs(invoice_by_customer, closing_entry=None): try: @@ -340,6 +346,61 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): merge_log.set("pos_invoices", invoices) merge_log.save(ignore_permissions=True) merge_log.submit() +======= +def split_invoices(invoices): + ''' + Splits invoices into multiple groups + Use-case: + If a serial no is sold and later it is returned + then split the invoices such that the selling entry is merged first and then the return entry + ''' + # Input + # invoices = [ + # {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}, + # {'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, + # {'pos_invoice': 'Invoice with SR#2', 'is_return': 0} + # ] + # Output + # _invoices = [ + # [{'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}], + # [{'pos_invoice': 'Invoice with SR#1', 'is_return': 1}, {'pos_invoice': 'Invoice with SR#2', 'is_return': 0}], + # ] + + _invoices = [] + special_invoices = [] + pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against] + for pos_invoice in pos_return_docs: + for item in pos_invoice.items: + if not item.serial_no: continue + + return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) + if return_against_is_added: break + + return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated' + if return_against_is_consolidated: break + + pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against] + _invoices.append(pos_invoice_row) + special_invoices.append(pos_invoice.return_against) + break + + _invoices.append([d for d in invoices if d.pos_invoice not in special_invoices]) + + return _invoices + +def create_merge_logs(invoice_by_customer, closing_entry=None): + try: + for customer, invoices in invoice_by_customer.items(): + for _invoices in split_invoices(invoices): + merge_log = frappe.new_doc('POS Invoice Merge Log') + merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() + merge_log.customer = customer + merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None + + merge_log.set('pos_invoices', _invoices) + merge_log.save(ignore_permissions=True) + merge_log.submit() +>>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) if closing_entry: closing_entry.set_status(update=True, status="Submitted") diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index c6d8179c40c..e3cd2863f70 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -391,3 +391,68 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") +<<<<<<< HEAD +======= + + def test_serial_no_case_1(self): + ''' + Create a POS Invoice with serial no + Create a Return Invoice with serial no + Create a POS Invoice with serial no again + Consolidate the invoices + + The first POS Invoice should be consolidated with a separate single Merge Log + The second and third POS Invoice should be consolidated with a single Merge Log + ''' + + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1 + ) + pos_inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 + }) + pos_inv.submit() + + pos_inv_cn = make_sales_return(pos_inv.name) + pos_inv_cn.paid_amount = -100 + pos_inv_cn.submit() + + pos_inv2 = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1 + ) + pos_inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 + }) + pos_inv.submit() + + consolidate_pos_invoices() + + pos_inv.load_from_db() + pos_inv2.load_from_db() + + self.assertNotEqual(pos_inv.consolidated_invoice, pos_inv2.consolidated_invoice) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") +>>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) diff --git a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json index 205c4ede901..387c4b0f360 100644 --- a/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json +++ b/erpnext/accounts/doctype/pos_invoice_reference/pos_invoice_reference.json @@ -9,7 +9,9 @@ "posting_date", "column_break_3", "customer", - "grand_total" + "grand_total", + "is_return", + "return_against" ], "fields": [ { @@ -48,11 +50,27 @@ "in_list_view": 1, "label": "Amount", "reqd": 1 + }, + { + "default": "0", + "fetch_from": "pos_invoice.is_return", + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "read_only": 1 + }, + { + "fetch_from": "pos_invoice.return_against", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against", + "options": "POS Invoice", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-05-29 15:08:42.194979", + "modified": "2022-03-24 13:32:02.366257", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Reference", @@ -61,5 +79,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 650274e973c9f143e3499c0c9e8b86fa4bedca74 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Mar 2022 17:56:27 +0530 Subject: [PATCH 746/951] fix: sider issues (cherry picked from commit cb4873c019f7694f64b94b5845e2fa73a602103a) --- .../pos_invoice_merge_log/pos_invoice_merge_log.py | 9 ++++++--- erpnext/stock/doctype/serial_no/serial_no.py | 11 +++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index d97ce1cffc0..77f9755de16 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -371,13 +371,16 @@ def split_invoices(invoices): pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against] for pos_invoice in pos_return_docs: for item in pos_invoice.items: - if not item.serial_no: continue + if not item.serial_no: + continue return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) - if return_against_is_added: break + if return_against_is_added: + break return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated' - if return_against_is_consolidated: break + if return_against_is_consolidated: + break pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against] _invoices.append(pos_invoice_row) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index e9a5d53fdd7..3b4c358e425 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -828,14 +828,9 @@ def get_delivered_serial_nos(serial_nos): SerialNo = frappe.qb.DocType("Serial No") serial_nos = get_serial_nos(serial_nos) - query = ( - frappe.qb - .from_(SerialNo) - .select(SerialNo.name) - .where( - (SerialNo.name.isin(serial_nos)) - & (Coalesce(SerialNo.delivery_document_type, "") != "") - ) + query = frappe.qb.select(SerialNo.name).from_(SerialNo).where( + (SerialNo.name.isin(serial_nos)) + & (Coalesce(SerialNo.delivery_document_type, "") != "") ) result = query.run() From cf3e09588fdb803b4b084d4eecefc6dc78c01fdd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Mar 2022 17:59:34 +0530 Subject: [PATCH 747/951] fix: test cases (cherry picked from commit 1b556d1c5353c9fca94f9d00496776e1c2a69839) --- erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py | 1 + .../doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 0925614cbc7..0493b8a90a7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -439,6 +439,7 @@ class TestPOSInvoice(unittest.TestCase): ) si.get("items")[0].serial_no = serial_nos[0] + si.update_stock = 1 si.insert() si.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index e3cd2863f70..71cb87f75cd 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -442,7 +442,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv2.append('payments', { 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 }) - pos_inv.submit() + pos_inv2.submit() consolidate_pos_invoices() From 82aea2b998d3100c23fc2a6de6ed65c2f7eb488a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Mar 2022 10:51:30 +0530 Subject: [PATCH 748/951] fix: set is_return & return_against in POS Invoice Reference table (cherry picked from commit 16253a2f7207d8c4182e92a45247144fe24489db) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 9 +++++ ...eturn_against_in_pos_invoice_references.py | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 50050afd316..23e86332f74 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,8 +352,17 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs +<<<<<<< HEAD erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances +======= +erpnext.patches.v14_0.update_batch_valuation_flag +erpnext.patches.v14_0.delete_non_profit_doctypes +erpnext.patches.v14_0.update_employee_advance_status +erpnext.patches.v13_0.add_cost_center_in_loans +erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items +erpnext.patches.v13_0.set_return_against_in_pos_invoice_references +>>>>>>> 16253a2f72 (fix: set is_return & return_against in POS Invoice Reference table) diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py new file mode 100644 index 00000000000..6c24f520274 --- /dev/null +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -0,0 +1,38 @@ +import frappe + + +def execute(): + ''' + Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table. + ''' + + POSClosingEntry = frappe.qb.DocType("POS Closing Entry") + open_pos_closing_entries = ( + frappe.qb + .from_(POSClosingEntry) + .select(POSClosingEntry.name) + .where(POSClosingEntry.docstatus == 0) + .run(pluck=True) + ) + + if not open_pos_closing_entries: + return + + POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") + POSInvoice = frappe.qb.DocType("POS Invoice") + pos_invoice_references = ( + frappe.qb + .from_(POSInvoiceReference) + .join(POSInvoice) + .on(POSInvoiceReference.pos_invoice == POSInvoice.name) + .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) + .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) + .run(as_dict=True) + ) + + for row in pos_invoice_references: + frappe.db.set_value("POS Invoice Reference", row.name, "is_return", row.is_return) + if row.is_return: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", row.return_against) + else: + frappe.db.set_value("POS Invoice Reference", row.name, "return_against", None) From 47567c66c11a9c8013d6fce36aa32733c6d396e6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Mar 2022 14:26:12 +0530 Subject: [PATCH 749/951] chore: ignore rules for QB formatting (cherry picked from commit e0c36d87e0198392c5a7369c31690f5922ea5d64) --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.flake8 b/.flake8 index 5735456ae7d..4ff88403244 100644 --- a/.flake8 +++ b/.flake8 @@ -29,6 +29,8 @@ ignore = B950, W191, E124, # closing bracket, irritating while writing QB code + E131, # continuation line unaligned for hanging indent + E123, # closing bracket does not match indentation of opening bracket's line max-line-length = 200 exclude=.github/helper/semgrep_rules From a51b32b7e013ca6e1bbb5f31c956f07c11111c89 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 14:19:53 +0530 Subject: [PATCH 750/951] fix: merge conflicts --- .../doctype/purchase_invoice/purchase_invoice.py | 12 ------------ erpnext/controllers/accounts_controller.py | 14 -------------- 2 files changed, 26 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 716bda0d33e..8500d57f44b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -245,26 +245,14 @@ class PurchaseInvoice(BuyingController): def validate_warehouse(self, for_validate=True): if self.update_stock and for_validate: -<<<<<<< HEAD -<<<<<<< HEAD - for d in self.get("items"): - if not d.warehouse: -======= stock_items = self.get_stock_items() for d in self.get("items"): if not d.warehouse and d.item_code in stock_items: ->>>>>>> 6528218ac3 (perf: skip warehouse validation for non-stock items) frappe.throw( _( "Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}" ).format(d.idx, d.item_code, self.company) ) -======= - for d in self.get('items'): - if not d.warehouse and not d.is_fixed_asset: - frappe.throw(_("Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}"). - format(d.idx, d.item_code, self.company), exc=WarehouseMissingError) ->>>>>>> 136466d255 (fix(asset): do not validate warehouse on asset purchase) super(PurchaseInvoice, self).validate_warehouse() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f06ced36373..61db921f9cc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1268,23 +1268,9 @@ class AccountsController(TransactionBase): stock_items = [] item_codes = list(set(item.item_code for item in self.get("items"))) if item_codes: -<<<<<<< HEAD - stock_items = [ - r[0] - for r in frappe.db.sql( - """ - select name from `tabItem` - where name in (%s) and is_stock_item=1 - """ - % (", ".join((["%s"] * len(item_codes))),), - item_codes, - ) - ] -======= stock_items = frappe.db.get_values( "Item", {"name": ["in", item_codes], "is_stock_item": 1}, pluck="name", cache=True ) ->>>>>>> 199a6da960 (perf: skip warehouse validation for non-stock items) return stock_items From 0bafec23844727b27b8bf7c827fdd9129af92868 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 14:24:42 +0530 Subject: [PATCH 751/951] fix: merge conflicts --- .../doctype/pos_invoice/pos_invoice.py | 27 ++------ .../pos_invoice_merge_log.py | 69 ++++++++++--------- .../test_pos_invoice_merge_log.py | 35 ++++------ erpnext/patches.txt | 8 --- erpnext/stock/doctype/serial_no/serial_no.py | 57 ++++++--------- 5 files changed, 75 insertions(+), 121 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8df2e3587df..885e3882287 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -183,16 +183,7 @@ class POSInvoice(SalesInvoice): ) def validate_delivered_serial_nos(self, item): -<<<<<<< HEAD - serial_nos = get_serial_nos(item.serial_no) - delivered_serial_nos = frappe.db.get_list( - "Serial No", - {"item_code": item.item_code, "name": ["in", serial_nos], "sales_invoice": ["is", "set"]}, - pluck="name", - ) -======= delivered_serial_nos = get_delivered_serial_nos(item.serial_no) ->>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) if delivered_serial_nos: bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos)) @@ -220,24 +211,18 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_stock_availablility(self): -<<<<<<< HEAD - if self.is_return or self.docstatus != 1: - return - allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - for d in self.get("items"): - is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) -======= if self.is_return: return - if self.docstatus.is_draft() and not frappe.db.get_value('POS Profile', self.pos_profile, 'validate_stock_on_save'): + if self.docstatus.is_draft() and not frappe.db.get_value( + "POS Profile", self.pos_profile, "validate_stock_on_save" + ): return - from erpnext.stock.stock_ledger import is_negative_stock_allowed + allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - for d in self.get('items'): - is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item')) ->>>>>>> aff7408775 (fix(pos): allow validating stock on save) + for d in self.get("items"): + is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item")) if is_service_item: return if d.serial_no: diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 77f9755de16..d3a81fe61dc 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -5,7 +5,6 @@ import json import frappe -import six from frappe import _ from frappe.core.page.background_jobs.background_jobs import get_info from frappe.model.document import Document @@ -66,7 +65,9 @@ class POSInvoiceMergeLog(Document): frappe.throw(msg) def on_submit(self): - pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices + ] returns = [d for d in pos_invoice_docs if d.get("is_return") == 1] sales = [d for d in pos_invoice_docs if d.get("is_return") == 0] @@ -83,7 +84,9 @@ class POSInvoiceMergeLog(Document): self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) def on_cancel(self): - pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices + ] self.update_pos_invoices(pos_invoice_docs) self.cancel_linked_invoices() @@ -279,16 +282,18 @@ def get_all_unconsolidated_invoices(): "status": ["not in", ["Consolidated"]], "docstatus": 1, } -<<<<<<< HEAD pos_invoices = frappe.db.get_all( "POS Invoice", filters=filters, - fields=["name as pos_invoice", "posting_date", "grand_total", "customer"], + fields=[ + "name as pos_invoice", + "posting_date", + "grand_total", + "customer", + "is_return", + "return_against", + ], ) -======= - pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, - fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer', 'is_return', 'return_against']) ->>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) return pos_invoices @@ -331,29 +336,14 @@ def unconsolidate_pos_invoices(closing_entry): else: cancel_merge_logs(merge_logs, closing_entry) -<<<<<<< HEAD -def create_merge_logs(invoice_by_customer, closing_entry=None): - try: - for customer, invoices in six.iteritems(invoice_by_customer): - merge_log = frappe.new_doc("POS Invoice Merge Log") - merge_log.posting_date = ( - getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() - ) - merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None - - merge_log.set("pos_invoices", invoices) - merge_log.save(ignore_permissions=True) - merge_log.submit() -======= def split_invoices(invoices): - ''' + """ Splits invoices into multiple groups Use-case: If a serial no is sold and later it is returned then split the invoices such that the selling entry is merged first and then the return entry - ''' + """ # Input # invoices = [ # {'pos_invoice': 'Invoice with SR#1 & SR#2', 'is_return': 0}, @@ -368,17 +358,26 @@ def split_invoices(invoices): _invoices = [] special_invoices = [] - pos_return_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in invoices if d.is_return and d.return_against] + pos_return_docs = [ + frappe.get_cached_doc("POS Invoice", d.pos_invoice) + for d in invoices + if d.is_return and d.return_against + ] for pos_invoice in pos_return_docs: for item in pos_invoice.items: if not item.serial_no: continue - return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) + return_against_is_added = any( + d for d in _invoices if d.pos_invoice == pos_invoice.return_against + ) if return_against_is_added: break - return_against_is_consolidated = frappe.db.get_value('POS Invoice', pos_invoice.return_against, 'status', cache=True) == 'Consolidated' + return_against_is_consolidated = ( + frappe.db.get_value("POS Invoice", pos_invoice.return_against, "status", cache=True) + == "Consolidated" + ) if return_against_is_consolidated: break @@ -391,19 +390,21 @@ def split_invoices(invoices): return _invoices + def create_merge_logs(invoice_by_customer, closing_entry=None): try: for customer, invoices in invoice_by_customer.items(): for _invoices in split_invoices(invoices): - merge_log = frappe.new_doc('POS Invoice Merge Log') - merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate() + merge_log = frappe.new_doc("POS Invoice Merge Log") + merge_log.posting_date = ( + getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() + ) merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None + merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None - merge_log.set('pos_invoices', _invoices) + merge_log.set("pos_invoices", _invoices) merge_log.save(ignore_permissions=True) merge_log.submit() ->>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) if closing_entry: closing_entry.set_status(update=True, status="Submitted") diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 71cb87f75cd..9e696f18b6a 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -391,11 +391,9 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") -<<<<<<< HEAD -======= def test_serial_no_case_1(self): - ''' + """ Create a POS Invoice with serial no Create a Return Invoice with serial no Create a POS Invoice with serial no again @@ -403,7 +401,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): The first POS Invoice should be consolidated with a separate single Merge Log The second and third POS Invoice should be consolidated with a single Merge Log - ''' + """ from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -417,15 +415,13 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): init_user_and_profile() pos_inv = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=serial_no, - qty=1, - rate=100, - do_not_submit=1 + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1, ) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) pos_inv.submit() pos_inv_cn = make_sales_return(pos_inv.name) @@ -433,15 +429,13 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): pos_inv_cn.submit() pos_inv2 = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=serial_no, - qty=1, - rate=100, - do_not_submit=1 + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=1, ) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 100 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100}) pos_inv2.submit() consolidate_pos_invoices() @@ -455,4 +449,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") ->>>>>>> cf51a0a1b8 (fix(pos): cannot close the pos if sr. no. is sold & returned) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 23e86332f74..98e07783c11 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -352,17 +352,9 @@ erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs -<<<<<<< HEAD erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances -======= -erpnext.patches.v14_0.update_batch_valuation_flag -erpnext.patches.v14_0.delete_non_profit_doctypes -erpnext.patches.v14_0.update_employee_advance_status -erpnext.patches.v13_0.add_cost_center_in_loans -erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items erpnext.patches.v13_0.set_return_against_in_pos_invoice_references ->>>>>>> 16253a2f72 (fix: set is_return & return_against in POS Invoice Reference table) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 3b4c358e425..6d3969892f2 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -821,16 +821,17 @@ def auto_fetch_serial_number( def get_delivered_serial_nos(serial_nos): - ''' + """ Returns serial numbers that delivered from the list of serial numbers - ''' + """ from frappe.query_builder.functions import Coalesce SerialNo = frappe.qb.DocType("Serial No") serial_nos = get_serial_nos(serial_nos) - query = frappe.qb.select(SerialNo.name).from_(SerialNo).where( - (SerialNo.name.isin(serial_nos)) - & (Coalesce(SerialNo.delivery_document_type, "") != "") + query = ( + frappe.qb.select(SerialNo.name) + .from_(SerialNo) + .where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != "")) ) result = query.run() @@ -838,48 +839,30 @@ def get_delivered_serial_nos(serial_nos): delivered_serial_nos = [row[0] for row in result] return delivered_serial_nos + @frappe.whitelist() def get_pos_reserved_serial_nos(filters): if isinstance(filters, str): filters = json.loads(filters) -<<<<<<< HEAD - pos_transacted_sr_nos = frappe.db.sql( - """select item.serial_no as serial_no - from `tabPOS Invoice` p, `tabPOS Invoice Item` item - where p.name = item.parent - and p.consolidated_invoice is NULL - and p.docstatus = 1 - and item.docstatus = 1 - and item.item_code = %(item_code)s - and item.warehouse = %(warehouse)s - and item.serial_no is NOT NULL and item.serial_no != '' - """, - filters, - as_dict=1, - ) -======= POSInvoice = frappe.qb.DocType("POS Invoice") POSInvoiceItem = frappe.qb.DocType("POS Invoice Item") - query = frappe.qb.from_( - POSInvoice - ).from_( - POSInvoiceItem - ).select( - POSInvoice.is_return, - POSInvoiceItem.serial_no - ).where( - (POSInvoice.name == POSInvoiceItem.parent) - & (POSInvoice.docstatus == 1) - & (POSInvoiceItem.docstatus == 1) - & (POSInvoiceItem.item_code == filters.get('item_code')) - & (POSInvoiceItem.warehouse == filters.get('warehouse')) - & (POSInvoiceItem.serial_no.isnotnull()) - & (POSInvoiceItem.serial_no != '') + query = ( + frappe.qb.from_(POSInvoice) + .from_(POSInvoiceItem) + .select(POSInvoice.is_return, POSInvoiceItem.serial_no) + .where( + (POSInvoice.name == POSInvoiceItem.parent) + & (POSInvoice.docstatus == 1) + & (POSInvoiceItem.docstatus == 1) + & (POSInvoiceItem.item_code == filters.get("item_code")) + & (POSInvoiceItem.warehouse == filters.get("warehouse")) + & (POSInvoiceItem.serial_no.isnotnull()) + & (POSInvoiceItem.serial_no != "") + ) ) pos_transacted_sr_nos = query.run(as_dict=True) ->>>>>>> f2ae63cbfd (fix(pos): remove returned sr. nos. from pos reserved sr. nos. list) reserved_sr_nos = [] returned_sr_nos = [] From ab7417c26ab2653326bbfbe31c6ef3170cdff8aa Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 14:38:40 +0530 Subject: [PATCH 752/951] fix: invalid keyword argument 'pluck' --- ...eturn_against_in_pos_invoice_references.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py index 6c24f520274..6af9617bcee 100644 --- a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -2,18 +2,19 @@ import frappe def execute(): - ''' + """ Fetch and Set is_return & return_against from POS Invoice in POS Invoice References table. - ''' + """ POSClosingEntry = frappe.qb.DocType("POS Closing Entry") open_pos_closing_entries = ( - frappe.qb - .from_(POSClosingEntry) - .select(POSClosingEntry.name) - .where(POSClosingEntry.docstatus == 0) - .run(pluck=True) - ) + frappe.qb.from_(POSClosingEntry) + .select(POSClosingEntry.name) + .where(POSClosingEntry.docstatus == 0) + .run() + ) + if open_pos_closing_entries: + open_pos_closing_entries = [d[0] for d in open_pos_closing_entries] if not open_pos_closing_entries: return @@ -21,13 +22,12 @@ def execute(): POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") POSInvoice = frappe.qb.DocType("POS Invoice") pos_invoice_references = ( - frappe.qb - .from_(POSInvoiceReference) - .join(POSInvoice) - .on(POSInvoiceReference.pos_invoice == POSInvoice.name) - .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) - .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) - .run(as_dict=True) + frappe.qb.from_(POSInvoiceReference) + .join(POSInvoice) + .on(POSInvoiceReference.pos_invoice == POSInvoice.name) + .select(POSInvoiceReference.name, POSInvoice.is_return, POSInvoice.return_against) + .where(POSInvoiceReference.parent.isin(open_pos_closing_entries)) + .run(as_dict=True) ) for row in pos_invoice_references: From 84247e91f32ae40082c87d48e9855a2c06f8c3ac Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 31 Mar 2022 11:47:20 +0530 Subject: [PATCH 753/951] fix: Add non-existent Item check and cleanup in `validate_for_items` - Added a validation if invalid item code ia passed via data import/API, etc. - Used DB APIs instead of raw sql - Separated checks into separate functions - Added return types to functions - Better variable naming and removed redundant utils import in function (cherry picked from commit 982a246eecfa0bbe9f1d31c06905227b0671267d) --- erpnext/buying/utils.py | 97 +++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 38 deletions(-) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index f97cd5e9dd0..e904af0dce3 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -3,19 +3,19 @@ import json +from typing import Dict import frappe from frappe import _ -from frappe.utils import cint, cstr, flt +from frappe.utils import cint, cstr, flt, getdate from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_end_of_life -def update_last_purchase_rate(doc, is_submit): +def update_last_purchase_rate(doc, is_submit) -> None: """updates last_purchase_rate in item table for each item""" - import frappe.utils - this_purchase_date = frappe.utils.getdate(doc.get("posting_date") or doc.get("transaction_date")) + this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date")) for d in doc.get("items"): # get last purchase details @@ -41,7 +41,7 @@ def update_last_purchase_rate(doc, is_submit): frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate)) -def validate_for_items(doc): +def validate_for_items(doc) -> None: items = [] for d in doc.get("items"): if not d.qty: @@ -49,40 +49,11 @@ def validate_for_items(doc): continue frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code)) - # update with latest quantities - bin = frappe.db.sql( - """select projected_qty from `tabBin` where - item_code = %s and warehouse = %s""", - (d.item_code, d.warehouse), - as_dict=1, - ) - - f_lst = { - "projected_qty": bin and flt(bin[0]["projected_qty"]) or 0, - "ordered_qty": 0, - "received_qty": 0, - } - if d.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"): - f_lst.pop("received_qty") - for x in f_lst: - if d.meta.get_field(x): - d.set(x, f_lst[x]) - - item = frappe.db.sql( - """select is_stock_item, - is_sub_contracted_item, end_of_life, disabled from `tabItem` where name=%s""", - d.item_code, - as_dict=1, - )[0] - + set_stock_levels(row=d) # update with latest quantities + item = validate_item_and_get_basic_data(row=d) + validate_stock_item_warehouse(row=d, item=item) validate_end_of_life(d.item_code, item.end_of_life, item.disabled) - # validate stock item - if item.is_stock_item == 1 and d.qty and not d.warehouse and not d.get("delivered_by_supplier"): - frappe.throw( - _("Warehouse is mandatory for stock Item {0} in row {1}").format(d.item_code, d.idx) - ) - items.append(cstr(d.item_code)) if ( @@ -93,7 +64,57 @@ def validate_for_items(doc): frappe.throw(_("Same item cannot be entered multiple times.")) -def check_on_hold_or_closed_status(doctype, docname): +def set_stock_levels(row) -> None: + projected_qty = frappe.db.get_value( + "Bin", + { + "item_code": row.item_code, + "warehouse": row.warehouse, + }, + "projected_qty", + ) + + qty_data = { + "projected_qty": flt(projected_qty), + "ordered_qty": 0, + "received_qty": 0, + } + if row.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"): + qty_data.pop("received_qty") + + for field in qty_data: + if row.meta.get_field(field): + row.set(field, qty_data[field]) + + +def validate_item_and_get_basic_data(row) -> Dict: + item = frappe.db.get_values( + "Item", + filters={"name": row.item_code}, + fieldname=["is_stock_item", "is_sub_contracted_item", "end_of_life", "disabled"], + as_dict=1, + ) + if not item: + frappe.throw(_("Row #{0}: Item {1} does not exist").format(row.idx, frappe.bold(row.item_code))) + + return item[0] + + +def validate_stock_item_warehouse(row, item) -> None: + if ( + item.is_stock_item == 1 + and row.qty + and not row.warehouse + and not row.get("delivered_by_supplier") + ): + frappe.throw( + _("Row #{1}: Warehouse is mandatory for stock Item {0}").format( + frappe.bold(row.item_code), row.idx + ) + ) + + +def check_on_hold_or_closed_status(doctype, docname) -> None: status = frappe.db.get_value(doctype, docname, "status") if status in ("Closed", "On Hold"): From 40a154e64db8c89ff59831d0c2994970c0dad3a6 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 31 Mar 2022 13:07:51 +0530 Subject: [PATCH 754/951] fix: (test) change expected exception due to https://github.com/frappe/frappe/pull/16454 (cherry picked from commit 93f6346ceaaba582fc84ca090049bda48997e48b) --- erpnext/assets/doctype/asset/test_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fcb2ad2277b..79455bb1b4e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -68,7 +68,7 @@ class TestAsset(AssetSetup): def test_item_exists(self): asset = create_asset(item_code="MacBook", do_not_save=1) - self.assertRaises(frappe.DoesNotExistError, asset.save) + self.assertRaises(frappe.ValidationError, asset.save) def test_validate_item(self): asset = create_asset(item_code="MacBook Pro", do_not_save=1) From 7317a0696b7370631cd22b8faafe3594adcb9909 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Mar 2022 19:03:07 +0530 Subject: [PATCH 755/951] refactor: Add exception handling in background job within BOM Update Tool (cherry picked from commit f57725f8fa016b9826e8fdf2f14dbf1a3d9991f7) --- .../bom_update_tool/bom_update_tool.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 9f120d175ed..11092702ca3 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -117,21 +117,32 @@ def update_latest_price_in_all_boms(): def replace_bom(args): - frappe.db.auto_commit_on_many_writes = 1 - args = frappe._dict(args) - - doc = frappe.get_doc("BOM Update Tool") - doc.current_bom = args.current_bom - doc.new_bom = args.new_bom - doc.replace_bom() - - frappe.db.auto_commit_on_many_writes = 0 + try: + frappe.db.auto_commit_on_many_writes = 1 + args = frappe._dict(args) + doc = frappe.get_doc("BOM Update Tool") + doc.current_bom = args.current_bom + doc.new_bom = args.new_bom + doc.replace_bom() + except Exception: + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + finally: + frappe.db.auto_commit_on_many_writes = 0 def update_cost(): - frappe.db.auto_commit_on_many_writes = 1 - bom_list = get_boms_in_bottom_up_order() - for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) - - frappe.db.auto_commit_on_many_writes = 0 + try: + frappe.db.auto_commit_on_many_writes = 1 + bom_list = get_boms_in_bottom_up_order() + for bom in bom_list: + frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) + except Exception: + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + finally: + frappe.db.auto_commit_on_many_writes = 0 From 7aa37ec5114d8b5aefb2d1d87bb6a4be2a5afe2a Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Mar 2022 19:45:03 +0530 Subject: [PATCH 756/951] feat: BOM Update Log - Created BOM Update Log that will handle queued job status and failures - Moved validation and BG job to thus new doctype - BOM Update Tool only works as an endpoint (cherry picked from commit 4283a13e5a6a6b9f1e8e1cbcc639646a4e957b36) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- erpnext/hooks.py | 2 +- .../doctype/bom_update_log/__init__.py | 0 .../doctype/bom_update_log/bom_update_log.js | 8 ++ .../bom_update_log/bom_update_log.json | 101 +++++++++++++++ .../doctype/bom_update_log/bom_update_log.py | 117 ++++++++++++++++++ .../bom_update_log/test_bom_update_log.py | 9 ++ .../bom_update_tool/bom_update_tool.py | 34 ++++- 7 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_update_log/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py create mode 100644 erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 906eb10c64f..a21a0313544 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -511,7 +511,7 @@ scheduler_events = { ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", - "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", + "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", "erpnext.hr.utils.generate_leave_encashment", "erpnext.hr.utils.allocate_earned_leaves", diff --git a/erpnext/manufacturing/doctype/bom_update_log/__init__.py b/erpnext/manufacturing/doctype/bom_update_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js new file mode 100644 index 00000000000..6da808e26d1 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('BOM Update Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json new file mode 100644 index 00000000000..222168be8cf --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "autoname": "BOM-UPDT-LOG-.#####", + "creation": "2022-03-16 14:23:35.210155", + "description": "BOM Update Tool Log with job status maintained", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "current_bom", + "new_bom", + "column_break_3", + "update_type", + "status", + "amended_from" + ], + "fields": [ + { + "fieldname": "current_bom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Current BOM", + "options": "BOM", + "reqd": 1 + }, + { + "fieldname": "new_bom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "New BOM", + "options": "BOM", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "update_type", + "fieldtype": "Select", + "label": "Update Type", + "options": "Replace BOM\nUpdate Cost" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Queued\nIn Progress\nCompleted\nFailed" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "BOM Update Log", + "print_hide": 1, + "read_only": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-03-16 18:25:49.833836", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Update Log", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py new file mode 100644 index 00000000000..10db0de9a11 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -0,0 +1,117 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import cstr + +from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order + +from rq.timeouts import JobTimeoutException + + +class BOMMissingError(frappe.ValidationError): pass + +class BOMUpdateLog(Document): + def validate(self): + self.validate_boms_are_specified() + self.validate_same_bom() + self.validate_bom_items() + self.status = "Queued" + + def validate_boms_are_specified(self): + if self.update_type == "Replace BOM" and not (self.current_bom and self.new_bom): + frappe.throw( + msg=_("Please mention the Current and New BOM for replacement."), + title=_("Mandatory"), exc=BOMMissingError + ) + + def validate_same_bom(self): + if cstr(self.current_bom) == cstr(self.new_bom): + frappe.throw(_("Current BOM and New BOM can not be same")) + + def validate_bom_items(self): + current_bom_item = frappe.db.get_value("BOM", self.current_bom, "item") + new_bom_item = frappe.db.get_value("BOM", self.new_bom, "item") + + if current_bom_item != new_bom_item: + frappe.throw(_("The selected BOMs are not for the same item")) + + def on_submit(self): + if frappe.flags.in_test: + return + + if self.update_type == "Replace BOM": + boms = { + "current_bom": self.current_bom, + "new_bom": self.new_bom + } + frappe.enqueue( + method="erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", + boms=boms, doc=self, timeout=40000 + ) + else: + frappe.enqueue( + method="erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost_queue", + doc=self, timeout=40000 + ) + +def replace_bom(boms, doc): + try: + doc.db_set("status", "In Progress") + if not frappe.flags.in_test: + frappe.db.commit() + + frappe.db.auto_commit_on_many_writes = 1 + + args = frappe._dict(boms) + doc = frappe.get_doc("BOM Update Tool") + doc.current_bom = args.current_bom + doc.new_bom = args.new_bom + doc.replace_bom() + + doc.db_set("status", "Completed") + + except (Exception, JobTimeoutException): + frappe.db.rollback() + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + doc.db_set("status", "Failed") + + finally: + frappe.db.auto_commit_on_many_writes = 0 + frappe.db.commit() + +def update_cost_queue(doc): + try: + doc.db_set("status", "In Progress") + if not frappe.flags.in_test: + frappe.db.commit() + + frappe.db.auto_commit_on_many_writes = 1 + + bom_list = get_boms_in_bottom_up_order() + for bom in bom_list: + frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) + + doc.db_set("status", "Completed") + + except (Exception, JobTimeoutException): + frappe.db.rollback() + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + doc.db_set("status", "Failed") + + finally: + frappe.db.auto_commit_on_many_writes = 0 + frappe.db.commit() + +def update_cost(): + bom_list = get_boms_in_bottom_up_order() + for bom in bom_list: + frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py new file mode 100644 index 00000000000..f74bdc356a7 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBOMUpdateLog(FrappeTestCase): + pass diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 11092702ca3..7e072a9d1b2 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -11,13 +11,11 @@ from frappe.model.document import Document from frappe.utils import cstr, flt from six import string_types -from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import update_cost class BOMUpdateTool(Document): def replace_bom(self): - self.validate_bom() - unit_cost = get_new_bom_unit_cost(self.new_bom) self.update_new_bom(unit_cost) @@ -43,6 +41,7 @@ class BOMUpdateTool(Document): except Exception: frappe.log_error(frappe.get_traceback()) +<<<<<<< HEAD def validate_bom(self): if cstr(self.current_bom) == cstr(self.new_bom): frappe.throw(_("Current BOM and New BOM can not be same")) @@ -52,6 +51,8 @@ class BOMUpdateTool(Document): ): frappe.throw(_("The selected BOMs are not for the same item")) +======= +>>>>>>> 4283a13e5a (feat: BOM Update Log) def update_new_bom(self, unit_cost): frappe.db.sql( """update `tabBOM Item` set bom_no=%s, @@ -93,16 +94,21 @@ def enqueue_replace_bom(args): if isinstance(args, string_types): args = json.loads(args) +<<<<<<< HEAD frappe.enqueue( "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", args=args, timeout=40000, ) +======= + create_bom_update_log(boms=args) +>>>>>>> 4283a13e5a (feat: BOM Update Log) frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) @frappe.whitelist() def enqueue_update_cost(): +<<<<<<< HEAD frappe.enqueue( "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000 ) @@ -110,11 +116,18 @@ def enqueue_update_cost(): _("Queued for updating latest price in all Bill of Materials. It may take a few minutes.") ) +======= + create_bom_update_log(update_type="Update Cost") + frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) +>>>>>>> 4283a13e5a (feat: BOM Update Log) -def update_latest_price_in_all_boms(): + +def auto_update_latest_price_in_all_boms(): + "Called via hooks.py." if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() +<<<<<<< HEAD def replace_bom(args): try: @@ -146,3 +159,16 @@ def update_cost(): ) finally: frappe.db.auto_commit_on_many_writes = 0 +======= +def create_bom_update_log(boms=None, update_type="Replace BOM"): + "Creates a BOM Update Log that handles the background job." + current_bom = boms.get("current_bom") if boms else None + new_bom = boms.get("new_bom") if boms else None + log_doc = frappe.get_doc({ + "doctype": "BOM Update Log", + "current_bom": current_bom, + "new_bom": new_bom, + "update_type": update_type + }) + log_doc.submit() +>>>>>>> 4283a13e5a (feat: BOM Update Log) From 59af5562413e1616c2c0a24541c2aabf1e80538f Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 12:32:37 +0530 Subject: [PATCH 757/951] chore: Polish error handling and code sepration - Added Typing - Moved all job business logic to bom update log - Added `run_bom_job` that handles errors and runs either of two methods - UX: Replace button disabled until both inputs are filled - Show log creation message on UI for correctness - APIs return log document as result - Converted raw sql to QB (cherry picked from commit cff91558d4f380cc7566d009ea85ccba36976f69) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- .../bom_update_log/bom_update_log.json | 8 +- .../doctype/bom_update_log/bom_update_log.py | 146 ++++++++++++------ .../bom_update_tool/bom_update_tool.js | 43 +++++- .../bom_update_tool/bom_update_tool.py | 55 ++++++- 4 files changed, 186 insertions(+), 66 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index 222168be8cf..d89427edc0b 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -20,16 +20,14 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Current BOM", - "options": "BOM", - "reqd": 1 + "options": "BOM" }, { "fieldname": "new_bom", "fieldtype": "Link", "in_list_view": 1, "label": "New BOM", - "options": "BOM", - "reqd": 1 + "options": "BOM" }, { "fieldname": "column_break_3", @@ -61,7 +59,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-16 18:25:49.833836", + "modified": "2022-03-17 12:21:16.156437", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 10db0de9a11..b08d6f906c2 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,23 +1,27 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from typing import Dict, List, Optional +import click import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr - -from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order - +from frappe.utils import cstr, flt from rq.timeouts import JobTimeoutException +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost -class BOMMissingError(frappe.ValidationError): pass + +class BOMMissingError(frappe.ValidationError): + pass class BOMUpdateLog(Document): def validate(self): - self.validate_boms_are_specified() - self.validate_same_bom() - self.validate_bom_items() + if self.update_type == "Replace BOM": + self.validate_boms_are_specified() + self.validate_same_bom() + self.validate_bom_items() + self.status = "Queued" def validate_boms_are_specified(self): @@ -48,16 +52,88 @@ class BOMUpdateLog(Document): "new_bom": self.new_bom } frappe.enqueue( - method="erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", - boms=boms, doc=self, timeout=40000 + method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", + doc=self, boms=boms, timeout=40000 ) else: frappe.enqueue( - method="erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost_queue", - doc=self, timeout=40000 + method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", + doc=self, update_type="Update Cost", timeout=40000 ) -def replace_bom(boms, doc): +def replace_bom(boms: Dict) -> None: + """Replace current BOM with new BOM in parent BOMs.""" + current_bom = boms.get("current_bom") + new_bom = boms.get("new_bom") + + unit_cost = get_new_bom_unit_cost(new_bom) + update_new_bom(unit_cost, current_bom, new_bom) + + frappe.cache().delete_key('bom_children') + parent_boms = get_parent_boms(new_bom) + + with click.progressbar(parent_boms) as parent_boms: + pass + for bom in parent_boms: + bom_obj = frappe.get_cached_doc('BOM', bom) + # this is only used for versioning and we do not want + # to make separate db calls by using load_doc_before_save + # which proves to be expensive while doing bulk replace + bom_obj._doc_before_save = bom_obj + bom_obj.update_new_bom(unit_cost, current_bom, new_bom) + bom_obj.update_exploded_items() + bom_obj.calculate_cost() + bom_obj.update_parent_cost() + bom_obj.db_update() + if bom_obj.meta.get('track_changes') and not bom_obj.flags.ignore_version: + bom_obj.save_version() + +def update_new_bom(unit_cost: float, current_bom: str, new_bom: str) -> None: + bom_item = frappe.qb.DocType("BOM Item") + frappe.qb.update(bom_item).set( + bom_item.bom_no, new_bom + ).set( + bom_item.rate, unit_cost + ).set( + bom_item.amount, (bom_item.stock_qty * unit_cost) + ).where( + (bom_item.bom_no == current_bom) + & (bom_item.docstatus < 2) + & (bom_item.parenttype == "BOM") + ).run() + +def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List: + bom_list = bom_list or [] + bom_item = frappe.qb.DocType("BOM Item") + + parents = frappe.qb.from_(bom_item).select( + bom_item.parent + ).where( + (bom_item.bom_no == new_bom) + & (bom_item.docstatus <2) + & (bom_item.parenttype == "BOM") + ).run(as_dict=True) + + for d in parents: + if new_bom == d.parent: + frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) + + bom_list.append(d.parent) + get_parent_boms(d.parent, bom_list) + + return list(set(bom_list)) + +def get_new_bom_unit_cost(new_bom: str) -> float: + bom = frappe.qb.DocType("BOM") + new_bom_unitcost = frappe.qb.from_(bom).select( + bom.total_cost / bom.quantity + ).where( + bom.name == new_bom + ).run() + + return flt(new_bom_unitcost[0][0]) + +def run_bom_job(doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: Optional[str] = "Replace BOM") -> None: try: doc.db_set("status", "In Progress") if not frappe.flags.in_test: @@ -65,18 +141,19 @@ def replace_bom(boms, doc): frappe.db.auto_commit_on_many_writes = 1 - args = frappe._dict(boms) - doc = frappe.get_doc("BOM Update Tool") - doc.current_bom = args.current_bom - doc.new_bom = args.new_bom - doc.replace_bom() + boms = frappe._dict(boms or {}) + + if update_type == "Replace BOM": + replace_bom(boms) + else: + update_cost() doc.db_set("status", "Completed") except (Exception, JobTimeoutException): frappe.db.rollback() frappe.log_error( - msg=frappe.get_traceback(), + message=frappe.get_traceback(), title=_("BOM Update Tool Error") ) doc.db_set("status", "Failed") @@ -84,34 +161,3 @@ def replace_bom(boms, doc): finally: frappe.db.auto_commit_on_many_writes = 0 frappe.db.commit() - -def update_cost_queue(doc): - try: - doc.db_set("status", "In Progress") - if not frappe.flags.in_test: - frappe.db.commit() - - frappe.db.auto_commit_on_many_writes = 1 - - bom_list = get_boms_in_bottom_up_order() - for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) - - doc.db_set("status", "Completed") - - except (Exception, JobTimeoutException): - frappe.db.rollback() - frappe.log_error( - msg=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) - doc.db_set("status", "Failed") - - finally: - frappe.db.auto_commit_on_many_writes = 0 - frappe.db.commit() - -def update_cost(): - bom_list = get_boms_in_bottom_up_order() - for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index bf5fe2e18de..ec6a76d61c4 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -20,30 +20,63 @@ frappe.ui.form.on('BOM Update Tool', { refresh: function(frm) { frm.disable_save(); + frm.events.disable_button(frm, "replace"); }, - replace: function(frm) { + disable_button: (frm, field, disable=true) => { + frm.get_field(field).input.disabled = disable; + }, + + current_bom: (frm) => { + if (frm.doc.current_bom && frm.doc.new_bom){ + frm.events.disable_button(frm, "replace", false); + } + }, + + new_bom: (frm) => { + if (frm.doc.current_bom && frm.doc.new_bom){ + frm.events.disable_button(frm, "replace", false); + } + }, + + replace: (frm) => { if (frm.doc.current_bom && frm.doc.new_bom) { frappe.call({ method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_replace_bom", freeze: true, args: { - args: { + boms: { "current_bom": frm.doc.current_bom, "new_bom": frm.doc.new_bom } + }, + callback: result => { + if (result && result.message && !result.exc) { + frm.events.confirm_job_start(frm, result.message); + } } }); } }, - update_latest_price_in_all_boms: function() { + update_latest_price_in_all_boms: (frm) => { frappe.call({ method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_update_cost", freeze: true, - callback: function() { - frappe.msgprint(__("Latest price updated in all BOMs")); + callback: result => { + if (result && result.message && !result.exc) { + frm.events.confirm_job_start(frm, result.message); + } } }); + }, + + confirm_job_start: (frm, log_data) => { + let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true) + frappe.msgprint({ + "message": __(`BOM Updation is queued and may take a few minutes. Check ${log_link} for progress.`), + "title": __("BOM Update Initiated"), + "indicator": "blue" + }); } }); diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 7e072a9d1b2..448b73a531d 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -1,20 +1,23 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import json +from typing import Dict, List, Optional, TYPE_CHECKING, Union + +if TYPE_CHECKING: + from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog -import click import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, flt from six import string_types -from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import update_cost +from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order class BOMUpdateTool(Document): +<<<<<<< HEAD def replace_bom(self): unit_cost = get_new_bom_unit_cost(self.new_bom) self.update_new_bom(unit_cost) @@ -87,9 +90,13 @@ def get_new_bom_unit_cost(bom): ) return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0 +======= + pass +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) @frappe.whitelist() +<<<<<<< HEAD def enqueue_replace_bom(args): if isinstance(args, string_types): args = json.loads(args) @@ -104,9 +111,19 @@ def enqueue_replace_bom(args): create_bom_update_log(boms=args) >>>>>>> 4283a13e5a (feat: BOM Update Log) frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) +======= +def enqueue_replace_bom(boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None) -> "BOMUpdateLog": + """Returns a BOM Update Log (that queues a job) for BOM Replacement.""" + boms = boms or args + if isinstance(boms, str): + boms = json.loads(boms) +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) + update_log = create_bom_update_log(boms=boms) + return update_log @frappe.whitelist() +<<<<<<< HEAD def enqueue_update_cost(): <<<<<<< HEAD frappe.enqueue( @@ -120,14 +137,21 @@ def enqueue_update_cost(): create_bom_update_log(update_type="Update Cost") frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) >>>>>>> 4283a13e5a (feat: BOM Update Log) +======= +def enqueue_update_cost() -> "BOMUpdateLog": + """Returns a BOM Update Log (that queues a job) for BOM Cost Updation.""" + update_log = create_bom_update_log(update_type="Update Cost") + return update_log +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) -def auto_update_latest_price_in_all_boms(): - "Called via hooks.py." +def auto_update_latest_price_in_all_boms() -> None: + """Called via hooks.py.""" if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() <<<<<<< HEAD +<<<<<<< HEAD def replace_bom(args): try: @@ -172,3 +196,22 @@ def create_bom_update_log(boms=None, update_type="Replace BOM"): }) log_doc.submit() >>>>>>> 4283a13e5a (feat: BOM Update Log) +======= +def update_cost() -> None: + """Updates Cost for all BOMs from bottom to top.""" + bom_list = get_boms_in_bottom_up_order() + for bom in bom_list: + frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) + +def create_bom_update_log(boms: Optional[Dict] = None, update_type: str = "Replace BOM") -> "BOMUpdateLog": + """Creates a BOM Update Log that handles the background job.""" + boms = boms or {} + current_bom = boms.get("current_bom") + new_bom = boms.get("new_bom") + return frappe.get_doc({ + "doctype": "BOM Update Log", + "current_bom": current_bom, + "new_bom": new_bom, + "update_type": update_type, + }).submit() +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) From 444af4588f8c194d800044017fd9e7bb8dfe71b2 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 12:58:09 +0530 Subject: [PATCH 758/951] feat: List View indicators for Log and Error Log link in log (cherry picked from commit 8aff75f8e8f6cf885f0e59ead89b8596d6f56c0a) --- .../doctype/bom_update_log/bom_update_log.json | 9 ++++++++- .../doctype/bom_update_log/bom_update_log.py | 4 +++- .../doctype/bom_update_log/bom_update_log_list.js | 13 +++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index d89427edc0b..38c685a64f1 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -12,6 +12,7 @@ "column_break_3", "update_type", "status", + "error_log", "amended_from" ], "fields": [ @@ -53,13 +54,19 @@ "options": "BOM Update Log", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "error_log", + "fieldtype": "Link", + "label": "Error Log", + "options": "Error Log" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-17 12:21:16.156437", + "modified": "2022-03-17 12:51:28.067900", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index b08d6f906c2..a69b15c5274 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -152,11 +152,13 @@ def run_bom_job(doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: O except (Exception, JobTimeoutException): frappe.db.rollback() - frappe.log_error( + error_log = frappe.log_error( message=frappe.get_traceback(), title=_("BOM Update Tool Error") ) + doc.db_set("status", "Failed") + doc.db_set("error_log", error_log.name) finally: frappe.db.auto_commit_on_many_writes = 0 diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js new file mode 100644 index 00000000000..8b3dc520cfa --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings['BOM Update Log'] = { + add_fields: ["status"], + get_indicator: function(doc) { + let status_map = { + "Queued": "orange", + "In Progress": "blue", + "Completed": "green", + "Failed": "red" + } + + return [__(doc.status), status_map[doc.status], "status,=," + doc.status]; + } +}; \ No newline at end of file From 8b5e759965f19e0af5b6017fe5b7e9c400fe61a3 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 15:03:20 +0530 Subject: [PATCH 759/951] fix: Sider and Linter (cherry picked from commit 3e3af95712b5241a243a5b6169be2fc888bb4c39) --- .../doctype/bom_update_log/bom_update_log.py | 56 +++++++++---------- .../bom_update_log/bom_update_log_list.js | 2 +- .../bom_update_tool/bom_update_tool.js | 6 +- .../bom_update_tool/bom_update_tool.py | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index a69b15c5274..7f60d8fc7df 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,8 +1,8 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from typing import Dict, List, Optional -import click +import click import frappe from frappe import _ from frappe.model.document import Document @@ -89,39 +89,39 @@ def replace_bom(boms: Dict) -> None: bom_obj.save_version() def update_new_bom(unit_cost: float, current_bom: str, new_bom: str) -> None: - bom_item = frappe.qb.DocType("BOM Item") - frappe.qb.update(bom_item).set( - bom_item.bom_no, new_bom - ).set( - bom_item.rate, unit_cost - ).set( - bom_item.amount, (bom_item.stock_qty * unit_cost) - ).where( - (bom_item.bom_no == current_bom) - & (bom_item.docstatus < 2) - & (bom_item.parenttype == "BOM") - ).run() + bom_item = frappe.qb.DocType("BOM Item") + frappe.qb.update(bom_item).set( + bom_item.bom_no, new_bom + ).set( + bom_item.rate, unit_cost + ).set( + bom_item.amount, (bom_item.stock_qty * unit_cost) + ).where( + (bom_item.bom_no == current_bom) + & (bom_item.docstatus < 2) + & (bom_item.parenttype == "BOM") + ).run() def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List: - bom_list = bom_list or [] - bom_item = frappe.qb.DocType("BOM Item") + bom_list = bom_list or [] + bom_item = frappe.qb.DocType("BOM Item") - parents = frappe.qb.from_(bom_item).select( - bom_item.parent - ).where( - (bom_item.bom_no == new_bom) - & (bom_item.docstatus <2) - & (bom_item.parenttype == "BOM") - ).run(as_dict=True) + parents = frappe.qb.from_(bom_item).select( + bom_item.parent + ).where( + (bom_item.bom_no == new_bom) + & (bom_item.docstatus <2) + & (bom_item.parenttype == "BOM") + ).run(as_dict=True) - for d in parents: - if new_bom == d.parent: - frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) + for d in parents: + if new_bom == d.parent: + frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) - bom_list.append(d.parent) - get_parent_boms(d.parent, bom_list) + bom_list.append(d.parent) + get_parent_boms(d.parent, bom_list) - return list(set(bom_list)) + return list(set(bom_list)) def get_new_bom_unit_cost(new_bom: str) -> float: bom = frappe.qb.DocType("BOM") diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js index 8b3dc520cfa..e39b5637c78 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js @@ -6,7 +6,7 @@ frappe.listview_settings['BOM Update Log'] = { "In Progress": "blue", "Completed": "green", "Failed": "red" - } + }; return [__(doc.status), status_map[doc.status], "status,=," + doc.status]; } diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index ec6a76d61c4..0c9816712c2 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -28,13 +28,13 @@ frappe.ui.form.on('BOM Update Tool', { }, current_bom: (frm) => { - if (frm.doc.current_bom && frm.doc.new_bom){ + if (frm.doc.current_bom && frm.doc.new_bom) { frm.events.disable_button(frm, "replace", false); } }, new_bom: (frm) => { - if (frm.doc.current_bom && frm.doc.new_bom){ + if (frm.doc.current_bom && frm.doc.new_bom) { frm.events.disable_button(frm, "replace", false); } }, @@ -72,7 +72,7 @@ frappe.ui.form.on('BOM Update Tool', { }, confirm_job_start: (frm, log_data) => { - let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true) + let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true); frappe.msgprint({ "message": __(`BOM Updation is queued and may take a few minutes. Check ${log_link} for progress.`), "title": __("BOM Update Initiated"), diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 448b73a531d..55674890b15 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -2,7 +2,7 @@ # For license information, please see license.txt import json -from typing import Dict, List, Optional, TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Dict, Optional, Union if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog From 5dca5563ff4db075071a9dd1d34ea7c6ae80cdd8 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 17:43:12 +0530 Subject: [PATCH 760/951] fix: Test, Sider and Added button to access log from Tool (cherry picked from commit f3715ab38260f21f5be8c6f9bdfcf8a02c051556) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- .../doctype/bom_update_tool/bom_update_tool.js | 4 ++++ .../doctype/bom_update_tool/bom_update_tool.py | 4 +++- .../bom_update_tool/test_bom_update_tool.py | 16 +++++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index 0c9816712c2..a793ed95354 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -21,6 +21,10 @@ frappe.ui.form.on('BOM Update Tool', { refresh: function(frm) { frm.disable_save(); frm.events.disable_button(frm, "replace"); + + frm.add_custom_button(__("View BOM Update Log"), () => { + frappe.set_route("List", "BOM Update Log"); + }); }, disable_button: (frm, field, disable=true) => { diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 55674890b15..56512526065 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -8,10 +8,12 @@ if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog import frappe -from frappe import _ from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import cstr, flt from six import string_types +======= +>>>>>>> f3715ab382 (fix: Test, Sider and Added button to access log from Tool) from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 57785e58dd0..8da5393f913 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -5,6 +5,7 @@ import frappe from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item @@ -19,18 +20,19 @@ class TestBOMUpdateTool(FrappeTestCase): bom_doc.items[1].item_code = "_Test Item" bom_doc.insert() - update_tool = frappe.get_doc("BOM Update Tool") - update_tool.current_bom = current_bom - update_tool.new_bom = bom_doc.name - update_tool.replace_bom() + boms = frappe._dict( + current_bom=current_bom, + new_bom=bom_doc.name + ) + replace_bom(boms) self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom)) self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name)) # reverse, as it affects other testcases - update_tool.current_bom = bom_doc.name - update_tool.new_bom = current_bom - update_tool.replace_bom() + boms.current_bom = bom_doc.name + boms.new_bom = current_bom + replace_bom(boms) def test_bom_cost(self): for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: From 9b069ed04b59eb51368d076307dfa1eda0daef88 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 13:01:01 +0530 Subject: [PATCH 761/951] test: API hit via BOM Update Tool - test creation of log and it's impact (cherry picked from commit 1d1e925bcf6066cac03abfb60510e76d0f97f9be) --- .../bom_update_log/test_bom_update_log.py | 83 ++++++++++++++++++- .../bom_update_tool/test_bom_update_tool.py | 2 + 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index f74bdc356a7..52ca9cde1bd 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -1,9 +1,88 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import ( + BOMMissingError, + run_bom_job, +) +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom + +test_records = frappe.get_test_records("BOM") + class TestBOMUpdateLog(FrappeTestCase): - pass + "Test BOM Update Tool Operations via BOM Update Log." + + def setUp(self): + bom_doc = frappe.copy_doc(test_records[0]) + bom_doc.items[1].item_code = "_Test Item" + bom_doc.insert() + + self.boms = frappe._dict( + current_bom="BOM-_Test Item Home Desktop Manufactured-001", + new_bom=bom_doc.name, + ) + + self.new_bom_doc = bom_doc + + def tearDown(self): + frappe.db.rollback() + + if self._testMethodName == "test_bom_update_log_completion": + # clear logs and delete BOM created via setUp + frappe.db.delete("BOM Update Log") + self.new_bom_doc.cancel() + self.new_bom_doc.delete() + frappe.db.commit() # explicitly commit and restore to original state + + def test_bom_update_log_validate(self): + "Test if BOM presence is validated." + + with self.assertRaises(BOMMissingError): + enqueue_replace_bom(boms={}) + + def test_bom_update_log_queueing(self): + "Test if BOM Update Log is created and queued." + + log = enqueue_replace_bom( + boms=self.boms, + ) + + self.assertEqual(log.docstatus, 1) + self.assertEqual(log.status, "Queued") + + def test_bom_update_log_completion(self): + "Test if BOM Update Log handles job completion correctly." + + log = enqueue_replace_bom( + boms=self.boms, + ) + + # Explicitly commits log, new bom (setUp) and replacement impact. + # Is run via background jobs IRL + run_bom_job( + doc=log, + boms=self.boms, + update_type="Replace BOM", + ) + log.reload() + + self.assertEqual(log.status, "Completed") + + # teardown (undo replace impact) due to commit + boms = frappe._dict( + current_bom=self.boms.new_bom, + new_bom=self.boms.current_bom, + ) + log2 = enqueue_replace_bom( + boms=self.boms, + ) + run_bom_job( # Explicitly commits + doc=log2, + boms=boms, + update_type="Replace BOM", + ) + self.assertEqual(log2.status, "Completed") diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 8da5393f913..36bcd9dcd09 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -13,6 +13,8 @@ test_records = frappe.get_test_records("BOM") class TestBOMUpdateTool(FrappeTestCase): + "Test major functions run via BOM Update Tool." + def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" From 1f9ecb33979d3f9a6fd8d2cfce7d067ea403ef75 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 13:08:58 +0530 Subject: [PATCH 762/951] fix: Auto format `bom_update_log.py` (cherry picked from commit 79495679e209a31a1865b7d4bd1bfc42c4813403) --- .../doctype/bom_update_log/bom_update_log.py | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 7f60d8fc7df..172f38d250f 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -15,6 +15,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update class BOMMissingError(frappe.ValidationError): pass + class BOMUpdateLog(Document): def validate(self): if self.update_type == "Replace BOM": @@ -28,7 +29,8 @@ class BOMUpdateLog(Document): if self.update_type == "Replace BOM" and not (self.current_bom and self.new_bom): frappe.throw( msg=_("Please mention the Current and New BOM for replacement."), - title=_("Mandatory"), exc=BOMMissingError + title=_("Mandatory"), + exc=BOMMissingError, ) def validate_same_bom(self): @@ -47,20 +49,22 @@ class BOMUpdateLog(Document): return if self.update_type == "Replace BOM": - boms = { - "current_bom": self.current_bom, - "new_bom": self.new_bom - } + boms = {"current_bom": self.current_bom, "new_bom": self.new_bom} frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", - doc=self, boms=boms, timeout=40000 + doc=self, + boms=boms, + timeout=40000, ) else: frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", - doc=self, update_type="Update Cost", timeout=40000 + doc=self, + update_type="Update Cost", + timeout=40000, ) + def replace_bom(boms: Dict) -> None: """Replace current BOM with new BOM in parent BOMs.""" current_bom = boms.get("current_bom") @@ -69,13 +73,13 @@ def replace_bom(boms: Dict) -> None: unit_cost = get_new_bom_unit_cost(new_bom) update_new_bom(unit_cost, current_bom, new_bom) - frappe.cache().delete_key('bom_children') + frappe.cache().delete_key("bom_children") parent_boms = get_parent_boms(new_bom) with click.progressbar(parent_boms) as parent_boms: pass for bom in parent_boms: - bom_obj = frappe.get_cached_doc('BOM', bom) + bom_obj = frappe.get_cached_doc("BOM", bom) # this is only used for versioning and we do not want # to make separate db calls by using load_doc_before_save # which proves to be expensive while doing bulk replace @@ -85,34 +89,29 @@ def replace_bom(boms: Dict) -> None: bom_obj.calculate_cost() bom_obj.update_parent_cost() bom_obj.db_update() - if bom_obj.meta.get('track_changes') and not bom_obj.flags.ignore_version: + if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: bom_obj.save_version() + def update_new_bom(unit_cost: float, current_bom: str, new_bom: str) -> None: bom_item = frappe.qb.DocType("BOM Item") - frappe.qb.update(bom_item).set( - bom_item.bom_no, new_bom - ).set( - bom_item.rate, unit_cost - ).set( + frappe.qb.update(bom_item).set(bom_item.bom_no, new_bom).set(bom_item.rate, unit_cost).set( bom_item.amount, (bom_item.stock_qty * unit_cost) ).where( - (bom_item.bom_no == current_bom) - & (bom_item.docstatus < 2) - & (bom_item.parenttype == "BOM") + (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") ).run() + def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List: bom_list = bom_list or [] bom_item = frappe.qb.DocType("BOM Item") - parents = frappe.qb.from_(bom_item).select( - bom_item.parent - ).where( - (bom_item.bom_no == new_bom) - & (bom_item.docstatus <2) - & (bom_item.parenttype == "BOM") - ).run(as_dict=True) + parents = ( + frappe.qb.from_(bom_item) + .select(bom_item.parent) + .where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) + .run(as_dict=True) + ) for d in parents: if new_bom == d.parent: @@ -123,17 +122,19 @@ def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List: return list(set(bom_list)) + def get_new_bom_unit_cost(new_bom: str) -> float: bom = frappe.qb.DocType("BOM") - new_bom_unitcost = frappe.qb.from_(bom).select( - bom.total_cost / bom.quantity - ).where( - bom.name == new_bom - ).run() + new_bom_unitcost = ( + frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run() + ) return flt(new_bom_unitcost[0][0]) -def run_bom_job(doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: Optional[str] = "Replace BOM") -> None: + +def run_bom_job( + doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: Optional[str] = "Replace BOM" +) -> None: try: doc.db_set("status", "In Progress") if not frappe.flags.in_test: @@ -152,10 +153,7 @@ def run_bom_job(doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: O except (Exception, JobTimeoutException): frappe.db.rollback() - error_log = frappe.log_error( - message=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) + error_log = frappe.log_error(message=frappe.get_traceback(), title=_("BOM Update Tool Error")) doc.db_set("status", "Failed") doc.db_set("error_log", error_log.name) From c0c39f8c795ca8ca155ab52acfee9cb677de2958 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 13:22:29 +0530 Subject: [PATCH 763/951] fix: Semgrep - Explain explicit commits and skip semgrep - Format client side translated string correctly (cherry picked from commit ebf00946c91bf03105533d46c85e9b405cc7d62a) --- .../manufacturing/doctype/bom_update_log/bom_update_log.py | 2 +- .../doctype/bom_update_log/test_bom_update_log.py | 4 +++- .../manufacturing/doctype/bom_update_tool/bom_update_tool.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 172f38d250f..ce2774347b2 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -160,4 +160,4 @@ def run_bom_job( finally: frappe.db.auto_commit_on_many_writes = 0 - frappe.db.commit() + frappe.db.commit() # nosemgrep diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index 52ca9cde1bd..d1da18d0ab8 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -36,7 +36,9 @@ class TestBOMUpdateLog(FrappeTestCase): frappe.db.delete("BOM Update Log") self.new_bom_doc.cancel() self.new_bom_doc.delete() - frappe.db.commit() # explicitly commit and restore to original state + + # explicitly commit and restore to original state + frappe.db.commit() # nosemgrep def test_bom_update_log_validate(self): "Test if BOM presence is validated." diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index a793ed95354..7ba6517a4fb 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -78,7 +78,7 @@ frappe.ui.form.on('BOM Update Tool', { confirm_job_start: (frm, log_data) => { let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true); frappe.msgprint({ - "message": __(`BOM Updation is queued and may take a few minutes. Check ${log_link} for progress.`), + "message": __("BOM Updation is queued and may take a few minutes. Check {0} for progress.", [log_link]), "title": __("BOM Update Initiated"), "indicator": "blue" }); From 0d3c8e4d7458d6763115af7dbd262a0864ad991d Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 18:03:52 +0530 Subject: [PATCH 764/951] fix: Type Annotations, Redundancy, etc. - Renamed public function`update_new_bom` to `update_new_bom_in_bom_items` - Replaced `get_cached_doc` with `get_doc` - Removed click progress bar (drive through update log) - Removed `bom_obj.update_new_bom()`, was redundant. Did same job as `update_new_bom_in_bom_items` - Removed `update_new_bom()` in `bom.py`, unused. - Prettier query formatting - `update_type` annotated as non optional Literal - Removed redundant use of JobTimeoutException - Corrected type annotations in `create_bom_update_log()` (cherry picked from commit 620575a9012a9759c6285558ac25c6709c4e92cc) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- erpnext/manufacturing/doctype/bom/bom.py | 9 ------ .../doctype/bom_update_log/bom_update_log.py | 31 ++++++++++--------- .../bom_update_tool/bom_update_tool.py | 11 ++++++- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8fd6050b4f9..f8fcd073951 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -687,15 +687,6 @@ class BOM(WebsiteGenerator): self.scrap_material_cost = total_sm_cost self.base_scrap_material_cost = base_total_sm_cost - def update_new_bom(self, old_bom, new_bom, rate): - for d in self.get("items"): - if d.bom_no != old_bom: - continue - - d.bom_no = new_bom - d.rate = rate - d.amount = (d.stock_qty or d.qty) * rate - def update_exploded_items(self, save=True): """Update Flat BOM, following will be correct data""" self.get_exploded_items() diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index ce2774347b2..139dcbcdd90 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,13 +1,11 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from typing import Dict, List, Optional +from typing import Dict, List, Literal, Optional -import click import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, flt -from rq.timeouts import JobTimeoutException from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost @@ -71,20 +69,17 @@ def replace_bom(boms: Dict) -> None: new_bom = boms.get("new_bom") unit_cost = get_new_bom_unit_cost(new_bom) - update_new_bom(unit_cost, current_bom, new_bom) + update_new_bom_in_bom_items(unit_cost, current_bom, new_bom) frappe.cache().delete_key("bom_children") parent_boms = get_parent_boms(new_bom) - with click.progressbar(parent_boms) as parent_boms: - pass for bom in parent_boms: - bom_obj = frappe.get_cached_doc("BOM", bom) + bom_obj = frappe.get_doc("BOM", bom) # this is only used for versioning and we do not want # to make separate db calls by using load_doc_before_save # which proves to be expensive while doing bulk replace bom_obj._doc_before_save = bom_obj - bom_obj.update_new_bom(unit_cost, current_bom, new_bom) bom_obj.update_exploded_items() bom_obj.calculate_cost() bom_obj.update_parent_cost() @@ -93,12 +88,16 @@ def replace_bom(boms: Dict) -> None: bom_obj.save_version() -def update_new_bom(unit_cost: float, current_bom: str, new_bom: str) -> None: +def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: bom_item = frappe.qb.DocType("BOM Item") - frappe.qb.update(bom_item).set(bom_item.bom_no, new_bom).set(bom_item.rate, unit_cost).set( - bom_item.amount, (bom_item.stock_qty * unit_cost) - ).where( - (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") + ( + frappe.qb.update(bom_item) + .set(bom_item.bom_no, new_bom) + .set(bom_item.rate, unit_cost) + .set(bom_item.amount, (bom_item.stock_qty * unit_cost)) + .where( + (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") + ) ).run() @@ -133,7 +132,9 @@ def get_new_bom_unit_cost(new_bom: str) -> float: def run_bom_job( - doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: Optional[str] = "Replace BOM" + doc: "BOMUpdateLog", + boms: Optional[Dict[str, str]] = None, + update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", ) -> None: try: doc.db_set("status", "In Progress") @@ -151,7 +152,7 @@ def run_bom_job( doc.db_set("status", "Completed") - except (Exception, JobTimeoutException): + except Exception: frappe.db.rollback() error_log = frappe.log_error(message=frappe.get_traceback(), title=_("BOM Update Tool Error")) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 56512526065..a7573902d78 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -2,7 +2,7 @@ # For license information, please see license.txt import json -from typing import TYPE_CHECKING, Dict, Optional, Union +from typing import TYPE_CHECKING, Dict, Literal, Optional, Union if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -205,8 +205,17 @@ def update_cost() -> None: for bom in bom_list: frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) +<<<<<<< HEAD def create_bom_update_log(boms: Optional[Dict] = None, update_type: str = "Replace BOM") -> "BOMUpdateLog": +======= + +def create_bom_update_log( + boms: Optional[Dict[str, str]] = None, + update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", +) -> "BOMUpdateLog": +>>>>>>> 620575a901 (fix: Type Annotations, Redundancy, etc.) """Creates a BOM Update Log that handles the background job.""" + boms = boms or {} current_bom = boms.get("current_bom") new_bom = boms.get("new_bom") From 770f8da792b0baa32be0b0909e300d513dd0ed49 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 18:20:54 +0530 Subject: [PATCH 765/951] test: Added test for 2 more validations - Covers full validate function (cherry picked from commit a945484af4f69c8b698a2283f4078b99c38df039) --- .../doctype/bom_update_log/test_bom_update_log.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index d1da18d0ab8..47efea961b4 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -46,6 +46,12 @@ class TestBOMUpdateLog(FrappeTestCase): with self.assertRaises(BOMMissingError): enqueue_replace_bom(boms={}) + with self.assertRaises(frappe.ValidationError): + enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom=self.boms.new_bom)) + + with self.assertRaises(frappe.ValidationError): + enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM")) + def test_bom_update_log_queueing(self): "Test if BOM Update Log is created and queued." From a9ec72d83320bdd8d1be2d5253523d2ff6873b6d Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 31 Mar 2022 12:55:48 +0530 Subject: [PATCH 766/951] chore: Added BOM std filters and update type in List View (cherry picked from commit 2fece523f6c0cda8025334e4680794b963fb6914) --- .../manufacturing/doctype/bom_update_log/bom_update_log.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index 38c685a64f1..98c1acb71ce 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -20,6 +20,7 @@ "fieldname": "current_bom", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "Current BOM", "options": "BOM" }, @@ -27,6 +28,7 @@ "fieldname": "new_bom", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "New BOM", "options": "BOM" }, @@ -37,6 +39,7 @@ { "fieldname": "update_type", "fieldtype": "Select", + "in_list_view": 1, "label": "Update Type", "options": "Replace BOM\nUpdate Cost" }, @@ -66,7 +69,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-17 12:51:28.067900", + "modified": "2022-03-31 12:51:44.885102", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", From 8d315a6573e21456f280268a1113b1cc9042a75a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 16:17:51 +0530 Subject: [PATCH 767/951] fix: 'int' object has no attribute 'is_draft' --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 885e3882287..96975e9d116 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -214,7 +214,7 @@ class POSInvoice(SalesInvoice): if self.is_return: return - if self.docstatus.is_draft() and not frappe.db.get_value( + if self.docstatus == 0 and not frappe.db.get_value( "POS Profile", self.pos_profile, "validate_stock_on_save" ): return From 8416dc713c5197f71ea5deae2794bee1e10dfe0b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 31 Mar 2022 14:36:26 +0530 Subject: [PATCH 768/951] fix: unexpected keyword argument 'pluck' --- erpnext/controllers/accounts_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 61db921f9cc..0dbff48f02a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1269,8 +1269,10 @@ class AccountsController(TransactionBase): item_codes = list(set(item.item_code for item in self.get("items"))) if item_codes: stock_items = frappe.db.get_values( - "Item", {"name": ["in", item_codes], "is_stock_item": 1}, pluck="name", cache=True + "Item", {"name": ["in", item_codes], "is_stock_item": 1}, as_dict=True, cache=True ) + if stock_items: + stock_items = [d.get("name") for d in stock_items] return stock_items From f213dc99998fc2e46a2f602226a5eff3eec87e79 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Mar 2022 19:33:44 +0530 Subject: [PATCH 769/951] fix(India): Tax fetching based on tax category (cherry picked from commit 532961fad2a1372edfea902d58d2feb98eace889) --- erpnext/regional/india/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index f6f2f8d4934..4eeb83779b3 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -269,6 +269,7 @@ def get_regional_address_details(party_details, doctype, company): if tax_template_by_category: party_details["taxes_and_charges"] = tax_template_by_category + party_details["taxes"] = get_taxes_and_charges(master_doctype, tax_template_by_category) return party_details if not party_details.place_of_supply: @@ -293,7 +294,7 @@ def get_regional_address_details(party_details, doctype, company): return party_details party_details["taxes_and_charges"] = default_tax - party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + party_details["taxes"] = get_taxes_and_charges(master_doctype, default_tax) return party_details From c92df4eed341906310be313f7cd02b17f615fb54 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 31 Mar 2022 18:40:09 +0530 Subject: [PATCH 770/951] feat: minor, pick list item reference on delivery note item table (cherry picked from commit 2f63ae2ee9782c879d7b7ef806444f41a16ac4c6) --- .../delivery_note_item/delivery_note_item.json | 12 +++++++++++- erpnext/stock/doctype/pick_list/pick_list.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) 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 f1f5d96e628..e2eb2a4bbb2 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -74,6 +74,7 @@ "against_sales_invoice", "si_detail", "dn_detail", + "pick_list_item", "section_break_40", "batch_no", "serial_no", @@ -762,13 +763,22 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "fieldname": "pick_list_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Pick List Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-02-24 14:42:20.211085", + "modified": "2022-03-31 18:36:24.671913", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 7061ee1eea4..d3476a88f05 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -534,6 +534,7 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): dn_item = map_child_doc(source_doc, delivery_note, table_mapper) if dn_item: + dn_item.pick_list_item = location.name dn_item.warehouse = location.warehouse dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1) dn_item.batch_no = location.batch_no From a74198f974deece936e921d4d6d7b79fe84439ea Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 31 Mar 2022 18:47:09 +0530 Subject: [PATCH 771/951] test: test case to check pick list name has mapped or not (cherry picked from commit 2f51011f913454c1ca4e96f647af4476c69fbfe3) --- erpnext/stock/doctype/pick_list/test_pick_list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 7496b6b1798..ec5011b93d6 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -521,6 +521,8 @@ class TestPickList(FrappeTestCase): for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): self.assertEqual(dn_item.item_code, "_Test Item") self.assertEqual(dn_item.against_sales_order, sales_order_1.name) + self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name) + for dn in frappe.get_all( "Delivery Note", filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"}, From 328b9431b21541b3029f89f9f4e44aeca76ec313 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 31 Mar 2022 12:14:16 +0530 Subject: [PATCH 772/951] fix: Taxes getting overriden from mapped to target doc (cherry picked from commit 4720969ce62c5d2f1a8d3fbb86c6eec391b4a2c4) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 ++ erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 19c0d8aaf3d..4e4e2154375 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -276,6 +276,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference) return; + if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8a6d3cd5935..50c94b419b6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -281,6 +281,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte } var me = this; if(this.frm.updating_party_details) return; + + if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, From 9335578a0b9bac6a8a0a3d9455d1e7b23af12f74 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 1 Apr 2022 11:02:18 +0530 Subject: [PATCH 773/951] fix: linting errors --- .../doctype/pos_invoice/test_pos_invoice.py | 38 +++++++++---------- .../page/point_of_sale/point_of_sale.py | 7 ++-- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 0493b8a90a7..70f128e0e39 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -774,14 +774,12 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - frappe.db.savepoint('before_test_delivered_serial_no_case') + frappe.db.savepoint("before_test_delivered_serial_no_case") try: se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] - dn = create_delivery_note( - item_code="_Test Serialized Item With Series", serial_no=serial_no - ) + dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") self.assertEquals(delivery_document_no, dn.name) @@ -789,17 +787,17 @@ class TestPOSInvoice(unittest.TestCase): init_user_and_profile() pos_inv = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=serial_no, - qty=1, - rate=100, - do_not_submit=True + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=True, ) self.assertRaises(frappe.ValidationError, pos_inv.submit) finally: - frappe.db.rollback(save_point='before_test_delivered_serial_no_case') + frappe.db.rollback(save_point="before_test_delivered_serial_no_case") frappe.set_user("Administrator") def test_returned_serial_no_case(self): @@ -810,7 +808,7 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item - frappe.db.savepoint('before_test_returned_serial_no_case') + frappe.db.savepoint("before_test_returned_serial_no_case") try: se = make_serialized_item() serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] @@ -818,10 +816,10 @@ class TestPOSInvoice(unittest.TestCase): init_user_and_profile() pos_inv = create_pos_invoice( - item_code="_Test Serialized Item With Series", - serial_no=serial_no, - qty=1, - rate=100, + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, ) pos_return = make_sales_return(pos_inv.name) @@ -829,16 +827,16 @@ class TestPOSInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - pos_reserved_serial_nos = get_pos_reserved_serial_nos({ - 'item_code': '_Test Serialized Item With Series', - 'warehouse': '_Test Warehouse - _TC' - }) + pos_reserved_serial_nos = get_pos_reserved_serial_nos( + {"item_code": "_Test Serialized Item With Series", "warehouse": "_Test Warehouse - _TC"} + ) self.assertTrue(serial_no not in pos_reserved_serial_nos) finally: - frappe.db.rollback(save_point='before_test_returned_serial_no_case') + frappe.db.rollback(save_point="before_test_returned_serial_no_case") frappe.set_user("Administrator") + def create_pos_invoice(**args): args = frappe._dict(args) pos_profile = None diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 26a80763c9b..bf629824ad9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -325,15 +325,16 @@ def set_customer_info(fieldname, customer, value=""): frappe.db.set_value("Customer", customer, "mobile_no", value) contact_doc.save() + @frappe.whitelist() def get_pos_profile_data(pos_profile): - pos_profile = frappe.get_doc('POS Profile', pos_profile) + pos_profile = frappe.get_doc("POS Profile", pos_profile) pos_profile = pos_profile.as_dict() _customer_groups_with_children = [] for row in pos_profile.customer_groups: - children = get_child_nodes('Customer Group', row.customer_group) + children = get_child_nodes("Customer Group", row.customer_group) _customer_groups_with_children.extend(children) pos_profile.customer_groups = _customer_groups_with_children - return pos_profile \ No newline at end of file + return pos_profile From fd29722d3635c358fd489b6af7598234c8ce52d6 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 31 Mar 2022 16:29:18 +0530 Subject: [PATCH 774/951] fix: Call Redisearch index creation functions on enabling redisearch in settings --- .../e_commerce_settings.json | 12 ++++++- .../e_commerce_settings.py | 17 ++++++++++ erpnext/e_commerce/redisearch_utils.py | 34 +++++++++---------- erpnext/templates/pages/product_search.py | 10 +++--- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index d5fb9697f89..62505e61db6 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -47,6 +47,7 @@ "item_search_settings_section", "redisearch_warning", "search_index_fields", + "is_redisearch_enabled", "show_categories_in_search_autocomplete", "is_redisearch_loaded", "shop_by_category_section", @@ -303,6 +304,7 @@ }, { "default": "1", + "depends_on": "is_redisearch_enabled", "fieldname": "show_categories_in_search_autocomplete", "fieldtype": "Check", "label": "Show Categories in Search Autocomplete", @@ -365,12 +367,19 @@ "fieldname": "show_price_in_quotation", "fieldtype": "Check", "label": "Show Price in Quotation" + }, + { + "default": "0", + "fieldname": "is_redisearch_enabled", + "fieldtype": "Check", + "label": "Enable Redisearch", + "read_only_depends_on": "eval:!doc.is_redisearch_loaded" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-09-02 14:02:44.785824", + "modified": "2022-03-31 16:01:46.308663", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", @@ -389,5 +398,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file 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 b8f975c5dec..06039f6a240 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 @@ -8,6 +8,7 @@ from frappe.utils import comma_and, flt, unique from erpnext.e_commerce.redisearch_utils import ( create_website_items_index, + define_autocomplete_dictionary, get_indexable_web_fields, is_search_module_loaded, ) @@ -20,6 +21,8 @@ class ShoppingCartSetupError(frappe.ValidationError): class ECommerceSettings(Document): def onload(self): self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series") + + # flag >> if redisearch is installed and loaded self.is_redisearch_loaded = is_search_module_loaded() def validate(self): @@ -33,6 +36,20 @@ class ECommerceSettings(Document): frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings") + self.is_redisearch_enabled_pre_save = frappe.db.get_single_value( + "E Commerce Settings", "is_redisearch_enabled" + ) + + def after_save(self): + self.create_redisearch_indexes() + + def create_redisearch_indexes(self): + # if redisearch is enabled (value changed) create indexes and dictionary + value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save + if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed: + define_autocomplete_dictionary() + create_website_items_index() + def validate_field_filters(self): if not (self.enable_field_filters and self.filter_fields): return diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index 82829bf1eff..78cc05af38b 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -22,6 +22,12 @@ def get_indexable_web_fields(): return [df.fieldname for df in valid_fields] +def is_redisearch_enabled(): + "Return True only if redisearch is loaded and enabled." + is_redisearch_enabled = frappe.db.get_single_value("E Commerce Settings", "is_redisearch_enabled") + return is_search_module_loaded() and is_redisearch_enabled + + def is_search_module_loaded(): try: cache = frappe.cache() @@ -35,11 +41,11 @@ def is_search_module_loaded(): return False -def if_redisearch_loaded(function): - "Decorator to check if Redisearch is loaded." +def if_redisearch_enabled(function): + "Decorator to check if Redisearch is enabled." def wrapper(*args, **kwargs): - if is_search_module_loaded(): + if is_redisearch_enabled(): func = function(*args, **kwargs) return func return @@ -51,7 +57,7 @@ def make_key(key): return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8") -@if_redisearch_loaded +@if_redisearch_enabled def create_website_items_index(): "Creates Index Definition." @@ -91,7 +97,7 @@ def to_search_field(field): return TextField(field) -@if_redisearch_loaded +@if_redisearch_enabled def insert_item_to_index(website_item_doc): # Insert item to index key = get_cache_key(website_item_doc.name) @@ -104,7 +110,7 @@ def insert_item_to_index(website_item_doc): insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) -@if_redisearch_loaded +@if_redisearch_enabled def insert_to_name_ac(web_name, doc_name): ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache()) ac.add_suggestions(Suggestion(web_name, payload=doc_name)) @@ -120,14 +126,14 @@ def create_web_item_map(website_item_doc): return web_item -@if_redisearch_loaded +@if_redisearch_enabled def update_index_for_item(website_item_doc): # Reinsert to Cache insert_item_to_index(website_item_doc) define_autocomplete_dictionary() -@if_redisearch_loaded +@if_redisearch_enabled def delete_item_from_index(website_item_doc): cache = frappe.cache() key = get_cache_key(website_item_doc.name) @@ -141,7 +147,7 @@ def delete_item_from_index(website_item_doc): return True -@if_redisearch_loaded +@if_redisearch_enabled def delete_from_ac_dict(website_item_doc): """Removes this items's name from autocomplete dictionary""" cache = frappe.cache() @@ -149,7 +155,7 @@ def delete_from_ac_dict(website_item_doc): name_ac.delete(website_item_doc.web_item_name) -@if_redisearch_loaded +@if_redisearch_enabled def define_autocomplete_dictionary(): """Creates an autocomplete search dictionary for `name`. Also creats autocomplete dictionary for `categories` if @@ -182,7 +188,7 @@ def define_autocomplete_dictionary(): return True -@if_redisearch_loaded +@if_redisearch_enabled def reindex_all_web_items(): items = frappe.get_all("Website Item", fields=get_fields_indexed(), filters={"published": True}) @@ -208,9 +214,3 @@ def get_fields_indexed(): fields_to_index = fields_to_index + mandatory_fields return fields_to_index - - -# TODO: Remove later -# # Figure out a way to run this at startup -define_autocomplete_dictionary() -create_website_items_index() diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index e5e00ef5a1e..4f2dc6e062b 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -9,7 +9,7 @@ from erpnext.e_commerce.redisearch_utils import ( WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE, - is_search_module_loaded, + is_redisearch_enabled, make_key, ) from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website @@ -74,8 +74,8 @@ def search(query): def product_search(query, limit=10, fuzzy_search=True): search_results = {"from_redisearch": True, "results": []} - if not is_search_module_loaded(): - # Redisearch module not loaded + if not is_redisearch_enabled(): + # Redisearch module not enabled search_results["from_redisearch"] = False search_results["results"] = get_product_data(query, 0, limit) return search_results @@ -121,8 +121,8 @@ def convert_to_dict(redis_search_doc): def get_category_suggestions(query): search_results = {"results": []} - if not is_search_module_loaded(): - # Redisearch module not loaded, query db + if not is_redisearch_enabled(): + # Redisearch module not enabled, query db categories = frappe.db.get_all( "Item Group", filters={"name": ["like", "%{0}%".format(query)], "show_in_website": 1}, From a35cc7004d565d29c6e983b9d9b09720dd8be7b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 1 Apr 2022 14:17:25 +0530 Subject: [PATCH 775/951] fix: convert dates to datetime before comparing in leave days calculation and fix half day edge case (backport #30538) (#30541) --- .../leave_application/leave_application.py | 4 ++-- .../test_leave_application.py | 20 ++++++++++++++++--- .../doctype/salary_slip/test_salary_slip.py | 13 +++++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 9036727f76f..e6fc2e6fc06 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -735,9 +735,9 @@ def get_number_of_leave_days( (Based on the include_holiday setting in Leave Type)""" number_of_days = 0 if cint(half_day) == 1: - if from_date == to_date: + if getdate(from_date) == getdate(to_date): number_of_days = 0.5 - elif half_day_date and half_day_date <= to_date: + elif half_day_date and getdate(from_date) <= getdate(half_day_date) <= getdate(to_date): number_of_days = date_diff(to_date, from_date) + 0.5 else: number_of_days = date_diff(to_date, from_date) + 1 diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index dc8187cf5b7..8924a57708e 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -205,7 +205,12 @@ class TestLeaveApplication(unittest.TestCase): # creates separate leave ledger entries frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) leave_type = frappe.get_doc( - dict(leave_type_name="Test Leave Validation", doctype="Leave Type", allow_negative=True) + dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=True, + include_holiday=True, + ) ).insert() employee = get_employee() @@ -217,8 +222,14 @@ class TestLeaveApplication(unittest.TestCase): # application across allocations # CASE 1: from date has no allocation, to date has an allocation / both dates have allocation + start_date = add_days(year_start, -10) application = make_leave_application( - employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name + employee.name, + start_date, + add_days(year_start, 3), + leave_type.name, + half_day=1, + half_day_date=start_date, ) # 2 separate leave ledger entries @@ -827,6 +838,7 @@ class TestLeaveApplication(unittest.TestCase): leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, expire_carry_forwarded_leaves_after_days=90, + include_holiday=True, ) leave_type.submit() @@ -839,6 +851,8 @@ class TestLeaveApplication(unittest.TestCase): leave_type=leave_type.name, from_date=add_days(nowdate(), -3), to_date=add_days(nowdate(), 7), + half_day=1, + half_day_date=add_days(nowdate(), -3), description="_Test Reason", company="_Test Company", docstatus=1, @@ -854,7 +868,7 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(len(leave_ledger_entry), 2) self.assertEqual(leave_ledger_entry[0].employee, leave_application.employee) self.assertEqual(leave_ledger_entry[0].leave_type, leave_application.leave_type) - self.assertEqual(leave_ledger_entry[0].leaves, -9) + self.assertEqual(leave_ledger_entry[0].leaves, -8.5) self.assertEqual(leave_ledger_entry[1].leaves, -2) def test_leave_application_creation_after_expiry(self): diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 2a68d9b979a..65dae625b6e 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -1294,7 +1294,16 @@ def create_additional_salary(employee, payroll_period, amount): return salary_date -def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True): +def make_leave_application( + employee, + from_date, + to_date, + leave_type, + company=None, + half_day=False, + half_day_date=None, + submit=True, +): leave_application = frappe.get_doc( dict( doctype="Leave Application", @@ -1302,6 +1311,8 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non leave_type=leave_type, from_date=from_date, to_date=to_date, + half_day=half_day, + half_day_date=half_day_date, company=company or erpnext.get_default_company() or "_Test Company", status="Approved", leave_approver="test@example.com", From 63dacaa03d5f64889b76e02e946b7d7fe094c049 Mon Sep 17 00:00:00 2001 From: Alberto826 <46285948+Alberto826@users.noreply.github.com> Date: Fri, 1 Apr 2022 11:18:15 +0200 Subject: [PATCH 776/951] fix: Remove trailing slashes "/" from route (#30531) Trailing slashes "/" in the routes causes redirect to port 8080 in docker implementation with reverse proxy. The "student_admission_row.html" template has an href attribute with a trailing slash. --- .../student_admission/templates/student_admission_row.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student_admission/templates/student_admission_row.html b/erpnext/education/doctype/student_admission/templates/student_admission_row.html index 529d65184a8..dc4587bc940 100644 --- a/erpnext/education/doctype/student_admission/templates/student_admission_row.html +++ b/erpnext/education/doctype/student_admission/templates/student_admission_row.html @@ -1,6 +1,6 @@
    {% set today = frappe.utils.getdate(frappe.utils.nowdate()) %} - +
    Date: Fri, 1 Apr 2022 15:50:04 +0530 Subject: [PATCH 777/951] perf: index barcode for faster scans (#30543) (#30544) (cherry picked from commit 6c5b01c60df4c125a3f3b220c351b35b924703df) Co-authored-by: Ankush Menat --- .../doctype/item_barcode/item_barcode.json | 137 +++++------------- 1 file changed, 35 insertions(+), 102 deletions(-) diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index d89ca55a4f3..eef70c95d05 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -1,109 +1,42 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:barcode", - "beta": 0, - "creation": "2017-12-09 18:54:50.562438", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "autoname": "hash", + "creation": "2022-02-11 11:26:22.155183", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "barcode", + "barcode_type" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "barcode", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Barcode", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "barcode", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Barcode", + "no_copy": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "barcode_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Barcode Type", - "length": 0, - "no_copy": 0, - "options": "\nEAN\nUPC-A", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "barcode_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Barcode Type", + "options": "\nEAN\nUPC-A" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-13 06:03:09.814357", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Barcode", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2022-04-01 05:54:27.314030", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Barcode", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file From 17f95b1f838df7e76e3d35ae6c14ce23035e036a Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 1 Apr 2022 18:47:01 +0530 Subject: [PATCH 778/951] fix: Use Payload in AutoCompleter (categories in search) and misc - Separate Item group and Item autocomplete dict definition - Add payload along with Item group, containing namke and route - Pass weightage while defining item group autocomplete dict (auto sort) - Use payload while getting results for categories in search - Remove check to show categories, always show - Search fields mandatory if reidsearch enabled - Code separation (rough) --- .../e_commerce_settings.json | 12 +---- erpnext/e_commerce/redisearch_utils.py | 46 +++++++++++++------ erpnext/templates/pages/product_search.py | 8 +++- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json index 62505e61db6..e6f08f708a8 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.json @@ -48,7 +48,6 @@ "redisearch_warning", "search_index_fields", "is_redisearch_enabled", - "show_categories_in_search_autocomplete", "is_redisearch_loaded", "shop_by_category_section", "slideshow", @@ -294,6 +293,7 @@ "fieldname": "search_index_fields", "fieldtype": "Small Text", "label": "Search Index Fields", + "mandatory_depends_on": "is_redisearch_enabled", "read_only_depends_on": "eval:!doc.is_redisearch_loaded" }, { @@ -302,14 +302,6 @@ "fieldtype": "Section Break", "label": "Item Search Settings" }, - { - "default": "1", - "depends_on": "is_redisearch_enabled", - "fieldname": "show_categories_in_search_autocomplete", - "fieldtype": "Check", - "label": "Show Categories in Search Autocomplete", - "read_only_depends_on": "eval:!doc.is_redisearch_loaded" - }, { "default": "0", "fieldname": "is_redisearch_loaded", @@ -379,7 +371,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-03-31 16:01:46.308663", + "modified": "2022-04-01 18:35:56.106756", "modified_by": "Administrator", "module": "E-commerce", "name": "E Commerce Settings", diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index 78cc05af38b..32b35db04ce 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -157,17 +157,14 @@ def delete_from_ac_dict(website_item_doc): @if_redisearch_enabled def define_autocomplete_dictionary(): - """Creates an autocomplete search dictionary for `name`. - Also creats autocomplete dictionary for `categories` if - checked in E Commerce Settings""" + """ + Defines/Redefines an autocomplete search dictionary for Website Item Name. + Also creats autocomplete dictionary for Published Item Groups. + """ cache = frappe.cache() - name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) - cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache) - - ac_categories = frappe.db.get_single_value( - "E Commerce Settings", "show_categories_in_search_autocomplete" - ) + item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) + item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache) # Delete both autocomplete dicts try: @@ -176,16 +173,39 @@ def define_autocomplete_dictionary(): except Exception: return False + create_items_autocomplete_dict(autocompleter=item_ac) + create_item_groups_autocomplete_dict(autocompleter=item_group_ac) + + +@if_redisearch_enabled +def create_items_autocomplete_dict(autocompleter): + "Add items as suggestions in Autocompleter." items = frappe.get_all( "Website Item", fields=["web_item_name", "item_group"], filters={"published": 1} ) for item in items: - name_ac.add_suggestions(Suggestion(item.web_item_name)) - if ac_categories and item.item_group: - cat_ac.add_suggestions(Suggestion(item.item_group)) + autocompleter.add_suggestions(Suggestion(item.web_item_name)) - return True + +@if_redisearch_enabled +def create_item_groups_autocomplete_dict(autocompleter): + "Add item groups with weightage as suggestions in Autocompleter." + published_item_groups = frappe.get_all( + "Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1} + ) + if not published_item_groups: + return + + for item_group in published_item_groups: + payload = {"name": item_group, "route": item_group.route} + autocompleter.add_suggestions( + Suggestion( + string=item_group.name, + score=item_group.weightage, + payload=payload, # additional info that can be retrieved later + ) + ) @if_redisearch_enabled diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index 4f2dc6e062b..f0d634be428 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe.utils import cint, cstr from redisearch import AutoCompleter, Client, Query @@ -135,8 +137,10 @@ def get_category_suggestions(query): return search_results ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) - suggestions = ac.get_suggestions(query, num=10) + suggestions = ac.get_suggestions(query, num=10, with_payloads=True) - search_results["results"] = [s.string for s in suggestions] + results = [json.loads(s.payload) for s in suggestions] + + search_results["results"] = results return search_results From c44a4e559bc05c2d21614c9ce856c0472081e040 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 11 Mar 2022 16:44:21 +0530 Subject: [PATCH 779/951] fix: incorrect payable amount for loan closure - Add penalty amount to payable amount for loan closure (cherry picked from commit 4e92926a525b396173dbc4d6dd476b2ab4874f9b) # Conflicts: # erpnext/loan_management/doctype/loan_repayment/loan_repayment.py --- .../doctype/loan_repayment/loan_repayment.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index ce50dd3b38d..2362d80223b 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -743,9 +743,17 @@ def calculate_amounts(against_loan, posting_date, payment_type=""): amounts = get_amounts(amounts, against_loan, posting_date) # update values for closure +<<<<<<< HEAD if payment_type == "Loan Closure": amounts["payable_principal_amount"] = amounts["pending_principal_amount"] amounts["interest_amount"] += amounts["unaccrued_interest"] amounts["payable_amount"] = amounts["payable_principal_amount"] + amounts["interest_amount"] +======= + if payment_type == 'Loan Closure': + amounts['payable_principal_amount'] = amounts['pending_principal_amount'] + amounts['interest_amount'] += amounts['unaccrued_interest'] + amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] + amounts['payable_amount'] = amounts['penalty_amount'] +>>>>>>> 4e92926a52 (fix: incorrect payable amount for loan closure) return amounts From 1d9a6efb1baf74fb3f8f01a69bbd15a13692a3a5 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Fri, 11 Mar 2022 16:46:30 +0530 Subject: [PATCH 780/951] fix: incorrect payable amount for loan closure (cherry picked from commit 8c76a76154d8976760d19d95f421dd2b0ee238bf) # Conflicts: # erpnext/loan_management/doctype/loan_repayment/loan_repayment.py --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 2362d80223b..d0559a51847 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -753,7 +753,11 @@ def calculate_amounts(against_loan, posting_date, payment_type=""): amounts['payable_principal_amount'] = amounts['pending_principal_amount'] amounts['interest_amount'] += amounts['unaccrued_interest'] amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] +<<<<<<< HEAD amounts['payable_amount'] = amounts['penalty_amount'] >>>>>>> 4e92926a52 (fix: incorrect payable amount for loan closure) +======= + amounts['payable_amount'] += amounts['penalty_amount'] +>>>>>>> 8c76a76154 (fix: incorrect payable amount for loan closure) return amounts From 3fc43cb259b912743c7e35df9e24e7861e07845d Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 21 Mar 2022 17:06:23 +0530 Subject: [PATCH 781/951] fix: Code cleanup (cherry picked from commit 1b2c6a5b78d4ee2e31817eb78bb1f614b672eda4) # Conflicts: # erpnext/loan_management/doctype/loan_repayment/loan_repayment.py --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d0559a51847..8468b236dba 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -752,6 +752,7 @@ def calculate_amounts(against_loan, posting_date, payment_type=""): if payment_type == 'Loan Closure': amounts['payable_principal_amount'] = amounts['pending_principal_amount'] amounts['interest_amount'] += amounts['unaccrued_interest'] +<<<<<<< HEAD amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] <<<<<<< HEAD amounts['payable_amount'] = amounts['penalty_amount'] @@ -759,5 +760,8 @@ def calculate_amounts(against_loan, posting_date, payment_type=""): ======= amounts['payable_amount'] += amounts['penalty_amount'] >>>>>>> 8c76a76154 (fix: incorrect payable amount for loan closure) +======= + amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] + amounts['penalty_amount'] +>>>>>>> 1b2c6a5b78 (fix: Code cleanup) return amounts From 7bf6de18834b2749187979911115c4eeae957612 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 2 Apr 2022 20:33:46 +0530 Subject: [PATCH 782/951] fix: Resolve conflicts --- .../doctype/loan_repayment/loan_repayment.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 8468b236dba..6159275c5d1 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -743,25 +743,12 @@ def calculate_amounts(against_loan, posting_date, payment_type=""): amounts = get_amounts(amounts, against_loan, posting_date) # update values for closure -<<<<<<< HEAD if payment_type == "Loan Closure": amounts["payable_principal_amount"] = amounts["pending_principal_amount"] amounts["interest_amount"] += amounts["unaccrued_interest"] amounts["payable_amount"] = amounts["payable_principal_amount"] + amounts["interest_amount"] -======= - if payment_type == 'Loan Closure': - amounts['payable_principal_amount'] = amounts['pending_principal_amount'] - amounts['interest_amount'] += amounts['unaccrued_interest'] -<<<<<<< HEAD - amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] -<<<<<<< HEAD - amounts['payable_amount'] = amounts['penalty_amount'] ->>>>>>> 4e92926a52 (fix: incorrect payable amount for loan closure) -======= - amounts['payable_amount'] += amounts['penalty_amount'] ->>>>>>> 8c76a76154 (fix: incorrect payable amount for loan closure) -======= - amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] + amounts['penalty_amount'] ->>>>>>> 1b2c6a5b78 (fix: Code cleanup) + amounts["payable_amount"] = ( + amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["penalty_amount"] + ) return amounts From b4d427b42960e43236bc916eac4c91a3fb7a2a49 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Apr 2022 11:07:53 +0530 Subject: [PATCH 783/951] fix: Better Exception Handling and vaeiabl naming - Function to handle RS exceptions (create log and raise error) - Handle `ResponseError` where it is anticipated - Misc: Better variables --- erpnext/e_commerce/redisearch_utils.py | 44 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index 32b35db04ce..f9890cca1a8 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -1,8 +1,10 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt import frappe +from frappe import _ from frappe.utils.redis_wrapper import RedisWrapper +from redis import ResponseError from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField WEBSITE_ITEM_INDEX = "website_items_index" @@ -38,7 +40,7 @@ def is_search_module_loaded(): ) return "search" in parsed_output except Exception: - return False + return False # handling older redis versions def if_redisearch_enabled(function): @@ -64,15 +66,18 @@ def create_website_items_index(): # CREATE index client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) - # DROP if already exists try: - client.drop_index() - except Exception: + client.drop_index() # drop if already exists + except ResponseError: + # will most likely raise a ResponseError if index does not exist + # ignore and create index pass + except Exception: + raise_redisearch_error() idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)]) - # Based on e-commerce settings + # Index fields mentioned in e-commerce settings idx_fields = frappe.db.get_single_value("E Commerce Settings", "search_index_fields") idx_fields = idx_fields.split(",") if idx_fields else [] @@ -104,8 +109,8 @@ def insert_item_to_index(website_item_doc): cache = frappe.cache() web_item = create_web_item_map(website_item_doc) - for k, v in web_item.items(): - super(RedisWrapper, cache).hset(make_key(key), k, v) + for field, value in web_item.items(): + super(RedisWrapper, cache).hset(make_key(key), field, value) insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name) @@ -120,8 +125,8 @@ def create_web_item_map(website_item_doc): fields_to_index = get_fields_indexed() web_item = {} - for f in fields_to_index: - web_item[f] = website_item_doc.get(f) or "" + for field in fields_to_index: + web_item[field] = website_item_doc.get(field) or "" return web_item @@ -141,7 +146,7 @@ def delete_item_from_index(website_item_doc): try: cache.delete(key) except Exception: - return False + raise_redisearch_error() delete_from_ac_dict(website_item_doc) return True @@ -171,7 +176,7 @@ def define_autocomplete_dictionary(): cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE)) cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE)) except Exception: - return False + raise_redisearch_error() create_items_autocomplete_dict(autocompleter=item_ac) create_item_groups_autocomplete_dict(autocompleter=item_group_ac) @@ -217,8 +222,8 @@ def reindex_all_web_items(): web_item = create_web_item_map(item) key = make_key(get_cache_key(item.name)) - for k, v in web_item.items(): - super(RedisWrapper, cache).hset(key, k, v) + for field, value in web_item.items(): + super(RedisWrapper, cache).hset(key, field, value) def get_cache_key(name): @@ -234,3 +239,14 @@ def get_fields_indexed(): fields_to_index = fields_to_index + mandatory_fields return fields_to_index + + +def raise_redisearch_error(): + "Create an Error Log and raise error." + traceback = frappe.get_traceback() + log = frappe.log_error(traceback, frappe._("Redisearch Error")) + log_link = frappe.utils.get_link_to_form("Error Log", log.name) + + frappe.throw( + msg=_("Something went wrong. Check {0}").format(log_link), title=_("Redisearch Error") + ) From 5c36d1236957aa8f5996f32fc1830e99565b1220 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Apr 2022 11:32:49 +0530 Subject: [PATCH 784/951] fix: Convert payload to string before adding to autocompleter --- erpnext/e_commerce/redisearch_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index f9890cca1a8..95b74e0795c 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -1,6 +1,8 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +import json + import frappe from frappe import _ from frappe.utils.redis_wrapper import RedisWrapper @@ -203,7 +205,7 @@ def create_item_groups_autocomplete_dict(autocompleter): return for item_group in published_item_groups: - payload = {"name": item_group, "route": item_group.route} + payload = json.dumps({"name": item_group, "route": item_group.route}) autocompleter.add_suggestions( Suggestion( string=item_group.name, From 0f9277a8c8850c918c3a725953deb9920930bc9a Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Apr 2022 12:04:35 +0530 Subject: [PATCH 785/951] fix: Add default score of 1 to Item Group Autocompleter - If score 0 is inserted into suggestions, RS does not consider that suggestion --- erpnext/e_commerce/redisearch_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index 95b74e0795c..b2f97e308c4 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -209,7 +209,7 @@ def create_item_groups_autocomplete_dict(autocompleter): autocompleter.add_suggestions( Suggestion( string=item_group.name, - score=item_group.weightage, + score=frappe.utils.flt(item_group.weightage) or 1.0, payload=payload, # additional info that can be retrieved later ) ) From 42ec9db5f422c7392d5e9b7e3af3c548996172aa Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Apr 2022 12:33:25 +0530 Subject: [PATCH 786/951] fix: Payload incorrect data (pass item_group.name) --- erpnext/e_commerce/redisearch_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index b2f97e308c4..f2dd796f2c5 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -205,7 +205,7 @@ def create_item_groups_autocomplete_dict(autocompleter): return for item_group in published_item_groups: - payload = json.dumps({"name": item_group, "route": item_group.route}) + payload = json.dumps({"name": item_group.name, "route": item_group.route}) autocompleter.add_suggestions( Suggestion( string=item_group.name, From 4dc7047a5997fb4e71667680f3616c517075aa82 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Apr 2022 13:24:56 +0530 Subject: [PATCH 787/951] chore: Add TODOs(perf) for Redisearch Query --- erpnext/templates/pages/product_search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py index f0d634be428..7d473f37b8e 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -88,6 +88,8 @@ def product_search(query, limit=10, fuzzy_search=True): red = frappe.cache() query = clean_up_query(query) + # TODO: Check perf/correctness with Suggestions & Query vs only Query + # TODO: Use Levenshtein Distance in Query (max=3) ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = ac.get_suggestions( From 4cc23830f170be836ff793b517ff64c014176e42 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 4 Apr 2022 15:46:49 +0530 Subject: [PATCH 788/951] fix: maintain FIFO queue even if outgoing_rate is not found (#30563) port of #30560 --- erpnext/stock/stock_ledger.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0664a8352b9..b95bcab7149 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -866,16 +866,9 @@ class update_entries_after(object): index = i break - # If no entry found with outgoing rate, collapse stack + # If no entry found with outgoing rate, consume as per FIFO if index is None: # nosemgrep - new_stock_value = ( - sum((d[0] * d[1] for d in self.wh_data.stock_queue)) - qty_to_pop * outgoing_rate - ) - new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop - self.wh_data.stock_queue = [ - [new_stock_qty, new_stock_value / new_stock_qty if new_stock_qty > 0 else outgoing_rate] - ] - break + index = 0 else: index = 0 From b524e657e21ac4a27bbbcffad4a9ddeb0afe1f99 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 16:35:46 +0530 Subject: [PATCH 789/951] fix(pos): do not reset search input on item selection (backport #30537) --- erpnext/selling/page/point_of_sale/pos_item_selector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 1177615aee9..b62b27bc4b3 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -243,7 +243,7 @@ erpnext.PointOfSale.ItemSelector = class { value: "+1", item: { item_code, batch_no, serial_no, uom, rate } }); - me.set_search_value(''); + me.search_field.set_focus(); }); this.search_field.$input.on('input', (e) => { @@ -328,6 +328,7 @@ erpnext.PointOfSale.ItemSelector = class { add_filtered_item_to_cart() { this.$items_container.find(".item-wrapper").click(); + this.set_search_value(''); } resize_selector(minimize) { From 012cab80bf2ad4b2ce68dfcdd909a0176633a42d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 17:31:26 +0530 Subject: [PATCH 790/951] fix(ux): refresh update to zero val checkbox (#30567) (#30568) (cherry picked from commit de83511091189194d70b77411298fd809060063d) Co-authored-by: Ankush Menat --- erpnext/stock/doctype/stock_entry/stock_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 61466cff032..4ec9f1f220f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -631,7 +631,7 @@ frappe.ui.form.on('Stock Entry Detail', { // set allow_zero_valuation_rate to 0 if s_warehouse is selected. let item = frappe.get_doc(cdt, cdn); if (item.s_warehouse) { - item.allow_zero_valuation_rate = 0; + frappe.model.set_value(cdt, cdn, "allow_zero_valuation_rate", 0); } }, From 157461ed0204b4fa45a19200977a26bc6128a54d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 4 Apr 2022 15:10:57 +0530 Subject: [PATCH 791/951] fix: if accepted warehouse not selected during rejection then stock ledger not created (cherry picked from commit 0a71cabab13075fa05a7df1942776a2f08c47089) --- erpnext/controllers/buying_controller.py | 26 ++++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6305146f31c..fc507cbfe16 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -464,6 +464,9 @@ class BuyingController(StockController, Subcontracting): stock_items = self.get_stock_items() for d in self.get("items"): + if d.item_code not in stock_items: + continue + if d.item_code in stock_items and d.warehouse: pr_qty = flt(d.qty) * flt(d.conversion_factor) @@ -489,6 +492,7 @@ class BuyingController(StockController, Subcontracting): sle = self.get_sl_entries( d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()} ) + if self.is_return: outgoing_rate = get_rate_for_return( self.doctype, self.name, d.item_code, self.return_against, item_row=d @@ -518,18 +522,18 @@ class BuyingController(StockController, Subcontracting): sl_entries.append(from_warehouse_sle) - if flt(d.rejected_qty) != 0: - sl_entries.append( - self.get_sl_entries( - d, - { - "warehouse": d.rejected_warehouse, - "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), - "serial_no": cstr(d.rejected_serial_no).strip(), - "incoming_rate": 0.0, - }, - ) + if flt(d.rejected_qty) != 0: + sl_entries.append( + self.get_sl_entries( + d, + { + "warehouse": d.rejected_warehouse, + "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor), + "serial_no": cstr(d.rejected_serial_no).strip(), + "incoming_rate": 0.0, + }, ) + ) self.make_sl_entries_for_supplier_warehouse(sl_entries) self.make_sl_entries( From 3038a5cd5a73d7b5a91ae9e3de642531cb673d00 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 4 Apr 2022 15:38:42 +0530 Subject: [PATCH 792/951] test: test case to validate rejected qty without accepted warehouse (cherry picked from commit ac5df1abbe80255d685966c108835cdb75f90659) --- erpnext/controllers/buying_controller.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fc507cbfe16..931b4f82d97 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -467,7 +467,7 @@ class BuyingController(StockController, Subcontracting): if d.item_code not in stock_items: continue - if d.item_code in stock_items and d.warehouse: + if d.warehouse: pr_qty = flt(d.qty) * flt(d.conversion_factor) if pr_qty: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0da89937ed0..65c30de0978 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -663,6 +663,45 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.cancel() pr.cancel() + def test_purchase_receipt_for_rejected_gle_without_accepted_warehouse(self): + from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse + + rejected_warehouse = "_Test Rejected Warehouse - TCP1" + if not frappe.db.exists("Warehouse", rejected_warehouse): + get_warehouse( + company="_Test Company with perpetual inventory", + abbr=" - TCP1", + warehouse_name="_Test Rejected Warehouse", + ).name + + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + received_qty=2, + rejected_qty=2, + rejected_warehouse=rejected_warehouse, + do_not_save=True, + ) + + pr.items[0].qty = 0.0 + pr.items[0].warehouse = "" + pr.submit() + + actual_qty = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "warehouse": pr.items[0].rejected_warehouse, + "is_cancelled": 0, + }, + "actual_qty", + ) + + self.assertEqual(actual_qty, 2) + self.assertFalse(pr.items[0].warehouse) + pr.cancel() + def test_purchase_return_for_serialized_items(self): def _check_serial_no_values(serial_no, field_values): serial_no = frappe.get_doc("Serial No", serial_no) From 0a2c72c594963f63551985a908c1c79302556e91 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Mon, 4 Apr 2022 06:37:51 -0700 Subject: [PATCH 793/951] fix: Validation for single threshold in Tax With Holding Category (#30382) --- .../tax_withholding_category/tax_withholding_category.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index ced29f960a7..63698439be1 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -25,7 +25,9 @@ class TaxWithholdingCategory(Document): def validate_thresholds(self): for d in self.get("rates"): - if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold: + if ( + d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold + ): frappe.throw( _("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format( d.idx From c0ebcfb39331caa678d36cc4694490a2363f10a0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 4 Apr 2022 20:05:10 +0530 Subject: [PATCH 794/951] fix: Do not apply shipping rule for POS transactions --- erpnext/controllers/taxes_and_totals.py | 5 +++++ erpnext/public/js/controllers/taxes_and_totals.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 6fbc98b591e..8b2a69542a7 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -307,6 +307,11 @@ class calculate_taxes_and_totals(object): self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"]) def calculate_shipping_charges(self): + + # Do not apply shipping rule for POS + if self.doc.is_pos: + return + if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule.apply(self.doc) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 308f0a4871f..2b1b0e3576b 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -271,6 +271,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, calculate_shipping_charges: function() { + // Do not apply shipping rule for POS + if (this.frm.doc.is_pos) { + return; + } + frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { return this.shipping_rule(); From 1093307dcc828fec9f196a782636ef25f5ea0e8a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 Apr 2022 17:52:30 +0530 Subject: [PATCH 795/951] fix: total leaves allocated not validated and recalculated on updates post submission (cherry picked from commit 3538656a7d6b82f6e84a7031f1542f1ce2ec57f4) --- .../leave_allocation/leave_allocation.py | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 98408afab64..27479a5e81f 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -39,11 +39,15 @@ class LeaveAllocation(Document): def validate(self): self.validate_period() self.validate_allocation_overlap() - self.validate_back_dated_allocation() - self.set_total_leaves_allocated() - self.validate_total_leaves_allocated() self.validate_lwp() set_employee_name(self) + self.set_total_leaves_allocated() + self.validate_leave_days_and_dates() + + def validate_leave_days_and_dates(self): + # all validations that should run on save as well as on update after submit + self.validate_back_dated_allocation() + self.validate_total_leaves_allocated() self.validate_leave_allocation_days() def validate_leave_allocation_days(self): @@ -56,14 +60,19 @@ class LeaveAllocation(Document): leave_allocated = 0 if leave_period: leave_allocated = get_leave_allocation_for_period( - self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date + self.employee, + self.leave_type, + leave_period[0].from_date, + leave_period[0].to_date, + exclude_allocation=self.name, ) leave_allocated += flt(self.new_leaves_allocated) if leave_allocated > max_leaves_allowed: frappe.throw( _( - "Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period" - ).format(self.leave_type, self.employee) + "Total allocated leaves are more than maximum allocation allowed for {0} leave type for employee {1} in the period" + ).format(self.leave_type, self.employee), + OverAllocationError, ) def on_submit(self): @@ -84,6 +93,12 @@ class LeaveAllocation(Document): def on_update_after_submit(self): if self.has_value_changed("new_leaves_allocated"): self.validate_against_leave_applications() + + # recalculate total leaves allocated + self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated) + # run required validations again since total leaves are being updated + self.validate_leave_days_and_dates() + leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count() args = { "leaves": leaves_to_be_added, @@ -92,6 +107,7 @@ class LeaveAllocation(Document): "is_carry_forward": 0, } create_leave_ledger_entry(self, args, True) + self.db_update() def get_existing_leave_count(self): ledger_entries = frappe.get_all( @@ -279,27 +295,27 @@ def get_previous_allocation(from_date, leave_type, employee): ) -def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): - leave_allocated = 0 - leave_allocations = frappe.db.sql( - """ - select employee, leave_type, from_date, to_date, total_leaves_allocated - from `tabLeave Allocation` - where employee=%(employee)s and leave_type=%(leave_type)s - and docstatus=1 - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) - """, - {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, - as_dict=1, - ) +def get_leave_allocation_for_period( + employee, leave_type, from_date, to_date, exclude_allocation=None +): + from frappe.query_builder.functions import Sum - if leave_allocations: - for leave_alloc in leave_allocations: - leave_allocated += leave_alloc.total_leaves_allocated - - return leave_allocated + Allocation = frappe.qb.DocType("Leave Allocation") + return ( + frappe.qb.from_(Allocation) + .select(Sum(Allocation.total_leaves_allocated).as_("total_allocated_leaves")) + .where( + (Allocation.employee == employee) + & (Allocation.leave_type == leave_type) + & (Allocation.docstatus == 1) + & (Allocation.name != exclude_allocation) + & ( + (Allocation.from_date.between(from_date, to_date)) + | (Allocation.to_date.between(from_date, to_date)) + | ((Allocation.from_date < from_date) & (Allocation.to_date > to_date)) + ) + ) + ).run()[0][0] or 0.0 @frappe.whitelist() From e41f35aa8292d765eb6d20024f87aad334934144 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 Apr 2022 18:32:17 +0530 Subject: [PATCH 796/951] test: leave allocation validations and total value for updates done before and after submission (cherry picked from commit 5499cecffd76f7e5414181855008cdb8e50634ef) --- .../leave_allocation/test_leave_allocation.py | 158 ++++++++++++++++-- 1 file changed, 148 insertions(+), 10 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index a53d4a82ba6..6b3636db355 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,24 +1,26 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, add_months, getdate, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.leave_allocation.leave_allocation import ( + BackDatedAllocationError, + OverAllocationError, +) from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type -class TestLeaveAllocation(unittest.TestCase): - @classmethod - def setUpClass(cls): - frappe.db.sql("delete from `tabLeave Period`") +class TestLeaveAllocation(FrappeTestCase): + def setUp(self): + frappe.db.delete("Leave Period") + frappe.db.delete("Leave Allocation") emp_id = make_employee("test_emp_leave_allocation@salary.com") - cls.employee = frappe.get_doc("Employee", emp_id) - - def tearDown(self): - frappe.db.rollback() + self.employee = frappe.get_doc("Employee", emp_id) def test_overlapping_allocation(self): leaves = [ @@ -65,7 +67,7 @@ class TestLeaveAllocation(unittest.TestCase): # invalid period self.assertRaises(frappe.ValidationError, doc.save) - def test_allocated_leave_days_over_period(self): + def test_validation_for_over_allocation(self): doc = frappe.get_doc( { "doctype": "Leave Allocation", @@ -80,7 +82,135 @@ class TestLeaveAllocation(unittest.TestCase): ) # allocated leave more than period - self.assertRaises(frappe.ValidationError, doc.save) + self.assertRaises(OverAllocationError, doc.save) + + def test_validation_for_over_allocation_post_submission(self): + allocation = frappe.get_doc( + { + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": self.employee.name, + "employee_name": self.employee.employee_name, + "leave_type": "_Test Leave Type", + "from_date": getdate("2015-09-1"), + "to_date": getdate("2015-09-30"), + "new_leaves_allocated": 15, + } + ).submit() + allocation.reload() + # allocated leaves more than period after submission + allocation.new_leaves_allocated = 35 + self.assertRaises(OverAllocationError, allocation.save) + + def test_validation_for_over_allocation_based_on_leave_setup(self): + frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period") + leave_period = frappe.get_doc( + dict( + name="Test Allocation Period", + doctype="Leave Period", + from_date=add_months(nowdate(), -6), + to_date=add_months(nowdate(), 6), + company="_Test Company", + is_active=1, + ) + ).insert() + + leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1) + leave_type.max_leaves_allowed = 25 + leave_type.save() + + # 15 leaves allocated in this period + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=leave_period.from_date, + to_date=nowdate(), + ) + allocation.submit() + + # trying to allocate additional 15 leaves + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=add_days(nowdate(), 1), + to_date=leave_period.to_date, + ) + self.assertRaises(OverAllocationError, allocation.save) + + def test_validation_for_over_allocation_based_on_leave_setup_post_submission(self): + frappe.delete_doc_if_exists("Leave Period", "Test Allocation Period") + leave_period = frappe.get_doc( + dict( + name="Test Allocation Period", + doctype="Leave Period", + from_date=add_months(nowdate(), -6), + to_date=add_months(nowdate(), 6), + company="_Test Company", + is_active=1, + ) + ).insert() + + leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1) + leave_type.max_leaves_allowed = 30 + leave_type.save() + + # 15 leaves allocated + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=leave_period.from_date, + to_date=nowdate(), + ) + allocation.submit() + allocation.reload() + + # allocate additional 15 leaves + allocation = create_leave_allocation( + leave_type=leave_type.name, + employee=self.employee.name, + employee_name=self.employee.employee_name, + from_date=add_days(nowdate(), 1), + to_date=leave_period.to_date, + ) + allocation.submit() + allocation.reload() + + # trying to allocate 25 leaves in 2nd alloc within leave period + # total leaves = 40 which is more than `max_leaves_allowed` setting i.e. 30 + allocation.new_leaves_allocated = 25 + self.assertRaises(OverAllocationError, allocation.save) + + def test_validate_back_dated_allocation_update(self): + leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1) + leave_type.save() + + # initial leave allocation = 15 + leave_allocation = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, + leave_type="_Test_CF_leave", + from_date=add_months(nowdate(), -12), + to_date=add_months(nowdate(), -1), + carry_forward=0, + ) + leave_allocation.submit() + + # new_leaves = 15, carry_forwarded = 10 + leave_allocation_1 = create_leave_allocation( + employee=self.employee.name, + employee_name=self.employee.employee_name, + leave_type="_Test_CF_leave", + carry_forward=1, + ) + leave_allocation_1.submit() + + # try updating initial leave allocation + leave_allocation.reload() + leave_allocation.new_leaves_allocated = 20 + self.assertRaises(BackDatedAllocationError, leave_allocation.save) def test_carry_forward_calculation(self): leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1) @@ -108,8 +238,10 @@ class TestLeaveAllocation(unittest.TestCase): carry_forward=1, ) leave_allocation_1.submit() + leave_allocation_1.reload() self.assertEqual(leave_allocation_1.unused_leaves, 10) + self.assertEqual(leave_allocation_1.total_leaves_allocated, 25) leave_allocation_1.cancel() @@ -197,9 +329,12 @@ class TestLeaveAllocation(unittest.TestCase): employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 40 leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 40) def test_leave_subtraction_after_submit(self): @@ -207,9 +342,12 @@ class TestLeaveAllocation(unittest.TestCase): employee=self.employee.name, employee_name=self.employee.employee_name ) leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 15) + leave_allocation.new_leaves_allocated = 10 leave_allocation.submit() + leave_allocation.reload() self.assertTrue(leave_allocation.total_leaves_allocated, 10) def test_validation_against_leave_application_after_submit(self): From cb2a8aab3131ee0eb40437dc03c560ca01443626 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 4 Apr 2022 19:08:27 +0530 Subject: [PATCH 797/951] fix(test): set company for employee in leave allocation test setup (cherry picked from commit 793164ac2efe9588d16e88cc27141cb03cf57d36) --- erpnext/hr/doctype/leave_allocation/test_leave_allocation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 6b3636db355..dde52d7ad8e 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -19,7 +19,7 @@ class TestLeaveAllocation(FrappeTestCase): frappe.db.delete("Leave Period") frappe.db.delete("Leave Allocation") - emp_id = make_employee("test_emp_leave_allocation@salary.com") + emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company") self.employee = frappe.get_doc("Employee", emp_id) def test_overlapping_allocation(self): From 01404b2b5bccdcdb0843583451728c11bd9281a0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 4 Apr 2022 12:28:09 +0530 Subject: [PATCH 798/951] fix(india): cannot generate e-invoice for is_pos invoices * If mode of payment > 18 characters, the e-invoice portal throws error (cherry picked from commit 0c26f9a8c8100104cb5ef3923e5cf9e739f3adae) --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 990fe25e59f..95cbcd51a9f 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -388,7 +388,7 @@ def update_other_charges( def get_payment_details(invoice): payee_name = invoice.company - mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments]) + mode_of_payment = "" paid_amount = invoice.base_paid_amount outstanding_amount = invoice.outstanding_amount From e1bd156f40612be9da51842ad083a784e610b269 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 4 Apr 2022 14:40:07 +0530 Subject: [PATCH 799/951] fix: server error while viewing gst e-invoice (cherry picked from commit b91bf40f1bb11162f5589f7bde408e266f3d89bd) --- .../print_format/gst_e_invoice/gst_e_invoice.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index e6580493095..605ce8383e4 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -1,7 +1,8 @@ {%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} -{%- set einvoice = json.loads(doc.signed_einvoice) -%}
    + {% if doc.signed_einvoice %} + {%- set einvoice = json.loads(doc.signed_einvoice) -%}
    {% if letter_head and not no_letterhead %}
    {{ letter_head }}
    @@ -170,4 +171,10 @@
    + {% else %} +
    + You must generate IRN before you can preview GST E-Invoice. +
    + {% endif %}
    + From 0700ee8a06b86404e9cc0afb0e374e5fc3c28ffd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 5 Apr 2022 15:38:39 +0530 Subject: [PATCH 800/951] fix: fetch from fields not working in eway bill dialog (cherry picked from commit c8779aa4465ddf58db7751c39e1a89c9ea92eead) --- erpnext/regional/india/e_invoice/einvoice.js | 32 +++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 348f0c6feed..17b018c65b4 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -105,6 +105,30 @@ erpnext.setup_einvoice_actions = (doctype) => { }, primary_action_label: __('Submit') }); + d.fields_dict.transporter.df.onchange = function () { + const transporter = d.fields_dict.transporter.value; + if (transporter) { + frappe.db.get_value('Supplier', transporter, ['gst_transporter_id', 'supplier_name']) + .then(({ message }) => { + d.set_value('gst_transporter_id', message.gst_transporter_id); + d.set_value('transporter_name', message.supplier_name); + }); + } else { + d.set_value('gst_transporter_id', ''); + d.set_value('transporter_name', ''); + } + }; + d.fields_dict.driver.df.onchange = function () { + const driver = d.fields_dict.driver.value; + if (driver) { + frappe.db.get_value('Driver', driver, ['full_name']) + .then(({ message }) => { + d.set_value('driver_name', message.full_name); + }); + } else { + d.set_value('driver_name', ''); + } + }; d.show(); }; @@ -153,7 +177,6 @@ const get_ewaybill_fields = (frm) => { 'fieldname': 'gst_transporter_id', 'label': 'GST Transporter ID', 'fieldtype': 'Data', - 'fetch_from': 'transporter.gst_transporter_id', 'default': frm.doc.gst_transporter_id }, { @@ -189,9 +212,9 @@ const get_ewaybill_fields = (frm) => { 'fieldname': 'transporter_name', 'label': 'Transporter Name', 'fieldtype': 'Data', - 'fetch_from': 'transporter.name', 'read_only': 1, - 'default': frm.doc.transporter_name + 'default': frm.doc.transporter_name, + 'depends_on': 'transporter' }, { 'fieldname': 'mode_of_transport', @@ -206,7 +229,8 @@ const get_ewaybill_fields = (frm) => { 'fieldtype': 'Data', 'fetch_from': 'driver.full_name', 'read_only': 1, - 'default': frm.doc.driver_name + 'default': frm.doc.driver_name, + 'depends_on': 'driver' }, { 'fieldname': 'lr_date', From 732b302be0162b27d20a11fb8c6dc3a9aabdb485 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 12:07:50 +0530 Subject: [PATCH 801/951] chore: Accessibility for E-commerce Doctypes - Add Website Item routing button and dashboard link in Item master - Group Item variant buttons together (cherry picked from commit d4301d6d2f4a2396b8dcfc2845574115e05636d1) --- erpnext/stock/doctype/item/item.js | 18 ++++++++++++------ erpnext/stock/doctype/item/item_dashboard.py | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 2a2eafbb391..8206e095797 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -55,10 +55,15 @@ frappe.ui.form.on("Item", { if (frm.doc.has_variants) { frm.set_intro(__("This Item is a Template and cannot be used in transactions. Item attributes will be copied over into the variants unless 'No Copy' is set"), true); + frm.add_custom_button(__("Show Variants"), function() { frappe.set_route("List", "Item", {"variant_of": frm.doc.name}); }, __("View")); + frm.add_custom_button(__("Item Variant Settings"), function() { + frappe.set_route("Form", "Item Variant Settings"); + }, __("View")); + frm.add_custom_button(__("Variant Details Report"), function() { frappe.set_route("query-report", "Item Variant Details", {"item": frm.doc.name}); }, __("View")); @@ -110,6 +115,13 @@ frappe.ui.form.on("Item", { } }); }, __('Actions')); + } else { + frm.add_custom_button(__("Website Item"), function() { + frappe.db.get_value("Website Item", {item_code: frm.doc.name}, "name", (d) => { + if (!d.name) frappe.throw(__("Website Item not found")); + frappe.set_route("Form", "Website Item", d.name); + }); + }, __("View")); } erpnext.item.edit_prices_button(frm); @@ -131,12 +143,6 @@ frappe.ui.form.on("Item", { frappe.set_route('Form', 'Item', new_item.name); }); - if(frm.doc.has_variants) { - frm.add_custom_button(__("Item Variant Settings"), function() { - frappe.set_route("Form", "Item Variant Settings"); - }, __("View")); - } - const stock_exists = (frm.doc.__onload && frm.doc.__onload.stock_exists) ? 1 : 0; diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py index 33acf4bfd8a..3caed02d69b 100644 --- a/erpnext/stock/doctype/item/item_dashboard.py +++ b/erpnext/stock/doctype/item/item_dashboard.py @@ -32,5 +32,6 @@ def get_data(): {"label": _("Manufacture"), "items": ["Production Plan", "Work Order", "Item Manufacturer"]}, {"label": _("Traceability"), "items": ["Serial No", "Batch"]}, {"label": _("Move"), "items": ["Stock Entry"]}, + {"label": _("E-commerce"), "items": ["Website Item"]}, ], } From c636b366d5c0eac58d80f0d26645ad910d66412c Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 12:30:02 +0530 Subject: [PATCH 802/951] chore: Add Prices, Stock and E-com Settings access from Website Item (cherry picked from commit 065623ce2526c3df6f3355675d3138fb86550af0) --- .../doctype/website_item/website_item.js | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js index 7108cabfb3f..7295e4b56a0 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.js +++ b/erpnext/e_commerce/doctype/website_item/website_item.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Website Item', { - onload: function(frm) { + onload: (frm) => { // should never check Private frm.fields_dict["website_image"].df.is_private = 0; @@ -13,18 +13,35 @@ frappe.ui.form.on('Website Item', { }); }, - image: function() { + refresh: (frm) => { + frm.add_custom_button(__("Prices"), function() { + frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code}); + }, __("View")); + + frm.add_custom_button(__("Stock"), function() { + frappe.route_options = { + "item_code": frm.doc.item_code + }; + frappe.set_route("query-report", "Stock Balance"); + }, __("View")); + + frm.add_custom_button(__("E Commerce Settings"), function() { + frappe.set_route("Form", "E Commerce Settings"); + }, __("View")); + }, + + image: () => { refresh_field("image_view"); }, - copy_from_item_group: function(frm) { + copy_from_item_group: (frm) => { return frm.call({ doc: frm.doc, method: "copy_specification_from_item_group" }); }, - set_meta_tags(frm) { + set_meta_tags: (frm) => { frappe.utils.set_meta_tag(frm.doc.route); } }); From e8f3e23008b9899297ead7951cb2c57ccdffb545 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 18:22:35 +0530 Subject: [PATCH 803/951] fix: Merge Conflicts --- .../bom_update_tool/bom_update_tool.py | 183 ++---------------- 1 file changed, 12 insertions(+), 171 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index a7573902d78..b0e7da12017 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -9,142 +9,32 @@ if TYPE_CHECKING: import frappe from frappe.model.document import Document -<<<<<<< HEAD -from frappe.utils import cstr, flt -from six import string_types -======= ->>>>>>> f3715ab382 (fix: Test, Sider and Added button to access log from Tool) from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order class BOMUpdateTool(Document): -<<<<<<< HEAD - def replace_bom(self): - unit_cost = get_new_bom_unit_cost(self.new_bom) - self.update_new_bom(unit_cost) - - frappe.cache().delete_key("bom_children") - bom_list = self.get_parent_boms(self.new_bom) - - with click.progressbar(bom_list) as bom_list: - pass - for bom in bom_list: - try: - bom_obj = frappe.get_cached_doc("BOM", bom) - # this is only used for versioning and we do not want - # to make separate db calls by using load_doc_before_save - # which proves to be expensive while doing bulk replace - bom_obj._doc_before_save = bom_obj - bom_obj.update_new_bom(self.current_bom, self.new_bom, unit_cost) - bom_obj.update_exploded_items() - bom_obj.calculate_cost() - bom_obj.update_parent_cost() - bom_obj.db_update() - if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: - bom_obj.save_version() - except Exception: - frappe.log_error(frappe.get_traceback()) - -<<<<<<< HEAD - def validate_bom(self): - if cstr(self.current_bom) == cstr(self.new_bom): - frappe.throw(_("Current BOM and New BOM can not be same")) - - if frappe.db.get_value("BOM", self.current_bom, "item") != frappe.db.get_value( - "BOM", self.new_bom, "item" - ): - frappe.throw(_("The selected BOMs are not for the same item")) - -======= ->>>>>>> 4283a13e5a (feat: BOM Update Log) - def update_new_bom(self, unit_cost): - frappe.db.sql( - """update `tabBOM Item` set bom_no=%s, - rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""", - (self.new_bom, unit_cost, unit_cost, self.current_bom), - ) - - def get_parent_boms(self, bom, bom_list=None): - if bom_list is None: - bom_list = [] - data = frappe.db.sql( - """SELECT DISTINCT parent FROM `tabBOM Item` - WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", - bom, - ) - - for d in data: - if self.new_bom == d[0]: - frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(bom, self.new_bom)) - - bom_list.append(d[0]) - self.get_parent_boms(d[0], bom_list) - - return list(set(bom_list)) - - -def get_new_bom_unit_cost(bom): - new_bom_unitcost = frappe.db.sql( - """SELECT `total_cost`/`quantity` - FROM `tabBOM` WHERE name = %s""", - bom, - ) - - return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0 -======= pass ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) @frappe.whitelist() -<<<<<<< HEAD -def enqueue_replace_bom(args): - if isinstance(args, string_types): - args = json.loads(args) - -<<<<<<< HEAD - frappe.enqueue( - "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", - args=args, - timeout=40000, - ) -======= - create_bom_update_log(boms=args) ->>>>>>> 4283a13e5a (feat: BOM Update Log) - frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) -======= -def enqueue_replace_bom(boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None) -> "BOMUpdateLog": +def enqueue_replace_bom( + boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None +) -> "BOMUpdateLog": """Returns a BOM Update Log (that queues a job) for BOM Replacement.""" boms = boms or args if isinstance(boms, str): boms = json.loads(boms) ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) update_log = create_bom_update_log(boms=boms) return update_log -@frappe.whitelist() -<<<<<<< HEAD -def enqueue_update_cost(): -<<<<<<< HEAD - frappe.enqueue( - "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000 - ) - frappe.msgprint( - _("Queued for updating latest price in all Bill of Materials. It may take a few minutes.") - ) -======= - create_bom_update_log(update_type="Update Cost") - frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) ->>>>>>> 4283a13e5a (feat: BOM Update Log) -======= +@frappe.whitelist() def enqueue_update_cost() -> "BOMUpdateLog": """Returns a BOM Update Log (that queues a job) for BOM Cost Updation.""" update_log = create_bom_update_log(update_type="Update Cost") return update_log ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) def auto_update_latest_price_in_all_boms() -> None: @@ -152,77 +42,28 @@ def auto_update_latest_price_in_all_boms() -> None: if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() -<<<<<<< HEAD -<<<<<<< HEAD -def replace_bom(args): - try: - frappe.db.auto_commit_on_many_writes = 1 - args = frappe._dict(args) - doc = frappe.get_doc("BOM Update Tool") - doc.current_bom = args.current_bom - doc.new_bom = args.new_bom - doc.replace_bom() - except Exception: - frappe.log_error( - msg=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) - finally: - frappe.db.auto_commit_on_many_writes = 0 - - -def update_cost(): - try: - frappe.db.auto_commit_on_many_writes = 1 - bom_list = get_boms_in_bottom_up_order() - for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) - except Exception: - frappe.log_error( - msg=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) - finally: - frappe.db.auto_commit_on_many_writes = 0 -======= -def create_bom_update_log(boms=None, update_type="Replace BOM"): - "Creates a BOM Update Log that handles the background job." - current_bom = boms.get("current_bom") if boms else None - new_bom = boms.get("new_bom") if boms else None - log_doc = frappe.get_doc({ - "doctype": "BOM Update Log", - "current_bom": current_bom, - "new_bom": new_bom, - "update_type": update_type - }) - log_doc.submit() ->>>>>>> 4283a13e5a (feat: BOM Update Log) -======= def update_cost() -> None: """Updates Cost for all BOMs from bottom to top.""" bom_list = get_boms_in_bottom_up_order() for bom in bom_list: frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) -<<<<<<< HEAD -def create_bom_update_log(boms: Optional[Dict] = None, update_type: str = "Replace BOM") -> "BOMUpdateLog": -======= def create_bom_update_log( boms: Optional[Dict[str, str]] = None, update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", ) -> "BOMUpdateLog": ->>>>>>> 620575a901 (fix: Type Annotations, Redundancy, etc.) """Creates a BOM Update Log that handles the background job.""" boms = boms or {} current_bom = boms.get("current_bom") new_bom = boms.get("new_bom") - return frappe.get_doc({ - "doctype": "BOM Update Log", - "current_bom": current_bom, - "new_bom": new_bom, - "update_type": update_type, - }).submit() ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) + return frappe.get_doc( + { + "doctype": "BOM Update Log", + "current_bom": current_bom, + "new_bom": new_bom, + "update_type": update_type, + } + ).submit() From ec646a1a44d993f5d6b36333e3133bf1850853dc Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 18:36:18 +0530 Subject: [PATCH 804/951] fix: User Literal from `typing_extensions` as its not supported in `typing` in py 3.7 - https://mypy.readthedocs.io/en/stable/literal_types.html --- .../manufacturing/doctype/bom_update_log/bom_update_log.py | 3 ++- .../manufacturing/doctype/bom_update_tool/bom_update_tool.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 139dcbcdd90..c3df96c99b1 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,11 +1,12 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from typing import Dict, List, Literal, Optional +from typing import Dict, List, Optional import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, flt +from typing_extensions import Literal from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index b0e7da12017..4061c5af7c2 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -2,7 +2,9 @@ # For license information, please see license.txt import json -from typing import TYPE_CHECKING, Dict, Literal, Optional, Union +from typing import TYPE_CHECKING, Dict, Optional, Union + +from typing_extensions import Literal if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog From e186d7633711ab562eeb7369b7d65aabaca21ae1 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 19:05:30 +0530 Subject: [PATCH 805/951] fix: Linter --- .../doctype/bom_update_tool/test_bom_update_tool.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 36bcd9dcd09..fae72a0f6f7 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -4,8 +4,8 @@ import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item @@ -22,10 +22,7 @@ class TestBOMUpdateTool(FrappeTestCase): bom_doc.items[1].item_code = "_Test Item" bom_doc.insert() - boms = frappe._dict( - current_bom=current_bom, - new_bom=bom_doc.name - ) + boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name) replace_bom(boms) self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom)) From 95298f04000c0299f35cdee7bce0f5f0d8c59525 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Apr 2022 09:27:40 +0530 Subject: [PATCH 806/951] fix: Use get instead of dot --- erpnext/controllers/taxes_and_totals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8b2a69542a7..2afba91b379 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -309,7 +309,7 @@ class calculate_taxes_and_totals(object): def calculate_shipping_charges(self): # Do not apply shipping rule for POS - if self.doc.is_pos: + if self.doc.get("is_pos"): return if hasattr(self.doc, "shipping_rule") and self.doc.shipping_rule: From 027b7e7de19532f21c5dde5df249d43ebf3f0a53 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 4 Apr 2022 12:32:33 +0530 Subject: [PATCH 807/951] fix: Ignore user perm for party account company (cherry picked from commit 18a3c5d536647a453e0a6fc2c39e32d18dcced7f) --- erpnext/accounts/doctype/party_account/party_account.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/party_account/party_account.json b/erpnext/accounts/doctype/party_account/party_account.json index c9f15a6a470..69330577ab3 100644 --- a/erpnext/accounts/doctype/party_account/party_account.json +++ b/erpnext/accounts/doctype/party_account/party_account.json @@ -3,6 +3,7 @@ "creation": "2014-08-29 16:02:39.740505", "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "company", "account" @@ -11,6 +12,7 @@ { "fieldname": "company", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_list_view": 1, "label": "Company", "options": "Company", @@ -27,7 +29,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-07 18:13:08.833822", + "modified": "2022-04-04 12:31:02.994197", "modified_by": "Administrator", "module": "Accounts", "name": "Party Account", @@ -35,5 +37,6 @@ "permissions": [], "quick_entry": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From ae11562611eee7402cf86dd86b05c1271feaccc8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Apr 2022 10:21:27 +0530 Subject: [PATCH 808/951] test: Ignore parent company account creation (cherry picked from commit dec0c1b5bb06455ac5641e9cdc2846374a018259) --- erpnext/setup/doctype/company/test_records.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json index 89be607d047..19b6ef27ac3 100644 --- a/erpnext/setup/doctype/company/test_records.json +++ b/erpnext/setup/doctype/company/test_records.json @@ -8,7 +8,8 @@ "domain": "Manufacturing", "chart_of_accounts": "Standard", "default_holiday_list": "_Test Holiday List", - "enable_perpetual_inventory": 0 + "enable_perpetual_inventory": 0, + "allow_account_creation_against_child_company": 1 }, { "abbr": "_TC1", From d7af8e1dfe685a188e17bb6316e2eeee077bc6f0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 4 Apr 2022 13:25:35 +0530 Subject: [PATCH 809/951] fix: Issues on loan repayment (cherry picked from commit 194605823e6ab0db8509b08095067edfc768d093) --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 6159275c5d1..6a6eb591629 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -585,9 +585,10 @@ def regenerate_repayment_schedule(loan, cancel=0): balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries ) else: - if not cancel: + repayment_period = loan_doc.repayment_periods - accrued_entries + if not cancel and repayment_period > 0: monthly_repayment_amount = get_monthly_repayment_amount( - balance_amount, loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries + balance_amount, loan_doc.rate_of_interest, repayment_period ) else: monthly_repayment_amount = last_repayment_amount From 14f46a3e2676a6b92abc74383c6c7afab4f21400 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 15:14:37 +0530 Subject: [PATCH 810/951] fix: check null values in is_cancelled patch (#30594) (#30595) (cherry picked from commit bb875fe217d5d5ac61a01e564fb115c2f1989788) Co-authored-by: Ankush Menat --- erpnext/patches/v12_0/update_is_cancelled_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/update_is_cancelled_field.py b/erpnext/patches/v12_0/update_is_cancelled_field.py index b567823b062..398dd700eda 100644 --- a/erpnext/patches/v12_0/update_is_cancelled_field.py +++ b/erpnext/patches/v12_0/update_is_cancelled_field.py @@ -20,7 +20,7 @@ def execute(): """ UPDATE `tab{doctype}` SET is_cancelled = 0 - where is_cancelled in ('', NULL, 'No')""".format( + where is_cancelled in ('', 'No') or is_cancelled is NULL""".format( doctype=doctype ) ) From 3c33a5e6ac01cea96a68e93be31b5271d51793e8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 16:37:15 +0530 Subject: [PATCH 811/951] fix: hide pending qty only if original item is assigned (#30599) (#30601) (cherry picked from commit 8b090a9f7d18aa25ae0568c2e52395812eb32462) Co-authored-by: Ankush Menat --- erpnext/public/js/utils/serial_no_batch_selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index cbecb11949b..ecfc37ee556 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -607,8 +607,8 @@ function check_can_calculate_pending_qty(me) { && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no; const itemChecks = !!item - && !item.allow_alternative_item - && erpnext.stock.bom && erpnext.stock.items + && !item.original_item + && erpnext.stock.bom && erpnext.stock.bom.items && (item.item_code in erpnext.stock.bom.items); return docChecks && itemChecks; } From 52aa3561e32cab9b645dd6287f55bfd4623c6db4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Apr 2022 16:38:25 +0530 Subject: [PATCH 812/951] fix: hide pending qty only if original item is assigned (#30599) (#30600) (cherry picked from commit 8b090a9f7d18aa25ae0568c2e52395812eb32462) Co-authored-by: Ankush Menat --- erpnext/public/js/utils/serial_no_batch_selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index cbecb11949b..ecfc37ee556 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -607,8 +607,8 @@ function check_can_calculate_pending_qty(me) { && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no; const itemChecks = !!item - && !item.allow_alternative_item - && erpnext.stock.bom && erpnext.stock.items + && !item.original_item + && erpnext.stock.bom && erpnext.stock.bom.items && (item.item_code in erpnext.stock.bom.items); return docChecks && itemChecks; } From f89c1b2c0ce5365861b2f67f085e77401a477555 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 6 Apr 2022 18:28:54 +0530 Subject: [PATCH 813/951] fix: dont trigger closed WO check on new Job card --- erpnext/manufacturing/doctype/job_card/job_card.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 768cea33dae..c4541fa68e6 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -28,12 +28,12 @@ frappe.ui.form.on('Job Card', { frappe.flags.resume_job = 0; let has_items = frm.doc.items && frm.doc.items.length; - if (frm.doc.__onload.work_order_stopped) { + if (!frm.is_new() && frm.doc.__onload.work_order_stopped) { frm.disable_save(); return; } - if (!frm.doc.__islocal && has_items && frm.doc.docstatus < 2) { + if (!frm.is_new() && has_items && frm.doc.docstatus < 2) { let to_request = frm.doc.for_quantity > frm.doc.transferred_qty; let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer; From 2f7b192ff90cae7fef23633e4e6d952caf53bd4d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Apr 2022 18:46:00 +0530 Subject: [PATCH 814/951] chore: version bump and change log for v13.25.0 --- erpnext/__init__.py | 2 +- erpnext/change_log/v13/v13_25_0.md | 47 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 erpnext/change_log/v13/v13_25_0.md diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a36eba4583c..fcd35dcedb9 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.24.0' +__version__ = '13.25.0' def get_default_company(user=None): '''Get default company for user''' diff --git a/erpnext/change_log/v13/v13_25_0.md b/erpnext/change_log/v13/v13_25_0.md new file mode 100644 index 00000000000..6c21a839870 --- /dev/null +++ b/erpnext/change_log/v13/v13_25_0.md @@ -0,0 +1,47 @@ +## Version 13.25.0 Release Notes + +### Features & Enhancements + +- feat: minor, pick list item reference on delivery note item table ([#30527](https://github.com/frappe/erpnext/pull/30527)) +- feat: configurable Contract naming ([#30450](https://github.com/frappe/erpnext/pull/30450)) + +### Fixes + +- fix: Account currency validation ([#30486](https://github.com/frappe/erpnext/pull/30486)) +- fix(india): minor e-invoicing fixes ([#30553](https://github.com/frappe/erpnext/pull/30553)) +- feat: Redisearch with consent (bp) ([#30539](https://github.com/frappe/erpnext/pull/30539)) +- fix: maintain FIFO queue even if outgoing_rate is not found ([#30563](https://github.com/frappe/erpnext/pull/30563)) +- fix(lead): reload address and contact before updating their links ([#29968](https://github.com/frappe/erpnext/pull/29968)) +- fix(lead): reload contact before updating links ([#29966](https://github.com/frappe/erpnext/pull/29966)) +- fix: Add non-existent Item check and cleanup in `validate_for_items` ([#30509](https://github.com/frappe/erpnext/pull/30509)) +- fix: incorrect payable amount for loan closure ([#30191](https://github.com/frappe/erpnext/pull/30191)) +- fix: Do not apply shipping rule for POS transactions ([#30575](https://github.com/frappe/erpnext/pull/30575)) +- perf: index barcode for faster scans ([#30543](https://github.com/frappe/erpnext/pull/30543)) +- fix: don't check for failed repost while freezing ([#30472](https://github.com/frappe/erpnext/pull/30472)) +- fix: if accepted warehouse not selected during rejection then stock ledger not created ([#30564](https://github.com/frappe/erpnext/pull/30564)) +- fix: hide pending qty only if original item is assigned ([#30599](https://github.com/frappe/erpnext/pull/30599)) +- fix(ux): refresh update to zero val checkbox ([#30567](https://github.com/frappe/erpnext/pull/30567)) +- refactor: Add exception handling in background job within BOM Update Tool ([#30146](https://github.com/frappe/erpnext/pull/30146)) +- fix: bom valuation - handle lack of LPP ([#30454](https://github.com/frappe/erpnext/pull/30454)) +- fix: total leaves allocated not validated and recalculated on updates post submission ([#30569](https://github.com/frappe/erpnext/pull/30569)) +- fix: convert dates to datetime before comparing in leave days calculation and fix half day edge case ([#30538](https://github.com/frappe/erpnext/pull/30538)) +- fix: Ignore user perm for party account company ([#30555](https://github.com/frappe/erpnext/pull/30555)) +- fix(asset): do not validate warehouse on asset purchase ([#30461](https://github.com/frappe/erpnext/pull/30461)) +- fix: credit limit validation in delivery note ([#30470](https://github.com/frappe/erpnext/pull/30470)) +- fix: enable row deletion in reference table ([#30453](https://github.com/frappe/erpnext/pull/30453)) +- fix: use `name` for links not `item_code` ([#30462](https://github.com/frappe/erpnext/pull/30462)) +- fix: multiple pos issues (copy #30324) ([#30515](https://github.com/frappe/erpnext/pull/30515)) +- fix: fetch from fields not working in eway bill dialog ([#30579](https://github.com/frappe/erpnext/pull/30579)) +- fix(pos): do not reset search input on item selection ([#30537](https://github.com/frappe/erpnext/pull/30537)) +- fix: explicitly check if additional salary is recurring while fetching components for payroll ([#30489](https://github.com/frappe/erpnext/pull/30489)) +- fix(India): Tax fetching based on tax category ([#30500](https://github.com/frappe/erpnext/pull/30500)) +- fix: submit Work Order when “Make Serial No / Batch from Work Order” is enabled ([#30468](https://github.com/frappe/erpnext/pull/30468)) +- fix: Dont set `idx` while adding WO items to Stock Entry ([#30377](https://github.com/frappe/erpnext/pull/30377)) +- fix(India): Auto tax fetching based on GSTIN ([#30385](https://github.com/frappe/erpnext/pull/30385)) +- fix: validate 0 transfer qty in stock entry (copy #30476) ([#30479](https://github.com/frappe/erpnext/pull/30479)) +- fix: Issues on loan repayment ([#30557](https://github.com/frappe/erpnext/pull/30557)) +- fix: Added validation for single_threshold in Tax With Holding Category ([#30382](https://github.com/frappe/erpnext/pull/30382)) +- fix: Remove trailing slashes "/" from route ([#30531](https://github.com/frappe/erpnext/pull/30531)) +- fix: validate 0 transfer qty in stock entry ([#30476](https://github.com/frappe/erpnext/pull/30476)) +- fix: Taxes getting overriden from mapped to target doc ([#30510](https://github.com/frappe/erpnext/pull/30510)) +- fix: move item tax to item tax template patch ([#30419](https://github.com/frappe/erpnext/pull/30419)) From f94afc1fff895b9e1255ef6b40f0f84cc32d0948 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 6 Apr 2022 22:14:37 +0530 Subject: [PATCH 815/951] fix(pos): reload doc before set value --- .../v13_0/set_return_against_in_pos_invoice_references.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py index 6af9617bcee..fe9eb8b2cc1 100644 --- a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -19,6 +19,8 @@ def execute(): if not open_pos_closing_entries: return + frappe.reload_doc("Accounts", "doctype", "pos_invoice_reference") + POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") POSInvoice = frappe.qb.DocType("POS Invoice") pos_invoice_references = ( From 23a7ab15b5d8c94b8d63cd16f05ee4bc5ca789ad Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 6 Apr 2022 22:31:50 +0530 Subject: [PATCH 816/951] fix(pos): reload doc before set value (#30612) --- .../v13_0/set_return_against_in_pos_invoice_references.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py index 6af9617bcee..fe9eb8b2cc1 100644 --- a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -19,6 +19,8 @@ def execute(): if not open_pos_closing_entries: return + frappe.reload_doc("Accounts", "doctype", "pos_invoice_reference") + POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") POSInvoice = frappe.qb.DocType("POS Invoice") pos_invoice_references = ( From d720d15e3d1a12b31ca8e963591d991f164953e2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 6 Apr 2022 22:32:23 +0530 Subject: [PATCH 817/951] fix(pos): reload doc before set value (#30610) --- .../v13_0/set_return_against_in_pos_invoice_references.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py index 6af9617bcee..fe9eb8b2cc1 100644 --- a/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py +++ b/erpnext/patches/v13_0/set_return_against_in_pos_invoice_references.py @@ -19,6 +19,8 @@ def execute(): if not open_pos_closing_entries: return + frappe.reload_doc("Accounts", "doctype", "pos_invoice_reference") + POSInvoiceReference = frappe.qb.DocType("POS Invoice Reference") POSInvoice = frappe.qb.DocType("POS Invoice") pos_invoice_references = ( From da7a2bb3a6c3a35ae78978f54fb729e470d176c3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Wed, 6 Apr 2022 22:37:47 +0530 Subject: [PATCH 818/951] chore: version bump --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 56beaca4bef..d089ec42ae9 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.25.0' +__version__ = '13.25.1' def get_default_company(user=None): """Get default company for user""" From c6f946e3e89808224392f59589c7e5c077434435 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Mar 2022 16:18:26 +0530 Subject: [PATCH 819/951] feat: additional filters in payment terms status report (cherry picked from commit eaeadbc422293b08eeed32f2e1c6183f63d17c64) --- .../payment_terms_status_for_sales_order.js | 68 ++++++++++++++----- .../payment_terms_status_for_sales_order.py | 49 +++++++++++-- 2 files changed, 94 insertions(+), 23 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 0e36b3fe3d2..019bf45f06b 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -27,28 +27,64 @@ function get_filters() { "default": frappe.datetime.get_today() }, { - "fieldname":"sales_order", - "label": __("Sales Order"), - "fieldtype": "MultiSelectList", + "fieldname":"customer_group", + "label": __("Customer Group"), + "fieldtype": "Link", "width": 100, - "options": "Sales Order", - "get_data": function(txt) { - return frappe.db.get_link_options("Sales Order", txt, this.filters()); - }, - "filters": () => { + "options": "Customer Group", + "get_query": () => { return { - docstatus: 1, - payment_terms_template: ['not in', ['']], - company: frappe.query_report.get_filter_value("company"), - transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]] + filters: { 'is_group': 0 } } - }, - on_change: function(){ - frappe.query_report.refresh(); + } + + }, + { + "fieldname":"customer", + "label": __("Customer"), + "fieldtype": "Link", + "width": 100, + "options": "Customer", + "get_query": () => { + filters = { + 'disabled': 0 + } + if(frappe.query_report.get_filter_value("customer_group") != "") { + filters['customer_group'] = frappe.query_report.get_filter_value("customer_group"); + } + return { 'filters': filters }; + } + }, + { + "fieldname":"item_group", + "label": __("Item Group"), + "fieldtype": "Link", + "width": 100, + "options": "Item Group", + "get_query": () => { + return { + filters: { 'is_group': 0 } + } + } + + }, + { + "fieldname":"item", + "label": __("Item"), + "fieldtype": "Link", + "width": 100, + "options": "Item", + "get_query": () => { + filters = { + 'disabled': 0 + } + if(frappe.query_report.get_filter_value("item_group") != "") { + filters['item_group'] = frappe.query_report.get_filter_value("item_group"); + } + return { 'filters': filters }; } } ] - return filters; } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 7f797f67eee..5b9550019fd 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -3,7 +3,7 @@ import frappe from frappe import _, qb, query_builder -from frappe.query_builder import functions +from frappe.query_builder import Criterion, functions def get_columns(): @@ -14,6 +14,12 @@ def get_columns(): "fieldtype": "Link", "options": "Sales Order", }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + }, { "label": _("Posting Date"), "fieldname": "submitted", @@ -79,11 +85,29 @@ def get_conditions(filters): conditions.start_date = filters.period_start_date or frappe.utils.add_months( conditions.end_date, -1 ) - conditions.sales_order = filters.sales_order or [] return conditions +def build_filter_criterions(filters): + filters = frappe._dict(filters) if filters else frappe._dict({}) + qb_criterions = [] + + if filters.customer_group: + qb_criterions.append(qb.DocType("Customer").customer_group == filters.customer_group) + + if filters.customer: + qb_criterions.append(qb.DocType("Customer").name == filters.customer) + + if filters.item_group: + qb_criterions.append(qb.DocType("Item").item_group == filters.item_group) + + if filters.item: + qb_criterions.append(qb.DocType("Item").name == filters.item) + + return qb_criterions + + def get_so_with_invoices(filters): """ Get Sales Order with payment terms template with their associated Invoices @@ -92,16 +116,29 @@ def get_so_with_invoices(filters): so = qb.DocType("Sales Order") ps = qb.DocType("Payment Schedule") + cust = qb.DocType("Customer") + item = qb.DocType("Item") + soi = qb.DocType("Sales Order Item") + + conditions = get_conditions(filters) + filter_criterions = build_filter_criterions(filters) + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) - conditions = get_conditions(filters) query_so = ( - qb.from_(so) + qb.from_(cust) + .join(so) + .on(so.customer == cust.name) + .join(soi) + .on(soi.parent == so.name) + .join(item) + .on(item.item_code == soi.item_code) .join(ps) .on(ps.parent == so.name) .select( so.name, + so.customer, so.transaction_date.as_("submitted"), ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), ps.payment_term, @@ -117,12 +154,10 @@ def get_so_with_invoices(filters): & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) ) + .where(Criterion.all(filter_criterions)) .orderby(so.name, so.transaction_date, ps.due_date) ) - if conditions.sales_order != []: - query_so = query_so.where(so.name.isin(conditions.sales_order)) - sorders = query_so.run(as_dict=True) invoices = [] From 9336a3413f7b00c9f4a6c136d543e8334a1bd3f8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Mar 2022 18:49:40 +0530 Subject: [PATCH 820/951] refactor: adding new filters and column to test cases (cherry picked from commit 7558f1b07879da066676f503c0ecb11c51b8e0bc) --- .../test_payment_terms_status_for_sales_order.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 89940a6e872..0b46e949b42 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -48,9 +48,9 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): template.insert() self.template = template - def test_payment_terms_status(self): + def test_01_payment_terms_status(self): self.create_payment_terms_template() - item = create_item(item_code="_Test Excavator", is_stock_item=0) + item = create_item(item_code="_Test Excavator 1", is_stock_item=0) so = make_sales_order( transaction_date="2021-06-15", delivery_date=add_days("2021-06-15", -30), @@ -78,13 +78,14 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "company": "_Test Company", "period_start_date": "2021-06-01", "period_end_date": "2021-06-30", - "sales_order": [so.name], + "item": item.item_code, } ) expected_value = [ { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Completed", "payment_term": None, @@ -98,6 +99,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): }, { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Partly Paid", "payment_term": None, @@ -132,11 +134,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): ) doc.insert() - def test_alternate_currency(self): + def test_02_alternate_currency(self): transaction_date = "2021-06-15" self.create_payment_terms_template() self.create_exchange_rate(transaction_date) - item = create_item(item_code="_Test Excavator", is_stock_item=0) + item = create_item(item_code="_Test Excavator 2", is_stock_item=0) so = make_sales_order( transaction_date=transaction_date, currency="USD", @@ -166,7 +168,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "company": "_Test Company", "period_start_date": "2021-06-01", "period_end_date": "2021-06-30", - "sales_order": [so.name], + "item": item.item_code, } ) @@ -174,6 +176,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): expected_value = [ { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Completed", "payment_term": None, @@ -187,6 +190,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): }, { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Partly Paid", "payment_term": None, From 00acb000dcefc55acaafaf6a8b151b43879a1066 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Apr 2022 14:52:19 +0530 Subject: [PATCH 821/951] refactor: item filters are linked with group filters (cherry picked from commit e324d668d3b33eb6eaee955295ffceb632e50860) --- .../payment_terms_status_for_sales_order.js | 37 ++++++-------- .../payment_terms_status_for_sales_order.py | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 019bf45f06b..c068ae3b5a4 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -32,12 +32,6 @@ function get_filters() { "fieldtype": "Link", "width": 100, "options": "Customer Group", - "get_query": () => { - return { - filters: { 'is_group': 0 } - } - } - }, { "fieldname":"customer", @@ -46,13 +40,14 @@ function get_filters() { "width": 100, "options": "Customer", "get_query": () => { - filters = { - 'disabled': 0 + var customer_group = frappe.query_report.get_filter_value('customer_group'); + return{ + "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + "filters": [ + ['Customer', 'disabled', '=', '0'], + ['Customer Group','name', '=', customer_group] + ] } - if(frappe.query_report.get_filter_value("customer_group") != "") { - filters['customer_group'] = frappe.query_report.get_filter_value("customer_group"); - } - return { 'filters': filters }; } }, { @@ -61,11 +56,6 @@ function get_filters() { "fieldtype": "Link", "width": 100, "options": "Item Group", - "get_query": () => { - return { - filters: { 'is_group': 0 } - } - } }, { @@ -75,13 +65,14 @@ function get_filters() { "width": 100, "options": "Item", "get_query": () => { - filters = { - 'disabled': 0 + var item_group = frappe.query_report.get_filter_value('item_group'); + return{ + "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + "filters": [ + ['Item', 'disabled', '=', '0'], + ['Item Group','name', '=', item_group] + ] } - if(frappe.query_report.get_filter_value("item_group") != "") { - filters['item_group'] = frappe.query_report.get_filter_value("item_group"); - } - return { 'filters': filters }; } } ] diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 5b9550019fd..befbf40e288 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -73,6 +73,55 @@ def get_columns(): return columns +def get_descendants_of(doctype, group_name): + group_doc = qb.DocType(doctype) + # get lft and rgt of group node + lft, rgt = ( + qb.from_(group_doc).select(group_doc.lft, group_doc.rgt).where(group_doc.name == group_name) + ).run()[0] + + # get all children of group node + query = ( + qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt)) + ) + + child_nodes = [] + for x in query.run(): + child_nodes.append(x[0]) + + return child_nodes + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters): + filter_list = [] + if isinstance(filters, list): + for item in filters: + if item[0] == doctype: + filter_list.append(item) + elif item[0] == "Customer Group": + if item[3] != "": + filter_list.append( + [doctype, "customer_group", "in", get_descendants_of("Customer Group", item[3])] + ) + elif item[0] == "Item Group": + if item[3] != "": + filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])]) + + if searchfield and txt: + filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt]) + + return frappe.desk.reportview.execute( + doctype, + filters=filter_list, + fields=["name", "customer_group"] if doctype == "Customer" else ["name", "item_group"], + limit_start=start, + limit_page_length=page_len, + as_list=True, + ) + + def get_conditions(filters): """ Convert filter options to conditions used in query From a9d5000eab4654fcf18261d275066d45e727e195 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Apr 2022 16:21:46 +0530 Subject: [PATCH 822/951] refactor: use group fields from Sales Order and Sales Order Items (cherry picked from commit b2ed9fd3fe62a1f714150f75bc08d0875f3e46c1) --- .../payment_terms_status_for_sales_order.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index befbf40e288..cb22fb6a80f 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -143,16 +143,24 @@ def build_filter_criterions(filters): qb_criterions = [] if filters.customer_group: - qb_criterions.append(qb.DocType("Customer").customer_group == filters.customer_group) + qb_criterions.append( + qb.DocType("Sales Order").customer_group.isin( + get_descendants_of("Customer Group", filters.customer_group) + ) + ) if filters.customer: - qb_criterions.append(qb.DocType("Customer").name == filters.customer) + qb_criterions.append(qb.DocType("Sales Order").customer == filters.customer) if filters.item_group: - qb_criterions.append(qb.DocType("Item").item_group == filters.item_group) + qb_criterions.append( + qb.DocType("Sales Order Item").item_group.isin( + get_descendants_of("Item Group", filters.item_group) + ) + ) if filters.item: - qb_criterions.append(qb.DocType("Item").name == filters.item) + qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item) return qb_criterions @@ -165,8 +173,6 @@ def get_so_with_invoices(filters): so = qb.DocType("Sales Order") ps = qb.DocType("Payment Schedule") - cust = qb.DocType("Customer") - item = qb.DocType("Item") soi = qb.DocType("Sales Order Item") conditions = get_conditions(filters) @@ -176,13 +182,9 @@ def get_so_with_invoices(filters): ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) query_so = ( - qb.from_(cust) - .join(so) - .on(so.customer == cust.name) + qb.from_(so) .join(soi) .on(soi.parent == so.name) - .join(item) - .on(item.item_code == soi.item_code) .join(ps) .on(ps.parent == so.name) .select( From 39f8ee2ea660d8804ed230dad2e3520501f70149 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Apr 2022 10:23:19 +0530 Subject: [PATCH 823/951] test: added test cases for group filters (cherry picked from commit 16bfb930f810e3e44e71192f3676d9636b100a79) --- ...st_payment_terms_status_for_sales_order.py | 136 +++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 0b46e949b42..9d542f5079c 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -11,10 +11,13 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s ) from erpnext.stock.doctype.item.test_item import create_item -test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template", "Customer"] class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): + def tearDown(self): + frappe.db.rollback() + def create_payment_terms_template(self): # create template for 50-50 payments template = None @@ -204,3 +207,134 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): }, ] self.assertEqual(data, expected_value) + + def test_03_group_filters(self): + transaction_date = "2021-06-15" + self.create_payment_terms_template() + item1 = create_item(item_code="_Test Excavator 1", is_stock_item=0) + item1.item_group = "Products" + item1.save() + + so1 = make_sales_order( + transaction_date=transaction_date, + delivery_date=add_days(transaction_date, -30), + item=item1.item_code, + qty=1, + rate=1000000, + do_not_save=True, + ) + so1.po_no = "" + so1.taxes_and_charges = "" + so1.taxes = "" + so1.payment_terms_template = self.template.name + so1.save() + so1.submit() + + item2 = create_item(item_code="_Test Steel", is_stock_item=0) + item2.item_group = "Raw Material" + item2.save() + + so2 = make_sales_order( + customer="_Test Customer 1", + transaction_date=transaction_date, + delivery_date=add_days(transaction_date, -30), + item=item2.item_code, + qty=100, + rate=1000, + do_not_save=True, + ) + so2.po_no = "" + so2.taxes_and_charges = "" + so2.taxes = "" + so2.payment_terms_template = self.template.name + so2.save() + so2.submit() + + base_filters = { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + } + + expected_value_so1 = [ + { + "name": so1.name, + "customer": so1.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 500000.0, + "paid_amount": 0.0, + "invoices": "", + }, + { + "name": so1.name, + "customer": so1.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 500000.0, + "paid_amount": 0.0, + "invoices": "", + }, + ] + + expected_value_so2 = [ + { + "name": so2.name, + "customer": so2.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 50000.0, + "paid_amount": 0.0, + "invoices": "", + }, + { + "name": so2.name, + "customer": so2.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 50000.0, + "paid_amount": 0.0, + "invoices": "", + }, + ] + + group_filters = [ + {"customer_group": "All Customer Groups"}, + {"item_group": "All Item Groups"}, + {"item_group": "Products"}, + {"item_group": "Raw Material"}, + ] + + expected_values_for_group_filters = [ + expected_value_so1 + expected_value_so2, + expected_value_so1 + expected_value_so2, + expected_value_so1, + expected_value_so2, + ] + + for idx, g in enumerate(group_filters, 0): + # build filter + filters = frappe._dict({}).update(base_filters).update(g) + with self.subTest(filters=filters): + columns, data, message, chart = execute(filters) + self.assertEqual(data, expected_values_for_group_filters[idx]) From af039be03e68d8255c362931e23f35677c1f5dd6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 13:20:36 +0530 Subject: [PATCH 824/951] fix: fallback to item_name if description is not found (backport #30619) (#30622) * fix: strip html tags before checking for empty description (#30619) (cherry picked from commit e4c6d6a1a6b9d9ed9aa01d9f3290546bfa76e4bb) # Conflicts: # erpnext/stock/doctype/item/test_item.py * fix: resolve conflicts Co-authored-by: Ankush Menat --- erpnext/stock/doctype/item/item.py | 7 ++----- erpnext/stock/doctype/item/test_item.py | 7 +++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 4ee429191ba..65930a404b4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -18,6 +18,7 @@ from frappe.utils import ( now_datetime, nowtime, strip, + strip_html, ) from frappe.utils.html_utils import clean_html @@ -69,10 +70,6 @@ class Item(Document): self.item_code = strip(self.item_code) self.name = self.item_code - def before_insert(self): - if not self.description: - self.description = self.item_name - def after_insert(self): """set opening stock and item price""" if self.standard_rate: @@ -86,7 +83,7 @@ class Item(Document): if not self.item_name: self.item_name = self.item_code - if not self.description: + if not strip_html(cstr(self.description)).strip(): self.description = self.item_name self.validate_uom() diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 7ef24020ef6..29d6bf2df8b 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -683,6 +683,13 @@ class TestItem(FrappeTestCase): self.assertEqual(item.sample_quantity, None) item.delete() + def test_empty_description(self): + item = make_item(properties={"description": "

    "}) + self.assertEqual(item.description, item.item_name) + item.description = "" + item.save() + self.assertEqual(item.description, item.item_name) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") From 0c163eef23a41de49ebcd8140427bf9be67c472a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 7 Apr 2022 10:06:08 +0530 Subject: [PATCH 825/951] fix: enable Track Changes in Leave Allocation (cherry picked from commit f8f1c3d8b57d95f799696de436f8a0d0c1b0e714) # Conflicts: # erpnext/hr/doctype/leave_allocation/leave_allocation.json --- .../doctype/leave_allocation/leave_allocation.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 52ee463db02..5e74b1894c9 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -237,7 +237,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2021-10-01 15:28:26.335104", +======= + "modified": "2022-04-07 09:50:33.145825", +>>>>>>> f8f1c3d8b5 (fix: enable Track Changes in Leave Allocation) "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -278,5 +282,13 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", +<<<<<<< HEAD "timeline_field": "employee" } +======= + "states": [], + "timeline_field": "employee", + "title_field": "employee_name", + "track_changes": 1 +} +>>>>>>> f8f1c3d8b5 (fix: enable Track Changes in Leave Allocation) From c68a38eb5bbc6095411a30793205497a7a147a7b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 7 Apr 2022 10:06:51 +0530 Subject: [PATCH 826/951] fix: make New Leaves Allocated read-only if policy assignment is linked to the allocation and leave type is earned leave (cherry picked from commit 6203ffc8fab9a6061b991fa689c73391d1671cdf) --- erpnext/hr/doctype/leave_allocation/leave_allocation.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index 9742387c16a..aef44122513 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -34,6 +34,15 @@ frappe.ui.form.on("Leave Allocation", { }); } } + + // make new leaves allocated field read only if allocation is created via leave policy assignment + // and leave type is earned leave, since these leaves would be allocated via the scheduler + if (frm.doc.leave_policy_assignment) { + frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => { + if (r && cint(r.is_earned_leave)) + frm.set_df_property("new_leaves_allocated", "read_only", 1); + }); + } }, expire_allocation: function(frm) { From 8393d113d41ed769dd73f41ed4d2967ffe3b6b03 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 7 Apr 2022 10:07:39 +0530 Subject: [PATCH 827/951] fix: show allocation history for earned leaves allocated via scheduler (cherry picked from commit ec65af5f38bc860701fc8a15f67212cbb4b357d2) --- erpnext/hr/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 2524872ea2b..f1c1608cdd0 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -489,6 +489,17 @@ def update_previous_leave_allocation( allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + if e_leave_type.based_on_date_of_joining: + text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format( + frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) + ) + else: + text = _("allocated {0} leave(s) via scheduler on {1}").format( + frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) + ) + + allocation.add_comment(comment_type="Info", text=text) + def get_monthly_earned_leave(annual_leaves, frequency, rounding): earned_leaves = 0.0 From 07ff2cb1d3e90ad96ac4d58b889acb49e6a524dd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 7 Apr 2022 13:01:02 +0530 Subject: [PATCH 828/951] fix: conflicts --- .../hr/doctype/leave_allocation/leave_allocation.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 5e74b1894c9..3d5c1037f1f 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -237,11 +237,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-10-01 15:28:26.335104", -======= "modified": "2022-04-07 09:50:33.145825", ->>>>>>> f8f1c3d8b5 (fix: enable Track Changes in Leave Allocation) "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -282,13 +278,8 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", -<<<<<<< HEAD - "timeline_field": "employee" -} -======= "states": [], "timeline_field": "employee", "title_field": "employee_name", "track_changes": 1 } ->>>>>>> f8f1c3d8b5 (fix: enable Track Changes in Leave Allocation) From 8932d7040fa6e45207364ee4863ee47241907dda Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 7 Apr 2022 13:01:39 +0530 Subject: [PATCH 829/951] fix: conflicts --- erpnext/hr/doctype/leave_allocation/leave_allocation.json | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 3d5c1037f1f..4a9b54034af 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -278,7 +278,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "states": [], "timeline_field": "employee", "title_field": "employee_name", "track_changes": 1 From 4b77a4de2869ed606ab45f1d26656c6b372b8db3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 13:24:48 +0530 Subject: [PATCH 830/951] fix: update translation (backport #30474) (#30593) * fix: update translation (#30474) * fix: update translation * fix: update translation * fix: update translation * fix: update translation * fix: update translation * fix: update translation * fix: update translation * fix: update translation * fix: update translation * fix: update translation (cherry picked from commit 4895761d89df932e725f0923f0af900ada1486a8) * chore: fix translation csv file Co-authored-by: HENRY Florian Co-authored-by: Ankush Menat --- erpnext/translations/fr.csv | 49 +++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 3cdae454ab5..4746e4f3d8b 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -285,7 +285,7 @@ Asset scrapped via Journal Entry {0},Actif mis au rebut via Écriture de Journal "Asset {0} cannot be scrapped, as it is already {1}","L'actif {0} ne peut pas être mis au rebut, car il est déjà {1}", Asset {0} does not belong to company {1},L'actif {0} ne fait pas partie à la société {1}, Asset {0} must be submitted,L'actif {0} doit être soumis, -Assets,Les atouts, +Assets,Actifs - Immo., Assign,Assigner, Assign Salary Structure,Affecter la structure salariale, Assign To,Attribuer À, @@ -1211,7 +1211,7 @@ Hello,Bonjour, Help Results for,Aide Résultats pour, High,Haut, High Sensitivity,Haute sensibilité, -Hold,Tenir, +Hold,Mettre en attente, Hold Invoice,Facture en attente, Holiday,Vacances, Holiday List,Liste de vacances, @@ -4240,7 +4240,7 @@ For Default Supplier (Optional),Pour le fournisseur par défaut (facultatif), From date cannot be greater than To date,La Date Initiale ne peut pas être postérieure à la Date Finale, Group by,Grouper Par, In stock,En stock, -Item name,Nom de l'article, +Item name,Libellé de l'article, Loan amount is mandatory,Le montant du prêt est obligatoire, Minimum Qty,Quantité minimum, More details,Plus de détails, @@ -5473,7 +5473,7 @@ Percentage you are allowed to transfer more against the quantity ordered. For ex PUR-ORD-.YYYY.-,PUR-ORD-.YYYY.-, Get Items from Open Material Requests,Obtenir des Articles de Demandes Matérielles Ouvertes, Fetch items based on Default Supplier.,Récupérez les articles en fonction du fournisseur par défaut., -Required By,Requis Par, +Required By,Requis pour le, Order Confirmation No,No de confirmation de commande, Order Confirmation Date,Date de confirmation de la commande, Customer Mobile No,N° de Portable du Client, @@ -7223,8 +7223,8 @@ Basic Rate (Company Currency),Taux de Base (Devise de la Société ), Scrap %,% de Rebut, Original Item,Article original, BOM Operation,Opération LDM, -Operation Time ,Moment de l'opération, -In minutes,En quelques minutes, +Operation Time ,Durée de l'opération, +In minutes,En minutes, Batch Size,Taille du lot, Base Hour Rate(Company Currency),Taux Horaire de Base (Devise de la Société), Operating Cost(Company Currency),Coût d'Exploitation (Devise Société), @@ -9267,7 +9267,7 @@ Sales Order Analysis,Analyse des commandes clients, Amount Delivered,Montant livré, Delay (in Days),Retard (en jours), Group by Sales Order,Regrouper par commande client, - Sales Value,La valeur des ventes, +Sales Value,La valeur des ventes, Stock Qty vs Serial No Count,Quantité de stock vs numéro de série, Serial No Count,Numéro de série, Work Order Summary,Résumé de l'ordre de travail, @@ -9647,7 +9647,7 @@ Allow Multiple Sales Orders Against a Customer's Purchase Order,Autoriser plusie Validate Selling Price for Item Against Purchase Rate or Valuation Rate,Valider le prix de vente de l'article par rapport au taux d'achat ou au taux de valorisation, Hide Customer's Tax ID from Sales Transactions,Masquer le numéro d'identification fiscale du client dans les transactions de vente, "The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.","Le pourcentage que vous êtes autorisé à recevoir ou à livrer plus par rapport à la quantité commandée. Par exemple, si vous avez commandé 100 unités et que votre allocation est de 10%, vous êtes autorisé à recevoir 110 unités.", -Action If Quality Inspection Is Not Submitted,Action si l'inspection de la qualité n'est pas soumise, +Action If Quality Inspection Is Not Submitted,Action si l'inspection qualité n'est pas soumise, Auto Insert Price List Rate If Missing,Taux de liste de prix d'insertion automatique s'il est manquant, Automatically Set Serial Nos Based on FIFO,Définir automatiquement les numéros de série en fonction de FIFO, Set Qty in Transactions Based on Serial No Input,Définir la quantité dans les transactions en fonction du numéro de série, @@ -9838,3 +9838,36 @@ Enable European Access,Activer l'accès européen, Creating Purchase Order ...,Création d'une commande d'achat ..., "Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Sélectionnez un fournisseur parmi les fournisseurs par défaut des articles ci-dessous. Lors de la sélection, un bon de commande sera effectué contre des articles appartenant uniquement au fournisseur sélectionné.", Row #{}: You must select {} serial numbers for item {}.,Ligne n ° {}: vous devez sélectionner {} numéros de série pour l'article {}., +Update Rate as per Last Purchase,Mettre à jour avec les derniers prix d'achats +Company Shipping Address,Adresse d'expédition +Shipping Address Details,Détail d'adresse d'expédition +Company Billing Address,Adresse de la société de facturation +Supplier Address Details, +Bank Reconciliation Tool,Outil de réconcialiation d'écritures bancaires +Supplier Contact,Contact fournisseur +Subcontracting,Sous traitance +Order Status,Statut de la commande +Build,Personnalisations avancées +Dispatch Address Name,Adresse de livraison intermédiaire +Amount Eligible for Commission,Montant éligible à comission +Grant Commission,Eligible aux commissions +Stock Transactions Settings, Paramétre des transactions +Role Allowed to Over Deliver/Receive, Rôle autorisé à dépasser cette limite +Users with this role are allowed to over deliver/receive against orders above the allowance percentage,Rôle Utilisateur qui sont autorisé à livrée/commandé au-delà de la limite +Over Transfer Allowance,Autorisation de limite de transfert +"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez receptionné en plus de la quantité commandée" +Quality Inspection Settings,Paramétre de l'inspection qualité +Action If Quality Inspection Is Rejected,Action si l'inspection qualité est rejetée +Disable Serial No And Batch Selector,Désactiver le sélecteur de numéro de lot/série +Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de débit +Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture +Control Historical Stock Transactions,Controle de l'historique des stransaction de stock +No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date. +Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées +Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée +"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","LEs utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire" +Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent +Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix +Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock +Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions +Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries From 30a86951c3632471e670f08139897d1efbeff65b Mon Sep 17 00:00:00 2001 From: Chillar Anand Date: Thu, 7 Apr 2022 14:58:38 +0530 Subject: [PATCH 831/951] test: Fix failing tests for healthcare service unit (#30625) --- .../healthcare_service_unit/healthcare_service_unit.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json index 8935ec7d3c9..9366b09d27e 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json +++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit.json @@ -2,7 +2,6 @@ "actions": [], "allow_import": 1, "allow_rename": 1, - "autoname": "field:healthcare_service_unit_name", "beta": 1, "creation": "2016-09-21 13:48:14.731437", "description": "Healthcare Service Unit", @@ -207,7 +206,7 @@ ], "is_tree": 1, "links": [], - "modified": "2021-08-19 14:09:11.643464", + "modified": "2022-04-07 03:11:36.023277", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Service Unit", From 560c55935f3cb697a10d6e4057cc4108c523f2c3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 15:37:33 +0530 Subject: [PATCH 832/951] fix: warehouse naming when suffix is present (#30621) (#30629) (cherry picked from commit be04eaf723804e72226462472884c38b0c0d26ff) Co-authored-by: Ankush Menat --- erpnext/stock/doctype/warehouse/test_warehouse.py | 10 ++++++++++ erpnext/stock/doctype/warehouse/warehouse.py | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 1e9d01aa4b6..5a7228a5068 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -38,6 +38,16 @@ class TestWarehouse(FrappeTestCase): self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(child_warehouse.is_group, 0) + def test_naming(self): + company = "Wind Power LLC" + warehouse_name = "Named Warehouse - WP" + wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert() + self.assertEqual(wh.name, warehouse_name) + + warehouse_name = "Unnamed Warehouse" + wh = frappe.get_doc(doctype="Warehouse", warehouse_name=warehouse_name, company=company).insert() + self.assertIn(warehouse_name, wh.name) + def test_unlinking_warehouse_from_item_defaults(self): company = "_Test Company" diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index c892ba3ddce..3b18a9ac26f 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -21,8 +21,9 @@ class Warehouse(NestedSet): suffix = " - " + frappe.get_cached_value("Company", self.company, "abbr") if not self.warehouse_name.endswith(suffix): self.name = self.warehouse_name + suffix - else: - self.name = self.warehouse_name + return + + self.name = self.warehouse_name def onload(self): """load account name for General Ledger Report""" From 709fcd50510520bc2a956131cb1fa85fdd8dca76 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 7 Apr 2022 11:04:51 +0530 Subject: [PATCH 833/951] feat: Receivable/Payable Account column and filter in AR/AP report --- .../accounts_payable/accounts_payable.js | 16 +++++++++ .../accounts_receivable.js | 16 +++++++++ .../accounts_receivable.py | 36 ++++++++++++------- .../test_accounts_receivable.py | 11 ++++-- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 81c60bb337d..f6961eb95fa 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -53,6 +53,22 @@ frappe.query_reports["Accounts Payable"] = { } } }, + { + "fieldname": "party_account", + "label": __("Payable Account"), + "fieldtype": "Link", + "options": "Account", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + 'account_type': 'Payable', + 'is_group': 0 + } + }; + } + }, { "fieldname": "ageing_based_on", "label": __("Ageing Based On"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 570029851e8..748bcde4354 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -66,6 +66,22 @@ frappe.query_reports["Accounts Receivable"] = { } } }, + { + "fieldname": "party_account", + "label": __("Receivable Account"), + "fieldtype": "Link", + "options": "Account", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + 'account_type': 'Receivable', + 'is_group': 0 + } + }; + } + }, { "fieldname": "ageing_based_on", "label": __("Ageing Based On"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 7bf9539b751..c9567f23a34 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -111,6 +111,7 @@ class ReceivablePayableReport(object): voucher_type=gle.voucher_type, voucher_no=gle.voucher_no, party=gle.party, + party_account=gle.account, posting_date=gle.posting_date, account_currency=gle.account_currency, remarks=gle.remarks if self.filters.get("show_remarks") else None, @@ -777,18 +778,21 @@ class ReceivablePayableReport(object): conditions.append("party=%s") values.append(self.filters.get(party_type_field)) - # get GL with "receivable" or "payable" account_type - account_type = "Receivable" if self.party_type == "Customer" else "Payable" - accounts = [ - d.name - for d in frappe.get_all( - "Account", filters={"account_type": account_type, "company": self.filters.company} - ) - ] - - if accounts: - conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts))) - values += accounts + if self.filters.party_account: + conditions.append("account =%s") + values.append(self.filters.party_account) + else: + # get GL with "receivable" or "payable" account_type + account_type = "Receivable" if self.party_type == "Customer" else "Payable" + accounts = [ + d.name + for d in frappe.get_all( + "Account", filters={"account_type": account_type, "company": self.filters.company} + ) + ] + if accounts: + conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts))) + values += accounts def add_customer_filters(self, conditions, values): if self.filters.get("customer_group"): @@ -889,6 +893,14 @@ class ReceivablePayableReport(object): width=180, ) + self.add_column( + label="Receivable Account" if self.party_type == "Customer" else "Payable Account", + fieldname="party_account", + fieldtype="Link", + options="Account", + width=180, + ) + if self.party_naming_by == "Naming Series": self.add_column( _("{0} Name").format(self.party_type), diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 7a6989f9e54..f38890e980c 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -50,12 +50,19 @@ class TestAccountsReceivable(unittest.TestCase): make_credit_note(name) report = execute(filters) - expected_data_after_credit_note = [100, 0, 0, 40, -40] + expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"] row = report[1][0] self.assertEqual( expected_data_after_credit_note, - [row.invoice_grand_total, row.invoiced, row.paid, row.credit_note, row.outstanding], + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.party_account, + ], ) From be438c08db74878be3da7c50ae6918eca43ddcfd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Apr 2022 20:51:51 +0530 Subject: [PATCH 834/951] fix: Exchange gain and loss button in Payment Entry (cherry picked from commit 8feb4f08c5c96f0ff5575e13931b07e2f67a25e4) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 345764fb418..3a89ce8cd12 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -226,10 +226,7 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.total_allocated_amount > party_amount))); frm.toggle_display("set_exchange_gain_loss", - (frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount && - ((frm.doc.paid_from_account_currency != company_currency || - frm.doc.paid_to_account_currency != company_currency) && - frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency))); + frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount); frm.refresh_fields(); }, From 4a7ae5c4a776245774e160ed1355d2cc2be47661 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 7 Apr 2022 13:30:24 +0000 Subject: [PATCH 835/951] fix: dont reassign mutable (list) to a different field (backport #30634) (#30635) This is an automatic backport of pull request #30634 done by [Mergify](https://mergify.com). Conflicts fixed. --- .../purchase_register/purchase_register.py | 18 +++++++++--------- erpnext/selling/doctype/customer/customer.py | 3 ++- .../selling/doctype/customer/test_customer.py | 3 ++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index c3599593103..a73c72c6d82 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -124,11 +124,10 @@ def get_columns(invoice_list, additional_table_columns): _("Purchase Receipt") + ":Link/Purchase Receipt:100", {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, ] - expense_accounts = ( - tax_accounts - ) = ( - expense_columns - ) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = [] + + expense_accounts = [] + tax_accounts = [] + unrealized_profit_loss_accounts = [] if invoice_list: expense_accounts = frappe.db.sql_list( @@ -163,10 +162,11 @@ def get_columns(invoice_list, additional_table_columns): unrealized_profit_loss_account_columns = [ (account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts ] - - for account in tax_accounts: - if account not in expense_accounts: - tax_columns.append(account + ":Currency/currency:120") + tax_columns = [ + (account + ":Currency/currency:120") + for account in tax_accounts + if account not in expense_accounts + ] columns = ( columns diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 2e5cbb80cb6..8889a5f939a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -100,7 +100,8 @@ class Customer(TransactionBase): @frappe.whitelist() def get_customer_group_details(self): doc = frappe.get_doc("Customer Group", self.customer_group) - self.accounts = self.credit_limits = [] + self.accounts = [] + self.credit_limits = [] self.payment_terms = self.default_price_list = "" tables = [["accounts", "account"], ["credit_limits", "credit_limit"]] diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 4979b8f976a..f631a6ef568 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -47,7 +47,8 @@ class TestCustomer(FrappeTestCase): c_doc.customer_name = "Testing Customer" c_doc.customer_group = "_Testing Customer Group" c_doc.payment_terms = c_doc.default_price_list = "" - c_doc.accounts = c_doc.credit_limits = [] + c_doc.accounts = [] + c_doc.credit_limits = [] c_doc.insert() c_doc.get_customer_group_details() self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") From 341e8dffd3382e56c6ee38c343c93849aff0998d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Apr 2022 11:27:17 +0530 Subject: [PATCH 836/951] fix: remove bad defaults from BOM operation (#30644) (#30645) [skip ci] (cherry picked from commit 49560d20bc98470f46e24a1566122df19eba1161) Co-authored-by: Ankush Menat --- erpnext/manufacturing/doctype/bom_operation/bom_operation.json | 3 +-- erpnext/stock/doctype/item/item.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 9877b2882af..210c0ea6a72 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -100,7 +100,6 @@ "read_only": 1 }, { - "default": "5", "depends_on": "eval:parent.doctype == 'BOM'", "fieldname": "base_operating_cost", "fieldtype": "Currency", @@ -178,7 +177,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-03-10 06:19:08.462027", + "modified": "2022-04-08 01:18:33.547481", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index d364d8a7d95..e6f1f0a2952 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -659,7 +659,6 @@ }, { "collapsible": 1, - "default": "eval:!doc.is_fixed_asset", "fieldname": "sales_details", "fieldtype": "Section Break", "label": "Sales Details", @@ -1026,4 +1025,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} From 8cac70a63c32a6cfcc229809ca25cc2298cd5add Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 8 Apr 2022 13:20:25 +0530 Subject: [PATCH 837/951] fix: prevent deleting repost queue for cancelled transactions (cherry picked from commit a281998bcb0901fa928cfd68b9da26b1ab507449) --- .../repost_item_valuation.py | 16 ++++++++++++++++ erpnext/stock/stock_ledger.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) 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 b11becd4cee..508f26b5f1d 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -61,6 +61,22 @@ class RepostItemValuation(Document): repost(self) + def before_cancel(self): + self.check_pending_repost_against_cancelled_transaction() + + def check_pending_repost_against_cancelled_transaction(self): + if self.status not in ("Queued", "In Progress"): + return + + if not (self.voucher_no and self.voucher_no): + return + + transaction_status = frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") + if transaction_status == 2: + msg = _("Cannot cancel as processing of cancelled documents is pending.") + msg += "
    " + _("Please try again in an hour.") + frappe.throw(msg, title=_("Pending processing")) + @frappe.whitelist() def restart_reposting(self): self.set_status("Queued", write=False) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b95bcab7149..597e2e28f1b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -175,9 +175,9 @@ def validate_cancellation(args): ) if repost_entry.status == "Queued": doc = frappe.get_doc("Repost Item Valuation", repost_entry.name) + doc.status = "Skipped" doc.flags.ignore_permissions = True doc.cancel() - doc.delete() def set_as_cancel(voucher_type, voucher_no): From 9b6c5f2e54895796d3a9bd163ce591b1c7a74726 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 8 Apr 2022 13:32:20 +0530 Subject: [PATCH 838/951] test: prevent cancelling RIV of cancelled voucher (cherry picked from commit d74181630a34306da4cc12e29434aada4d81f8ea) --- .../test_repost_item_valuation.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) 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 f3bebad5c09..55117ceb2e3 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 @@ -1,20 +1,25 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.controllers.stock_controller import create_item_wise_repost_entries +from erpnext.stock.doctype.item.test_item import make_item 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.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.utils import PendingRepostingError -class TestRepostItemValuation(unittest.TestCase): +class TestRepostItemValuation(FrappeTestCase): + def tearDown(self): + frappe.flags.dont_execute_stock_reposts = False + def test_repost_time_slot(self): repost_settings = frappe.get_doc("Stock Reposting Settings") @@ -162,3 +167,22 @@ class TestRepostItemValuation(unittest.TestCase): self.assertRaises(PendingRepostingError, stock_settings.save) riv.set_status("Skipped") + + def test_prevention_of_cancelled_transaction_riv(self): + frappe.flags.dont_execute_stock_reposts = True + + item = make_item() + warehouse = "_Test Warehouse - _TC" + old = make_stock_entry(item_code=item.name, to_warehouse=warehouse, qty=2, rate=5) + _new = make_stock_entry(item_code=item.name, to_warehouse=warehouse, qty=5, rate=10) + + old.cancel() + + riv = frappe.get_last_doc( + "Repost Item Valuation", {"voucher_type": old.doctype, "voucher_no": old.name} + ) + self.assertRaises(frappe.ValidationError, riv.cancel) + + riv.db_set("status", "Skipped") + riv.reload() + riv.cancel() # it should cancel now From 2ccf58d6adb8c190c24b3fd029725b3e0d81ae66 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 6 Apr 2022 14:44:10 +0530 Subject: [PATCH 839/951] fix: removed unused courses template (cherry picked from commit bce1c2a0284dcdb498b01d1e726522f9b535cc9b) --- erpnext/templates/pages/courses.html | 11 ----------- erpnext/templates/pages/courses.py | 18 ------------------ 2 files changed, 29 deletions(-) delete mode 100644 erpnext/templates/pages/courses.html delete mode 100644 erpnext/templates/pages/courses.py diff --git a/erpnext/templates/pages/courses.html b/erpnext/templates/pages/courses.html deleted file mode 100644 index 6592f7a2e5c..00000000000 --- a/erpnext/templates/pages/courses.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "templates/web.html" %} - -{% block header %} -

    About

    -{% endblock %} - -{% block page_content %} - -

    {{ intro }}

    - -{% endblock %} diff --git a/erpnext/templates/pages/courses.py b/erpnext/templates/pages/courses.py deleted file mode 100644 index fb1af387d22..00000000000 --- a/erpnext/templates/pages/courses.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe - - -def get_context(context): - course = frappe.get_doc("Course", frappe.form_dict.course) - sidebar_title = course.name - - context.no_cache = 1 - context.show_sidebar = True - course = frappe.get_doc("Course", frappe.form_dict.course) - course.has_permission("read") - context.doc = course - context.sidebar_title = sidebar_title - context.intro = course.course_intro From e4bb81d8303d6dda26024245fe6f8b3c10f0e1f2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 9 Apr 2022 16:13:29 +0530 Subject: [PATCH 840/951] fix(pos): cannot change paid amount in pos payments (#30657) --- erpnext/selling/page/point_of_sale/pos_controller.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 6974bed4f1f..65e0cbb7a5c 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -721,11 +721,14 @@ erpnext.PointOfSale.Controller = class { async save_and_checkout() { if (this.frm.is_dirty()) { + let save_error = false; + await this.frm.save(null, null, null, () => save_error = true); // only move to payment section if save is successful - frappe.route_hooks.after_save = () => this.payment.checkout(); - return this.frm.save( - null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error - ); + !save_error && this.payment.checkout(); + // show checkout button on error + save_error && setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish } else { this.payment.checkout(); } From 81812b608f13a9d9b27b32118661f09be9970e77 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 9 Apr 2022 16:14:26 +0530 Subject: [PATCH 841/951] fix(pos): cannot change paid amount in pos payments (#30659) --- erpnext/selling/page/point_of_sale/pos_controller.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 6974bed4f1f..65e0cbb7a5c 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -721,11 +721,14 @@ erpnext.PointOfSale.Controller = class { async save_and_checkout() { if (this.frm.is_dirty()) { + let save_error = false; + await this.frm.save(null, null, null, () => save_error = true); // only move to payment section if save is successful - frappe.route_hooks.after_save = () => this.payment.checkout(); - return this.frm.save( - null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error - ); + !save_error && this.payment.checkout(); + // show checkout button on error + save_error && setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish } else { this.payment.checkout(); } From 71f72458bfb3371b30ca240efa10e3b4602c6f62 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 9 Apr 2022 16:22:22 +0530 Subject: [PATCH 842/951] chore: version bump --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index d089ec42ae9..b19f1cecaaa 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.25.1' +__version__ = '13.25.2' def get_default_company(user=None): """Get default company for user""" From e69849d3334c5a2451c7698e2eb87bc2c74a135b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 9 Apr 2022 16:28:25 +0530 Subject: [PATCH 843/951] fix(pos): cannot change paid amount in pos payments (#30661) --- erpnext/selling/page/point_of_sale/pos_controller.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 6974bed4f1f..65e0cbb7a5c 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -721,11 +721,14 @@ erpnext.PointOfSale.Controller = class { async save_and_checkout() { if (this.frm.is_dirty()) { + let save_error = false; + await this.frm.save(null, null, null, () => save_error = true); // only move to payment section if save is successful - frappe.route_hooks.after_save = () => this.payment.checkout(); - return this.frm.save( - null, null, null, () => this.cart.toggle_checkout_btn(true) // show checkout button on error - ); + !save_error && this.payment.checkout(); + // show checkout button on error + save_error && setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish } else { this.payment.checkout(); } From 1b25a7fe765d334fce990d43224f8a790ccf8119 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 9 Apr 2022 19:20:51 +0530 Subject: [PATCH 844/951] fix: Implicit ignore pricing rule check on returns --- erpnext/controllers/sales_and_purchase_return.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index bc2122824f3..0ad39949b6d 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -330,7 +330,6 @@ def make_return_doc(doctype, source_name, target_doc=None): doc = frappe.get_doc(target) doc.is_return = 1 doc.return_against = source.name - doc.ignore_pricing_rule = 1 doc.set_warehouse = "" if doctype == "Sales Invoice" or doctype == "POS Invoice": doc.is_pos = source.is_pos From 37dc11e59a16f1ea0d037ac8019aa6409493d5c7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 1 Apr 2022 14:46:26 +0530 Subject: [PATCH 845/951] fix: Ignore disabled tax categories (cherry picked from commit 9a6a181f145f345a9973afc8673590b59aec1617) --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 4eeb83779b3..6bbaa358cde 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -341,7 +341,7 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code): tax_categories = frappe.get_all( "Tax Category", fields=["name", "is_inter_state", "gst_state"], - filters={"is_inter_state": is_inter_state, "is_reverse_charge": 0}, + filters={"is_inter_state": is_inter_state, "is_reverse_charge": 0, "disabled": 0}, ) default_tax = "" From e91dea62b8d98e213428f579c8a64b6602e5f53b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 14:48:17 +0530 Subject: [PATCH 846/951] fix: update translation (#30654) (#30676) (cherry picked from commit 03c631d7238a0e058235a3e4057d88451cd97ee6) Co-authored-by: HENRY Florian --- erpnext/translations/fr.csv | 67 +++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 4746e4f3d8b..90456c5c4ee 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -951,14 +951,14 @@ End time cannot be before start time,L'heure de fin ne peut pas être avant l'he Ends On date cannot be before Next Contact Date.,La date de fin ne peut pas être avant la prochaine date de contact, Energy,Énergie, Engineer,Ingénieur, -Enough Parts to Build,Pièces Suffisantes pour Construire, +Enough Parts to Build,Pièces Suffisantes pour Construire Enroll,Inscrire, Enrolling student,Inscrire un étudiant, Enrolling students,Inscription des étudiants, Enter depreciation details,Veuillez entrer les détails de l'amortissement, -Enter the Bank Guarantee Number before submittting.,Entrez le numéro de garantie bancaire avant de soumettre., -Enter the name of the Beneficiary before submittting.,Entrez le nom du bénéficiaire avant de soumettre., -Enter the name of the bank or lending institution before submittting.,Entrez le nom de la banque ou de l'institution de prêt avant de soumettre., +Enter the Bank Guarantee Number before submittting.,Entrez le numéro de garantie bancaire avant de valider. +Enter the name of the Beneficiary before submittting.,Entrez le nom du bénéficiaire avant de valider. +Enter the name of the bank or lending institution before submittting.,Entrez le nom de la banque ou de l'institution de prêt avant de valider., Enter value betweeen {0} and {1},Entrez une valeur entre {0} et {1}, Entertainment & Leisure,Divertissement et Loisir, Entertainment Expenses,Charges de Représentation, @@ -1068,7 +1068,7 @@ For Employee,Employé, For Quantity (Manufactured Qty) is mandatory,Pour Quantité (Qté Produite) est obligatoire, For Supplier,Pour Fournisseur, For Warehouse,Pour l’Entrepôt, -For Warehouse is required before Submit,Pour l’Entrepôt est requis avant de Soumettre, +For Warehouse is required before Submit,Pour l’Entrepôt est requis avant de Valider, "For an item {0}, quantity must be negative number","Pour l'article {0}, la quantité doit être un nombre négatif", "For an item {0}, quantity must be positive number","Pour un article {0}, la quantité doit être un nombre positif", "For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry","Pour la carte de travail {0}, vous pouvez uniquement saisir une entrée de stock de type "Transfert d'article pour fabrication".", @@ -1693,7 +1693,7 @@ No Items with Bill of Materials to Manufacture,Aucun Article avec une Liste de M No Items with Bill of Materials.,Aucun article avec nomenclature., No Permission,Aucune autorisation, No Remarks,Aucune Remarque, -No Result to submit,Aucun résultat à soumettre, +No Result to submit,Aucun résultat à valider, No Salary Structure assigned for Employee {0} on given date {1},Aucune structure de salaire attribuée à l'employé {0} à la date donnée {1}, No Staffing Plans found for this Designation,Aucun plan de dotation trouvé pour cette désignation, No Student Groups created.,Aucun Groupe d'Étudiants créé., @@ -2847,12 +2847,12 @@ Sub Type,Sous type, Sub-contracting,Sous-traitant, Subcontract,Sous-traiter, Subject,Sujet, -Submit,Soumettre, -Submit Proof,Soumettre une preuve, -Submit Salary Slip,Soumettre la Fiche de Paie, -Submit this Work Order for further processing.,Soumettre cet ordre de travail pour continuer son traitement., -Submit this to create the Employee record,Soumettre pour créer la fiche employé, -Submitting Salary Slips...,Soumission des bulletins de salaire ..., +Submit,Valider, +Submit Proof,Valider une preuve, +Submit Salary Slip,Valider la Fiche de Paie, +Submit this Work Order for further processing.,Valider cet ordre de travail pour continuer son traitement., +Submit this to create the Employee record,Valider pour créer la fiche employé, +Submitting Salary Slips...,Validation des bulletins de salaire ..., Subscription,Abonnement, Subscription Management,Gestion des abonnements, Subscriptions,Abonnements, @@ -2954,7 +2954,7 @@ The Term End Date cannot be earlier than the Term Start Date. Please correct the The Term End Date cannot be later than the Year End Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.,La Date de Fin de Terme ne peut pas être postérieure à la Date de Fin de l'Année Académique à laquelle le terme est lié (Année Académique {}). Veuillez corriger les dates et essayer à nouveau., The Term Start Date cannot be earlier than the Year Start Date of the Academic Year to which the term is linked (Academic Year {}). Please correct the dates and try again.,La Date de Début de Terme ne peut pas être antérieure à la Date de Début de l'Année Académique à laquelle le terme est lié (Année Académique {}). Veuillez corriger les dates et essayer à nouveau., The Year End Date cannot be earlier than the Year Start Date. Please correct the dates and try again.,La Date de Fin d'Année ne peut pas être antérieure à la Date de Début d’Année. Veuillez corriger les dates et essayer à nouveau., -The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.,Le montant {0} défini dans cette requête de paiement est différent du montant calculé de tous les plans de paiement: {1}.\nVeuillez vérifier que c'est correct avant de soumettre le document., +The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.,Le montant {0} défini dans cette requête de paiement est différent du montant calculé de tous les plans de paiement: {1}.\nVeuillez vérifier que c'est correct avant de valider le document., The day(s) on which you are applying for leave are holidays. You need not apply for leave.,Le(s) jour(s) pour le(s)quel(s) vous demandez un congé sont des jour(s) férié(s). Vous n’avez pas besoin d’effectuer de demande., The field From Shareholder cannot be blank,Le champ 'De l'actionnaire' ne peut pas être vide, The field To Shareholder cannot be blank,Le champ 'A l'actionnaire' ne peut pas être vide, @@ -3011,7 +3011,7 @@ This is based on transactions against this Healthcare Practitioner.,Ce graphique This is based on transactions against this Patient. See timeline below for details,Ceci est basé sur les transactions de ce patient. Voir la chronologie ci-dessous pour plus de détails, This is based on transactions against this Sales Person. See timeline below for details,Ceci est basé sur les transactions contre ce vendeur. Voir la chronologie ci-dessous pour plus de détails, This is based on transactions against this Supplier. See timeline below for details,Basé sur les transactions avec ce fournisseur. Voir la chronologie ci-dessous pour plus de détails, -This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?,Cela permettra de soumettre des bulletins de salaire et de créer une écriture de journal d'accumulation. Voulez-vous poursuivre?, +This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?,Cela permettra de valider des bulletins de salaire et de créer une écriture de journal d'accumulation. Voulez-vous poursuivre?, This {0} conflicts with {1} for {2} {3},Ce {0} est en conflit avec {1} pour {2} {3}, Time Sheet for manufacturing.,Feuille de Temps pour la production., Time Tracking,Suivi du temps, @@ -3312,7 +3312,7 @@ Work Order {0} must be cancelled before cancelling this Sales Order,L'ordre de t Work Order {0} must be submitted,L'ordre de travail {0} doit être soumis, Work Orders Created: {0},Ordres de travail créés: {0}, Work Summary for {0},Résumé de travail de {0}, -Work-in-Progress Warehouse is required before Submit,L'entrepôt des Travaux en Cours est nécessaire avant de Soumettre, +Work-in-Progress Warehouse is required before Submit,L'entrepôt des Travaux en Cours est nécessaire avant de Valider, Workflow,Flux de Travail, Working,Travail en cours, Working Hours,Heures de travail, @@ -3331,7 +3331,7 @@ You can only have Plans with the same billing cycle in a Subscription,Vous ne po You can only redeem max {0} points in this order.,Vous pouvez uniquement échanger un maximum de {0} points dans cet commande., You can only renew if your membership expires within 30 days,Vous ne pouvez renouveler que si votre abonnement expire dans les 30 jours, You can only select a maximum of one option from the list of check boxes.,Vous pouvez sélectionner au maximum une option dans la liste des cases à cocher., -You can only submit Leave Encashment for a valid encashment amount,Vous pouvez uniquement soumettre un encaissement de congé pour un montant d'encaissement valide, +You can only submit Leave Encashment for a valid encashment amount,Vous pouvez uniquement valider un encaissement de congé pour un montant d'encaissement valide, You can't redeem Loyalty Points having more value than the Grand Total.,Vous ne pouvez pas échanger des points de fidélité ayant plus de valeur que le total général., You cannot credit and debit same account at the same time,Vous ne pouvez pas créditer et débiter le même compte simultanément, You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Vous ne pouvez pas supprimer l'exercice fiscal {0}. L'exercice fiscal {0} est défini par défaut dans les Paramètres Globaux, @@ -3684,8 +3684,8 @@ Create Quality Inspection for Item {0},Créer un contrôle qualité pour l'artic Creating Accounts...,Création de comptes ..., Creating bank entries...,Création d'entrées bancaires ..., Credit limit is already defined for the Company {0},La limite de crédit est déjà définie pour la société {0}., -Ctrl + Enter to submit,Ctrl + Entrée pour soumettre, -Ctrl+Enter to submit,Ctrl + Entrée pour soumettre, +Ctrl + Enter to submit,Ctrl + Entrée pour valider, +Ctrl+Enter to submit,Ctrl + Entrée pour valider, Currency,Devise, Current Status,Statut Actuel, Customer PO,Bon de commande client, @@ -3709,7 +3709,7 @@ Dimension Filter,Filtre de dimension, Disabled,Desactivé, Disbursement and Repayment,Décaissement et remboursement, Distance cannot be greater than 4000 kms,La distance ne peut pas dépasser 4000 km, -Do you want to submit the material request,Voulez-vous soumettre la demande de matériel, +Do you want to submit the material request,Voulez-vous valider la demande de matériel, Doctype,Doctype, Document {0} successfully uncleared,Document {0} non effacé avec succès, Download Template,Télécharger le Modèle, @@ -4309,7 +4309,7 @@ Requested,Demandé, Partially Paid,Partiellement payé, Invalid Account Currency,Devise de compte non valide, "Row {0}: The item {1}, quantity must be positive number","Ligne {0}: l'article {1}, la quantité doit être un nombre positif", -"Please set {0} for Batched Item {1}, which is used to set {2} on Submit.","Veuillez définir {0} pour l'article par lots {1}, qui est utilisé pour définir {2} sur Soumettre.", +"Please set {0} for Batched Item {1}, which is used to set {2} on Submit.","Veuillez définir {0} pour l'article par lots {1}, qui est utilisé pour définir {2} sur Valider.", Expiry Date Mandatory,Date d'expiration obligatoire, Variant Item,Élément de variante, BOM 1 {0} and BOM 2 {1} should not be same,La nomenclature 1 {0} et la nomenclature 2 {1} ne doivent pas être identiques, @@ -4589,7 +4589,7 @@ Bank Transaction Entries,Ecritures de transactions bancaires, New Transactions,Nouvelles transactions, Match Transaction to Invoices,Faire correspondre la transaction aux factures, Create New Payment/Journal Entry,Créer un nouveau paiement / écriture de journal, -Submit/Reconcile Payments,Soumettre / rapprocher les paiements, +Submit/Reconcile Payments,Valider / rapprocher les paiements, Matching Invoices,Factures correspondantes, Payment Invoice Items,Articles de la facture de paiement, Reconciled Transactions,Transactions rapprochées, @@ -6208,7 +6208,7 @@ Collect Fee for Patient Registration,Collecter les honoraires pour l'inscription Checking this will create new Patients with a Disabled status by default and will only be enabled after invoicing the Registration Fee.,Cochez cette case pour créer de nouveaux patients avec un statut Désactivé par défaut et ne seront activés qu'après facturation des frais d'inscription., Registration Fee,Frais d'Inscription, Automate Appointment Invoicing,Automatiser la facturation des rendez-vous, -Manage Appointment Invoice submit and cancel automatically for Patient Encounter,Gérer les factures de rendez-vous soumettre et annuler automatiquement pour la consultation des patients, +Manage Appointment Invoice submit and cancel automatically for Patient Encounter,Gérer les factures de rendez-vous valider et annuler automatiquement pour la consultation des patients, Enable Free Follow-ups,Activer les suivis gratuits, Number of Patient Encounters in Valid Days,Nombre de rencontres de patients en jours valides, The number of free follow ups (Patient Encounters in valid days) allowed,Le nombre de suivis gratuits (rencontres de patients en jours valides) autorisés, @@ -8679,7 +8679,7 @@ Book Deferred Entries Based On,Enregistrer les entrées différées en fonction Days,Journées, Months,Mois, Book Deferred Entries Via Journal Entry,Enregistrer les écritures différées via l'écriture au journal, -Submit Journal Entries,Soumettre les entrées de journal, +Submit Journal Entries,Valider les entrées de journal, If this is unchecked Journal Entries will be saved in a Draft state and will have to be submitted manually,"Si cette case n'est pas cochée, les entrées de journal seront enregistrées dans un état Brouillon et devront être soumises manuellement", Enable Distributed Cost Center,Activer le centre de coûts distribués, Distributed Cost Center,Centre de coûts distribués, @@ -9065,7 +9065,7 @@ Rented To Date,Loué à ce jour, Monthly Eligible Amount,Montant mensuel admissible, Total Eligible HRA Exemption,Exemption HRA totale éligible, Validating Employee Attendance...,Validation de la présence des employés ..., -Submitting Salary Slips and creating Journal Entry...,Soumettre des fiches de salaire et créer une écriture au journal ..., +Submitting Salary Slips and creating Journal Entry...,Validation des fiches de salaire et créer une écriture au journal ..., Calculate Payroll Working Days Based On,Calculer les jours ouvrables de paie en fonction de, Consider Unmarked Attendance As,Considérez la participation non marquée comme, Fraction of Daily Salary for Half Day,Fraction du salaire journalier pour une demi-journée, @@ -9166,8 +9166,8 @@ Enter customer's phone number,Entrez le numéro de téléphone du client, Customer contact updated successfully.,Contact client mis à jour avec succès., Item will be removed since no serial / batch no selected.,L'article sera supprimé car aucun numéro de série / lot sélectionné., Discount (%),Remise (%), -You cannot submit the order without payment.,Vous ne pouvez pas soumettre la commande sans paiement., -You cannot submit empty order.,Vous ne pouvez pas soumettre de commande vide., +You cannot submit the order without payment.,Vous ne pouvez pas valider la commande sans paiement., +You cannot submit empty order.,Vous ne pouvez pas valider de commande vide., To Be Paid,Être payé, Create POS Opening Entry,Créer une entrée d'ouverture de PDV, Please add Mode of payments and opening balance details.,Veuillez ajouter le mode de paiement et les détails du solde d'ouverture., @@ -9305,7 +9305,7 @@ Courses updated,Cours mis à jour, {0} {1} has been added to all the selected topics successfully.,{0} {1} a bien été ajouté à tous les sujets sélectionnés., Topics updated,Sujets mis à jour, Academic Term and Program,Terme académique et programme, -Please remove this item and try to submit again or update the posting time.,Veuillez supprimer cet élément et réessayer de le soumettre ou mettre à jour l'heure de publication., +Please remove this item and try to submit again or update the posting time.,Veuillez supprimer cet élément et réessayer de le valider ou mettre à jour l'heure de publication., Failed to Authenticate the API key.,Échec de l'authentification de la clé API., Invalid Credentials,Les informations d'identification invalides, URL can only be a string,L'URL ne peut être qu'une chaîne, @@ -9416,7 +9416,7 @@ Import Italian Supplier Invoice.,Importer la facture du fournisseur italien., "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.",Le taux de valorisation de l'article {0} est requis pour effectuer des écritures comptables pour {1} {2}., Here are the options to proceed:,Voici les options pour continuer:, "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.","Si l'article est traité comme un article à taux de valorisation nul dans cette entrée, veuillez activer "Autoriser le taux de valorisation nul" dans le {0} tableau des articles.", -"If not, you can Cancel / Submit this entry ","Sinon, vous pouvez annuler / soumettre cette entrée", +"If not, you can Cancel / Submit this entry ","Sinon, vous pouvez annuler / valider cette entrée", performing either one below:,effectuer l'un ou l'autre ci-dessous:, Create an incoming stock transaction for the Item.,Créez une transaction de stock entrante pour l'article., Mention Valuation Rate in the Item master.,Mentionnez le taux de valorisation dans la fiche article., @@ -9573,7 +9573,7 @@ Accounting entries are frozen up to this date. Nobody can create or modify entri Role Allowed to Set Frozen Accounts and Edit Frozen Entries,Rôle autorisé à définir des comptes gelés et à modifier les entrées gelées, Address used to determine Tax Category in transactions,Adresse utilisée pour déterminer la catégorie de taxe dans les transactions, "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ","Le pourcentage que vous êtes autorisé à facturer davantage par rapport au montant commandé. Par exemple, si la valeur de la commande est de 100 USD pour un article et que la tolérance est définie sur 10%, vous êtes autorisé à facturer jusqu'à 110 USD.", -This role is allowed to submit transactions that exceed credit limits,Ce rôle est autorisé à soumettre des transactions qui dépassent les limites de crédit, +This role is allowed to submit transactions that exceed credit limits,Ce rôle est autorisé à valider des transactions qui dépassent les limites de crédit, "If ""Months"" is selected, a fixed amount will be booked as deferred revenue or expense for each month irrespective of the number of days in a month. It will be prorated if deferred revenue or expense is not booked for an entire month","Si «Mois» est sélectionné, un montant fixe sera comptabilisé en tant que revenus ou dépenses différés pour chaque mois, quel que soit le nombre de jours dans un mois. Il sera calculé au prorata si les revenus ou les dépenses différés ne sont pas comptabilisés pour un mois entier", "If this is unchecked, direct GL entries will be created to book deferred revenue or expense","Si cette case n'est pas cochée, des entrées GL directes seront créées pour enregistrer les revenus ou les dépenses différés", Show Inclusive Tax in Print,Afficher la taxe incluse en version imprimée, @@ -9744,7 +9744,7 @@ Print Receipt,Imprimer le reçu, Edit Receipt,Modifier le reçu, Focus on search input,Focus sur l'entrée de recherche, Focus on Item Group filter,Focus sur le filtre de groupe d'articles, -Checkout Order / Submit Order / New Order,Commander la commande / Soumettre la commande / Nouvelle commande, +Checkout Order / Submit Order / New Order,Commander la commande / Valider la commande / Nouvelle commande, Add Order Discount,Ajouter une remise de commande, Item Code: {0} is not available under warehouse {1}.,Code d'article: {0} n'est pas disponible dans l'entrepôt {1}., Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.,Numéros de série non disponibles pour l'article {0} sous l'entrepôt {1}. Veuillez essayer de changer d’entrepôt., @@ -9787,11 +9787,11 @@ because expense is booked against this account in Purchase Receipt {},car les d as no Purchase Receipt is created against Item {}. ,car aucun reçu d'achat n'est créé pour l'article {}., This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice,Ceci est fait pour gérer la comptabilité des cas où le reçu d'achat est créé après la facture d'achat, Purchase Order Required for item {},Bon de commande requis pour l'article {}, -To submit the invoice without purchase order please set {} ,"Pour soumettre la facture sans bon de commande, veuillez définir {}", +To submit the invoice without purchase order please set {} ,"Pour valider la facture sans bon de commande, veuillez définir {}", as {} in {},un péché {}, Mandatory Purchase Order,Bon de commande obligatoire, Purchase Receipt Required for item {},Reçu d'achat requis pour l'article {}, -To submit the invoice without purchase receipt please set {} ,"Pour soumettre la facture sans reçu d'achat, veuillez définir {}", +To submit the invoice without purchase receipt please set {} ,"Pour valider la facture sans reçu d'achat, veuillez définir {}", Mandatory Purchase Receipt,Reçu d'achat obligatoire, POS Profile {} does not belongs to company {},Le profil PDV {} n'appartient pas à l'entreprise {}, User {} is disabled. Please select valid user/cashier,L'utilisateur {} est désactivé. Veuillez sélectionner un utilisateur / caissier valide, @@ -9865,9 +9865,10 @@ Control Historical Stock Transactions,Controle de l'historique des stransact No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date. Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée -"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","LEs utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire" +"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire" Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries +"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" From c9af4e8ce54df7ad25db6008a5c503b961d1b428 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 11 Apr 2022 16:55:22 +0530 Subject: [PATCH 847/951] chore: broken link --- .../doctype/shopify_settings/shopify_settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js index a926a7e52a5..a5b676e2e39 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.js @@ -37,7 +37,7 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){ } - let app_link = "
    Ecommerce Integrations" + let app_link = "Ecommerce Integrations" frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true); }) From f6c9f052d24500ec80e5c06545981d882c019ee5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Apr 2022 17:33:46 +0530 Subject: [PATCH 848/951] fix: Deferred Revenue/Expense Account validation (cherry picked from commit 9bf5f76ac835914bf5124227e564770f93680113) --- erpnext/accounts/deferred_revenue.py | 3 +-- .../doctype/journal_entry/journal_entry.json | 13 +++++++++-- .../process_deferred_accounting.py | 4 ++-- .../sales_invoice/test_sales_invoice.py | 2 +- erpnext/controllers/accounts_controller.py | 22 +++++++++++++++++++ 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 0611f880c5e..a8776fa3448 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -386,7 +386,6 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): doc, credit_account, debit_account, - against, amount, base_amount, end_date, @@ -570,7 +569,6 @@ def book_revenue_via_journal_entry( doc, credit_account, debit_account, - against, amount, base_amount, posting_date, @@ -591,6 +589,7 @@ def book_revenue_via_journal_entry( journal_entry.voucher_type = ( "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense" ) + journal_entry.process_deferred_accounting = deferred_process debit_entry = { "account": credit_account, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 335fd350def..4493c722544 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -3,7 +3,7 @@ "allow_auto_repeat": 1, "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-03-25 10:53:52", + "creation": "2022-01-25 10:29:58.717206", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -13,6 +13,7 @@ "voucher_type", "naming_series", "finance_book", + "process_deferred_accounting", "reversal_of", "tax_withholding_category", "column_break1", @@ -524,13 +525,20 @@ "label": "Reversal Of", "options": "Journal Entry", "read_only": 1 + }, + { + "fieldname": "process_deferred_accounting", + "fieldtype": "Link", + "label": "Process Deferred Accounting", + "options": "Process Deferred Accounting", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-01-04 13:39:36.485954", + "modified": "2022-04-06 17:18:46.865259", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", @@ -578,6 +586,7 @@ "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py index 08a7f4110f6..8ec726b36cd 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -11,7 +11,7 @@ from erpnext.accounts.deferred_revenue import ( convert_deferred_expense_to_expense, convert_deferred_revenue_to_income, ) -from erpnext.accounts.general_ledger import make_reverse_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries class ProcessDeferredAccounting(Document): @@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document): filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, ) - make_reverse_gl_entries(gl_entries=gl_entries) + make_gl_entries(gl_entries=gl_entries, cancel=1) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6ddc3e03ecd..6e872d422f3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3017,7 +3017,7 @@ class TestSalesInvoice(unittest.TestCase): acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 - acc_settings.submit_journal_entriessubmit_journal_entries = 0 + acc_settings.submit_journal_entries = 0 acc_settings.save() frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0dbff48f02a..36887f444c8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -181,6 +181,7 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.validate_deferred_income_expense_account() self.set_inter_company_account() if self.doctype == "Purchase Invoice": @@ -209,6 +210,27 @@ class AccountsController(TransactionBase): (self.doctype, self.name), ) + def validate_deferred_income_expense_account(self): + field_map = { + "Sales Invoice": "deferred_revenue_account", + "Purchase Invoice": "deferred_expense_account", + } + + for item in self.get("items"): + if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): + if not item.get(field_map.get(self.doctype)): + default_deferred_account = frappe.db.get_value( + "Company", self.company, "default_" + field_map.get(self.doctype) + ) + if not default_deferred_account: + frappe.throw( + _( + "Row #{0}: Please update deferred revenue/expense account in item row or default account in company master" + ).format(item.idx) + ) + else: + item.set(field_map.get(self.doctype), default_deferred_account) + def validate_deferred_start_and_end_date(self): for d in self.items: if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): From 4296fc36cc0df41e487734bb5cbb669eb9fac8d6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 11 Apr 2022 15:29:20 +0530 Subject: [PATCH 849/951] test: Add test (cherry picked from commit 553178bfe72e7031a7fe775fc5d2136c9a5aefcf) --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6e872d422f3..fb2bb08c905 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2245,6 +2245,14 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, "2019-01-30") + def test_deferred_revenue_missing_account(self): + si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True) + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2019-01-10" + si.items[0].service_end_date = "2019-03-15" + + self.assertRaises(frappe.ValidationError, si.save) + def test_fixed_deferred_revenue(self): deferred_account = create_account( account_name="Deferred Revenue", From d079d8771b092e30a6a09654e65246fbaf8487b3 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Apr 2022 14:10:57 +0530 Subject: [PATCH 850/951] fix: Handle multiple item transfer in separate SEs against WO - Check for pending qty in child items to show/hide "Start" button - If no qty needed to transfer (FG qty is fulfilled), but RM qty pending: map pending in SE with For Quantity = 0 (cherry picked from commit dfff4beaf47da9a044f0a293a7fc50f8cda3c51e) --- erpnext/manufacturing/doctype/work_order/work_order.js | 6 ++++-- erpnext/stock/doctype/stock_entry/stock_entry.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 3f2f39e73af..602319eda7a 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -540,8 +540,10 @@ erpnext.work_order = { || frm.doc.transfer_material_against == 'Job Card') ? 0 : 1; if (show_start_btn) { - if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty)) - && frm.doc.status != 'Stopped') { + let pending_to_transfer = frm.doc.required_items.some( + item => flt(item.transferred_qty) < flt(item.required_qty) + ) + if (pending_to_transfer && frm.doc.status != 'Stopped') { frm.has_start_btn = true; frm.add_custom_button(__('Create Pick List'), function() { erpnext.work_order.create_pick_list(frm); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index cc7317a2cd8..e3af675178a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1802,7 +1802,9 @@ class StockEntry(StockController): or (desire_to_transfer > 0 and backflush_based_on == "Material Transferred for Manufacture") or allow_overproduction ): - item_dict[item]["qty"] = desire_to_transfer + # "No need for transfer but qty still pending to transfer" case can occur + # when transferring multiple RM in different Stock Entries + item_dict[item]["qty"] = desire_to_transfer if (desire_to_transfer > 0) else pending_to_issue elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue else: From c674180f1a1e66a1f35a63ebb4351f69a8127469 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Apr 2022 15:32:36 +0530 Subject: [PATCH 851/951] style: Missing Semicolon (cherry picked from commit be2e5ce966bb206777c35398ae6fb99f1dcd7e79) --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 602319eda7a..9b0c8382c53 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -542,7 +542,7 @@ erpnext.work_order = { if (show_start_btn) { let pending_to_transfer = frm.doc.required_items.some( item => flt(item.transferred_qty) < flt(item.required_qty) - ) + ); if (pending_to_transfer && frm.doc.status != 'Stopped') { frm.has_start_btn = true; frm.add_custom_button(__('Create Pick List'), function() { From e322e7654a32ce0f52039393701878f5aec8dd12 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 11 Apr 2022 16:44:55 +0530 Subject: [PATCH 852/951] test: Multiple RM transfer in separate Stock Entries - Added test and acceptance of 0 as For Quantity in test helper (cherry picked from commit 5aa60bb65142a9dbd2fbbec5c26150cd559275e6) --- .../doctype/work_order/test_work_order.py | 50 +++++++++++++++++++ .../doctype/work_order/work_order.py | 6 ++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 1e9b3ba1137..7131c335c8a 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1100,6 +1100,56 @@ class TestWorkOrder(FrappeTestCase): for index, row in enumerate(ste_manu.get("items"), start=1): self.assertEqual(index, row.idx) + @change_settings( + "Manufacturing Settings", + {"backflush_raw_materials_based_on": "Material Transferred for Manufacture"}, + ) + def test_work_order_multiple_material_transfer(self): + """ + Test transferring multiple RMs in separate Stock Entries. + """ + work_order = make_wo_order_test_record(planned_start_date=now(), qty=1) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item", + target="_Test Warehouse - _TC", + qty=1, + basic_rate=5000.0, + ) + test_stock_entry.make_stock_entry( # stock up RM + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse - _TC", + qty=2, + basic_rate=1000.0, + ) + + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1) + ) + del transfer_entry.get("items")[0] # transfer only one RM + transfer_entry.submit() + + # WO's "Material Transferred for Mfg" shows all is transferred, one RM is pending + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 0) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + + final_transfer_entry = frappe.get_doc( # transfer last RM with For Quantity = 0 + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 0) + ) + final_transfer_entry.save() + + self.assertEqual(final_transfer_entry.fg_completed_qty, 0.0) + self.assertEqual(final_transfer_entry.items[0].qty, 1) + + final_transfer_entry.submit() + work_order.reload() + + # WO's "Material Transferred for Mfg" shows all is transferred, no RM is pending + self.assertEqual(work_order.material_transferred_for_manufacturing, 1) + self.assertEqual(work_order.required_items[0].transferred_qty, 1) + self.assertEqual(work_order.required_items[1].transferred_qty, 2) + def update_job_card(job_card, jc_qty=None): employee = frappe.db.get_value("Employee", {"status": "Active"}, "name") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 7ef39c26aa0..dc553c15ad5 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1174,7 +1174,11 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.from_bom = 1 stock_entry.bom_no = work_order.bom_no stock_entry.use_multi_level_bom = work_order.use_multi_level_bom - stock_entry.fg_completed_qty = qty or (flt(work_order.qty) - flt(work_order.produced_qty)) + # accept 0 qty as well + stock_entry.fg_completed_qty = ( + qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty)) + ) + if work_order.bom_no: stock_entry.inspection_required = frappe.db.get_value( "BOM", work_order.bom_no, "inspection_required" From 9efc0d1043ffcf20e0254800f1f40a158de41df4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 8 Apr 2022 17:14:10 +0530 Subject: [PATCH 853/951] fix: Download JSON for GSTR-1 report (cherry picked from commit b532ade3839d502a0aab5a67db77654a2077066a) --- erpnext/regional/report/gstr_1/gstr_1.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 9999a6d167b..943bd2c3d20 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -78,8 +78,9 @@ frappe.query_reports["GSTR-1"] = { } }); - report.page.add_inner_button(__("Download as JSON"), function () { + let filters = report.get_values(); + frappe.call({ method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', args: { From 21066a48b6e065795426ab314a67b75ce3104299 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 12 Apr 2022 11:16:59 +0530 Subject: [PATCH 854/951] fix: ignore item-less maintenance visit for sr no (#30684) (#30685) (cherry picked from commit 60fb71bd2a6bfb0a223b233759c8aa8cbda7a1bb) Co-authored-by: Ankush Menat --- .../maintenance/doctype/maintenance_visit/maintenance_visit.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js index f4a0d4d399c..939df5c07c5 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js @@ -12,6 +12,9 @@ frappe.ui.form.on('Maintenance Visit', { // filters for serial no based on item code if (frm.doc.maintenance_type === "Scheduled") { let item_code = frm.doc.purposes[0].item_code; + if (!item_code) { + return; + } frappe.call({ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.get_serial_nos_from_schedule", args: { From 2ab431ad36fde53784a4041beb96fb1a6f6e4b3d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 7 Apr 2022 13:09:05 +0530 Subject: [PATCH 855/951] feat(india): e-invoicing for intra-state union territory transactions (cherry picked from commit 45fca6bed78ef162cf2969faf75086de94392180) --- .../doctype/gst_account/gst_account.json | 10 +++++++++- erpnext/regional/india/e_invoice/utils.py | 16 ++++++++++++---- erpnext/regional/india/utils.py | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/gst_account/gst_account.json b/erpnext/accounts/doctype/gst_account/gst_account.json index b6ec8844e18..be5124c2d4d 100644 --- a/erpnext/accounts/doctype/gst_account/gst_account.json +++ b/erpnext/accounts/doctype/gst_account/gst_account.json @@ -10,6 +10,7 @@ "sgst_account", "igst_account", "cess_account", + "utgst_account", "is_reverse_charge_account" ], "fields": [ @@ -64,12 +65,18 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Is Reverse Charge Account" + }, + { + "fieldname": "utgst_account", + "fieldtype": "Link", + "label": "UTGST Account", + "options": "Account" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-04-09 12:30:25.889993", + "modified": "2022-04-07 12:59:14.039768", "modified_by": "Administrator", "module": "Accounts", "name": "GST Account", @@ -78,5 +85,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 95cbcd51a9f..2b5d17710eb 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -315,10 +315,14 @@ def update_item_taxes(invoice, item): item.cess_rate += item_tax_rate item.cess_amount += abs(item_tax_amount_after_discount) - for tax_type in ["igst", "cgst", "sgst"]: + for tax_type in ["igst", "cgst", "sgst", "utgst"]: if t.account_head in gst_accounts[f"{tax_type}_account"]: item.tax_rate += item_tax_rate - item[f"{tax_type}_amount"] += abs(item_tax_amount) + if tax_type == "utgst": + # utgst taxes are reported same as sgst tax + item["sgst_amount"] += abs(item_tax_amount) + else: + item[f"{tax_type}_amount"] += abs(item_tax_amount) else: # TODO: other charges per item pass @@ -360,11 +364,15 @@ def update_invoice_taxes(invoice, invoice_value_details): # using after discount amt since item also uses after discount amt for cess calc invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) - for tax_type in ["igst", "cgst", "sgst"]: + for tax_type in ["igst", "cgst", "sgst", "utgst"]: if t.account_head in gst_accounts[f"{tax_type}_account"]: + if tax_type == "utgst": + invoice_value_details["total_sgst_amt"] += abs(tax_amount) + else: + invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount) - invoice_value_details[f"total_{tax_type}_amt"] += abs(tax_amount) update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) + else: invoice_value_details.total_other_charges += abs(tax_amount) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 6bbaa358cde..45104b09681 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -825,7 +825,7 @@ def get_gst_accounts( gst_settings_accounts = frappe.get_all( "GST Account", filters=filters, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"], + fields=["cgst_account", "sgst_account", "igst_account", "cess_account", "utgst_account"], ) if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate: From e650d99cdd4549513dda4fe32d51a784f5d52f1c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 12 Apr 2022 12:24:47 +0530 Subject: [PATCH 856/951] feat: Ignore permlevel for specific fields (cherry picked from commit 993c6c0de9c1e3cfc3c4673e3bc16542204022be) --- erpnext/controllers/buying_controller.py | 3 +++ erpnext/controllers/selling_controller.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 931b4f82d97..8e644814a54 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -22,6 +22,9 @@ class QtyMismatchError(ValidationError): class BuyingController(StockController, Subcontracting): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] + def get_feed(self): if self.get("supplier_name"): return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 77e0be3663a..e1c59cbf672 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -16,6 +16,9 @@ from erpnext.stock.utils import get_incoming_rate class SellingController(StockController): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"] + def get_feed(self): return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) From 46d773df5dc6547319a162f78917566d4c4e39ce Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 12 Apr 2022 15:27:08 +0530 Subject: [PATCH 857/951] fix: Do not show disabled dimensions in reports (cherry picked from commit 9a1c560c8248fd0253a3c1ccbceb6f3eee92bcce) --- .../accounting_dimension/accounting_dimension.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 897151a97b7..445378300bb 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -205,10 +205,16 @@ def get_doctypes_with_dimensions(): return frappe.get_hooks("accounting_dimension_doctypes") -def get_accounting_dimensions(as_list=True): +def get_accounting_dimensions(as_list=True, filters=None): + + if not filters: + filters = {"disabled": 0} + if frappe.flags.accounting_dimensions is None: frappe.flags.accounting_dimensions = frappe.get_all( - "Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"] + "Accounting Dimension", + fields=["label", "fieldname", "disabled", "document_type"], + filters=filters, ) if as_list: From 147499bc9c2b570e50c9ab0ffad077d4ec1d25ac Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 12 Apr 2022 23:01:09 +0530 Subject: [PATCH 858/951] chore: change log and version bump --- erpnext/__init__.py | 2 +- erpnext/change_log/v13/v13_26_0.md | 31 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 erpnext/change_log/v13/v13_26_0.md diff --git a/erpnext/__init__.py b/erpnext/__init__.py index b19f1cecaaa..b34176ed34a 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.25.2' +__version__ = "13.26.0" def get_default_company(user=None): """Get default company for user""" diff --git a/erpnext/change_log/v13/v13_26_0.md b/erpnext/change_log/v13/v13_26_0.md new file mode 100644 index 00000000000..67450983dfb --- /dev/null +++ b/erpnext/change_log/v13/v13_26_0.md @@ -0,0 +1,31 @@ +## Version 13.25.0 Release Notes + +### Features & Enhancements + +- feat: Receivable/Payable Account column and filter in AR/AP report ([#30620](https://github.com/frappe/erpnext/pull/30620)) +- feat(india): e-invoicing for intra-state union territory transactions ([#30626](https://github.com/frappe/erpnext/pull/30626)) +- feat: Ignore permlevel for specific fields ([#30686](https://github.com/frappe/erpnext/pull/30686)) +- feat: 'customer' column and more filter to Payment terms status report ([#30499](https://github.com/frappe/erpnext/pull/30499)) + +### Fixes + +- fix: fallback to item_name if description is not found ([#30619](https://github.com/frappe/erpnext/pull/30619)) +- fix: Deferred Revenue/Expense Account validation ([#30602](https://github.com/frappe/erpnext/pull/30602)) +- fix(pos): reload doc before set value ([#30610](https://github.com/frappe/erpnext/pull/30610)) +- fix: Exchange gain and loss button in Payment Entry ([#30606](https://github.com/frappe/erpnext/pull/30606)) +- fix(pos): cannot change paid amount in pos payments ([#30657](https://github.com/frappe/erpnext/pull/30657)) +- fix: hide pending qty only if original item is assigned ([#30599](https://github.com/frappe/erpnext/pull/30599)) +- fix(pos): reload doc before set value ([#30611](https://github.com/frappe/erpnext/pull/30611)) +- fix: Handle multiple item transfer in separate SEs against WO ([#30674](https://github.com/frappe/erpnext/pull/30674)) +- fix(patch): check null values in is_cancelled patch ([#30594](https://github.com/frappe/erpnext/pull/30594)) +- fix: Download JSON for GSTR-1 report ([#30651](https://github.com/frappe/erpnext/pull/30651)) +- fix: remove bad defaults from BOM operation ([#30644](https://github.com/frappe/erpnext/pull/30644)) +- fix: update translation ([#30474](https://github.com/frappe/erpnext/pull/30474)) +- fix: dont reassign mutable (list) to a different field ([#30634](https://github.com/frappe/erpnext/pull/30634)) +- fix: update translation ([#30654](https://github.com/frappe/erpnext/pull/30654)) +- fix: ignore item-less maintenance visit for sr no ([#30684](https://github.com/frappe/erpnext/pull/30684)) +- fix: removed unused courses template ([#30596](https://github.com/frappe/erpnext/pull/30596)) +- fix: Implicit ignore pricing rule check on returns ([#30662](https://github.com/frappe/erpnext/pull/30662)) +- fix: warehouse naming when suffix is present ([#30621](https://github.com/frappe/erpnext/pull/30621)) +- fix: Ignore disabled tax categories ([#30542](https://github.com/frappe/erpnext/pull/30542)) + From 512b480f6a9d8b9b2ed31c3c593c2e059363b7c6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 12 Apr 2022 23:31:47 +0530 Subject: [PATCH 859/951] chore: version bump --- erpnext/change_log/v13/v13_26_0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/change_log/v13/v13_26_0.md b/erpnext/change_log/v13/v13_26_0.md index 67450983dfb..b29d018039e 100644 --- a/erpnext/change_log/v13/v13_26_0.md +++ b/erpnext/change_log/v13/v13_26_0.md @@ -1,4 +1,4 @@ -## Version 13.25.0 Release Notes +## Version 13.26.0 Release Notes ### Features & Enhancements From 4008c95ac6afc3b06a0a602e3f469db61e1f0453 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 13 Apr 2022 12:21:33 +0530 Subject: [PATCH 860/951] fix: Payment reco query with max invocie and payment amount limit --- .../payment_reconciliation.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 907b76915af..b596df92247 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -350,9 +350,13 @@ class PaymentReconciliation(Document): ) if self.minimum_invoice_amount: - condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount)) + condition += " and {dr_or_cr} >= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount) + ) if self.maximum_invoice_amount: - condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount)) + condition += " and {dr_or_cr} <= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount) + ) elif get_return_invoices: condition = " and doc.company = '{0}' ".format(self.company) @@ -367,15 +371,19 @@ class PaymentReconciliation(Document): else "" ) dr_or_cr = ( - "gl.debit_in_account_currency" + "debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" - else "gl.credit_in_account_currency" + else "credit_in_account_currency" ) if self.minimum_invoice_amount: - condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount)) + condition += " and gl.{dr_or_cr} >= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount) + ) if self.maximum_invoice_amount: - condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount)) + condition += " and gl.{dr_or_cr} <= {amount}".format( + dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount) + ) else: condition += ( From f604101feabaeb1766bcb3d0376d22352f124c64 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 12 Apr 2022 14:30:01 +0530 Subject: [PATCH 861/951] fix: Map Production Plan company in subassembly WO created from it (cherry picked from commit 2777c5c67c369fc86aa1748fab2326e72581039a) --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 2139260df60..d673868a485 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -462,6 +462,7 @@ class ProductionPlan(Document): work_order_data = { "wip_warehouse": default_warehouses.get("wip_warehouse"), "fg_warehouse": default_warehouses.get("fg_warehouse"), + "company": self.get("company"), } self.prepare_data_for_sub_assembly_items(row, work_order_data) From 44be67e6ddf7556b6b929b6381675b7919d1c074 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 13 Apr 2022 11:47:58 +0530 Subject: [PATCH 862/951] fix: Map correct company to PO made via Prod Plan (subcontract) (cherry picked from commit 6315acc4507a4e9ff6ea3ec03971b0043e798783) --- erpnext/manufacturing/doctype/production_plan/production_plan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d673868a485..ff739e9d72b 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -500,6 +500,7 @@ class ProductionPlan(Document): for supplier, po_list in subcontracted_po.items(): po = frappe.new_doc("Purchase Order") + po.company = self.company po.supplier = supplier po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() po.is_subcontracted = "Yes" From 9cf790db9d81e809e34fd2c6ec8c3f63b29e3de2 Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Wed, 13 Apr 2022 14:15:10 +0530 Subject: [PATCH 863/951] fix: Depreciation Amount calculation for first row when Monthly Depreciation is allowed (#30692) --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a2ad2cf5bb2..21d90b5e48a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -364,7 +364,7 @@ class Asset(AccountsController): if has_pro_rata and n == 0: # For first entry of monthly depr if r == 0: - days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1 per_day_amt = depreciation_amount / days depreciation_amount_for_current_month = per_day_amt * days_until_first_depr depreciation_amount -= depreciation_amount_for_current_month From ef4eb4705ee12c50e3cd2afc13f44b95726cd63a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 13 Apr 2022 15:07:48 +0530 Subject: [PATCH 864/951] chore: warning for DATEV deprecation (#30703) --- erpnext/patches.txt | 1 + erpnext/patches/v13_0/datev_deprecation_warning.py | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 erpnext/patches/v13_0/datev_deprecation_warning.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 98e07783c11..b2d0871a17c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -350,6 +350,7 @@ erpnext.patches.v13_0.enable_provisional_accounting erpnext.patches.v13_0.update_disbursement_account erpnext.patches.v13_0.update_reserved_qty_closed_wo erpnext.patches.v13_0.amazon_mws_deprecation_warning +erpnext.patches.v13_0.datev_deprecation_warning erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 diff --git a/erpnext/patches/v13_0/datev_deprecation_warning.py b/erpnext/patches/v13_0/datev_deprecation_warning.py new file mode 100644 index 00000000000..bf58440a610 --- /dev/null +++ b/erpnext/patches/v13_0/datev_deprecation_warning.py @@ -0,0 +1,9 @@ +import click + + +def execute(): + click.secho( + "DATEV reports are moved to a separate app and will be removed from ERPNext in version-14.\n" + "Please install the app to continue using them: https://github.com/alyf-de/erpnext_datev", + fg="yellow", + ) From 5776881f34d8cc44dae6f05cdb87a010843dcf96 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 11 Apr 2022 14:35:22 +0530 Subject: [PATCH 865/951] feat: Item-wise provisional accounting for service items (cherry picked from commit 3ce64170dba0ffd0ecbba5c6b2e8161da0ad82de) --- .../purchase_invoice/purchase_invoice.py | 6 +++-- erpnext/setup/doctype/company/company.js | 3 ++- erpnext/stock/doctype/item/item.js | 11 ++++++++++ .../doctype/item_default/item_default.json | 10 ++++++++- .../purchase_receipt/purchase_receipt.json | 17 +------------- .../purchase_receipt/purchase_receipt.py | 22 +++++++++++-------- .../purchase_receipt_item.json | 22 +++++++++++++++++-- erpnext/stock/get_item_details.py | 5 +++++ 8 files changed, 65 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 8500d57f44b..a14ae251e01 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -801,7 +801,9 @@ class PurchaseInvoice(BuyingController): if provisional_accounting_for_non_stock_items: if item.purchase_receipt: - provisional_account = self.get_company_default("default_provisional_account") + provisional_account = frappe.db.get_value( + "Purchase Receipt Item", item.pr_detail, "provisional_expense_account" + ) or self.get_company_default("default_provisional_account") purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) if not purchase_receipt_doc: @@ -824,7 +826,7 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, reverse=1 + item, gl_entries, self.posting_date, provisional_account, reverse=1 ) if not self.is_internal_transfer(): diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index dd185fc6636..0de5b2d5a32 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -233,7 +233,8 @@ erpnext.company.setup_queries = function(frm) { ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], - ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}] + ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}], + ["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}] ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 8206e095797..399fe7a9ff6 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -377,6 +377,17 @@ $.extend(erpnext.item, { } } + frm.set_query('default_provisional_account', 'item_defaults', (doc, cdt, cdn) => { + let row = locals[cdt][cdn]; + return { + filters: { + "company": row.company, + "root_type": ["in", ["Liability", "Asset"]], + "is_group": 0 + } + } + }) + }, make_dashboard: function(frm) { diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index bc171604f43..042d398256a 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -15,6 +15,7 @@ "default_supplier", "column_break_8", "expense_account", + "default_provisional_account", "selling_defaults", "selling_cost_center", "column_break_12", @@ -101,11 +102,17 @@ "fieldtype": "Link", "label": "Default Discount Account", "options": "Account" + }, + { + "fieldname": "default_provisional_account", + "fieldtype": "Link", + "label": "Default Provisional Account", + "options": "Account" } ], "istable": 1, "links": [], - "modified": "2021-07-13 01:26:03.860065", + "modified": "2022-04-10 20:18:54.148195", "modified_by": "Administrator", "module": "Stock", "name": "Item Default", @@ -114,5 +121,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 6d4b4a19bd2..7c6189b1eb1 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -106,8 +106,6 @@ "terms", "bill_no", "bill_date", - "accounting_details_section", - "provisional_expense_account", "more_info", "project", "status", @@ -1146,26 +1144,13 @@ "label": "Represents Company", "options": "Company", "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "accounting_details_section", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, - { - "fieldname": "provisional_expense_account", - "fieldtype": "Link", - "hidden": 1, - "label": "Provisional Expense Account", - "options": "Account" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-03-10 11:40:52.690984", + "modified": "2022-04-10 22:50:37.761362", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 37074a2f000..f27bc376b94 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -146,10 +146,13 @@ class PurchaseReceipt(BuyingController): ) ) - if provisional_accounting_for_non_stock_items: - default_provisional_account = self.get_company_default("default_provisional_account") - if not self.provisional_expense_account: - self.provisional_expense_account = default_provisional_account + if not provisional_accounting_for_non_stock_items: + return + + default_provisional_account = self.get_company_default("default_provisional_account") + for item in self.get("items"): + if not item.get("provisional_expense_account"): + item.provisional_expense_account = default_provisional_account def validate_with_previous_doc(self): super(PurchaseReceipt, self).validate_with_previous_doc( @@ -475,9 +478,10 @@ class PurchaseReceipt(BuyingController): + "\n".join(warehouse_with_no_account) ) - def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0): - provisional_expense_account = self.get("provisional_expense_account") - credit_currency = get_account_currency(provisional_expense_account) + def add_provisional_gl_entry( + self, item, gl_entries, posting_date, provisional_account, reverse=0 + ): + credit_currency = get_account_currency(provisional_account) debit_currency = get_account_currency(item.expense_account) expense_account = item.expense_account remarks = self.get("remarks") or _("Accounting Entry for Service") @@ -491,7 +495,7 @@ class PurchaseReceipt(BuyingController): self.add_gl_entry( gl_entries=gl_entries, - account=provisional_expense_account, + account=provisional_account, cost_center=item.cost_center, debit=0.0, credit=multiplication_factor * item.amount, @@ -511,7 +515,7 @@ class PurchaseReceipt(BuyingController): debit=multiplication_factor * item.amount, credit=0.0, remarks=remarks, - against_account=provisional_expense_account, + against_account=provisional_account, account_currency=debit_currency, project=item.project, voucher_detail_no=item.name, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index e5994b2dd48..aa217441c0b 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -96,7 +96,6 @@ "include_exploded_items", "batch_no", "rejected_serial_no", - "expense_account", "item_tax_rate", "item_weight_details", "weight_per_unit", @@ -107,6 +106,10 @@ "manufacturer", "column_break_16", "manufacturer_part_no", + "accounting_details_section", + "expense_account", + "column_break_102", + "provisional_expense_account", "accounting_dimensions_section", "project", "dimension_col_break", @@ -971,12 +974,27 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "fieldname": "provisional_expense_account", + "fieldtype": "Link", + "label": "Provisional Expense Account", + "options": "Account" + }, + { + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "column_break_102", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-01 11:32:27.980524", + "modified": "2022-04-11 13:07:32.061402", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 19680f6286e..384dd7d94f4 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -335,6 +335,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), "discount_account": get_default_discount_account(args, item_defaults), + "provisional_expense_account": get_provisional_account(args, item_defaults), "cost_center": get_default_cost_center( args, item_defaults, item_group_defaults, brand_defaults ), @@ -689,6 +690,10 @@ def get_default_expense_account(args, item, item_group, brand): ) +def get_provisional_account(args, item): + return item.get("default_provisional_account") or args.default_provisional_account + + def get_default_discount_account(args, item): return item.get("default_discount_account") or args.discount_account From 3dcef9352ef6dc1bf2150fa69bbe13a5d2d56c4c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Apr 2022 12:03:12 +0530 Subject: [PATCH 866/951] test: Update test case (cherry picked from commit ad171c6225fc50305e565ce2a67bc60fc1e85722) --- .../doctype/purchase_invoice/test_purchase_invoice.py | 5 ++++- erpnext/stock/doctype/item/item.js | 4 ++-- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index d1543fe10d2..15803b5bfe1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1449,7 +1449,8 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) def test_provisional_accounting_entry(self): - item = create_item("_Test Non Stock Item", is_stock_item=0) + create_item("_Test Non Stock Item", is_stock_item=0) + provisional_account = create_account( account_name="Provision Account", parent_account="Current Liabilities - _TC", @@ -1472,6 +1473,8 @@ class TestPurchaseInvoice(unittest.TestCase): pi.save() pi.submit() + self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC") + # Check GLE for Purchase Invoice expected_gle = [ ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)], diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 399fe7a9ff6..de6316cc05d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -385,8 +385,8 @@ $.extend(erpnext.item, { "root_type": ["in", ["Liability", "Asset"]], "is_group": 0 } - } - }) + }; + }); }, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f27bc376b94..db94beccbcf 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -469,7 +469,9 @@ class PurchaseReceipt(BuyingController): and flt(d.qty) and provisional_accounting_for_non_stock_items ): - self.add_provisional_gl_entry(d, gl_entries, self.posting_date) + self.add_provisional_gl_entry( + d, gl_entries, self.posting_date, d.get("provisional_expense_account") + ) if warehouse_with_no_account: frappe.msgprint( From 3a7ac29907a85145f0191112979c20b3508ad672 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 14 Apr 2022 13:03:34 +0530 Subject: [PATCH 867/951] chore: check in missing __init__.py (#30713) --- erpnext/www/shop-by-category/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 erpnext/www/shop-by-category/__init__.py diff --git a/erpnext/www/shop-by-category/__init__.py b/erpnext/www/shop-by-category/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 9c5f8415bf1bd29bf34d32f49ad4c0b84ed68a6e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 13 Apr 2022 20:52:25 +0530 Subject: [PATCH 868/951] fix: Exchange gain and loss on advance jv allocation (cherry picked from commit 31883b699dc4ae545f0cbad081e47b6cb3c9d84a) --- .../purchase_invoice/purchase_invoice.js | 3 + .../doctype/sales_invoice/sales_invoice.js | 4 +- .../sales_invoice/test_sales_invoice.py | 56 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 2 +- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4e4e2154375..4f5640f9cb9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -30,6 +30,9 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ onload: function() { this._super(); + // Ignore linked advances + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry']; + if(!this.frm.doc.__islocal) { // show credit_to in print format if(!this.frm.doc.supplier && this.frm.doc.credit_to) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 50c94b419b6..c6a110dcab6 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte var me = this; this._super(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry']; + this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', + 'POS Closing Entry', 'Journal Entry', 'Payment Entry']; + if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format this.frm.set_df_property("debit_to", "print_hide", 0); diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index fb2bb08c905..15cfc200941 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3037,6 +3037,62 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertTrue(si.items[0].serial_no) + def test_gain_loss_with_advance_entry(self): + from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry + + unlink_enabled = frappe.db.get_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" + ) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1 + ) + + jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False) + + jv.accounts[0].exchange_rate = 70 + jv.accounts[0].credit_in_account_currency = 100 + jv.accounts[0].party_type = "Customer" + jv.accounts[0].party = "Overseas Customer" + + jv.save() + jv.submit() + + si = create_sales_invoice( + customer="Overseas Customer", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=75, + do_not_save=1, + rate=100, + ) + + si.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.accounts[0].name, + "advance_amount": 100, + "allocated_amount": 100, + "ref_exchange_rate": 70, + }, + ) + si.save() + si.submit() + + expected_gle = [ + ["_Test Receivable USD - _TC", 7500.0, 500], + ["Exchange Gain/Loss - _TC", 500.0, 0.0], + ["Sales - _TC", 0.0, 7500.0], + ] + + check_gl_entries(self, si.name, expected_gle, nowdate()) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled + ) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 36887f444c8..216c57de56d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2005,7 +2005,7 @@ def get_advance_journal_entries( select "Journal Entry" as reference_type, t1.name as reference_name, t1.remark as remarks, t2.{0} as amount, t2.name as reference_row, - t2.reference_name as against_order + t2.reference_name as against_order, t2.exchange_rate from `tabJournal Entry` t1, `tabJournal Entry Account` t2 where From e135c452aff8d0e5e201cc36927e0fbd90810f23 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Apr 2022 11:10:41 +0530 Subject: [PATCH 869/951] test: Update customer (cherry picked from commit 972d9ec5b4b6fee242eea2c11d2323d0bb79f4da) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 15cfc200941..eb18f9d0d23 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3053,7 +3053,7 @@ class TestSalesInvoice(unittest.TestCase): jv.accounts[0].exchange_rate = 70 jv.accounts[0].credit_in_account_currency = 100 jv.accounts[0].party_type = "Customer" - jv.accounts[0].party = "Overseas Customer" + jv.accounts[0].party = "_Test Customer USD" jv.save() jv.submit() From a167f9e17f8bf82346278a7993453973b4ba510f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 14 Apr 2022 13:26:47 +0530 Subject: [PATCH 870/951] test: Update customer in Sales Invoice (cherry picked from commit c38be53ce8790bead2f32c1b65a78c71458f0611) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- erpnext/controllers/accounts_controller.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index eb18f9d0d23..68a83f4be0c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3059,7 +3059,7 @@ class TestSalesInvoice(unittest.TestCase): jv.submit() si = create_sales_invoice( - customer="Overseas Customer", + customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", currency="USD", conversion_rate=75, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 216c57de56d..49196dfe22e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2000,6 +2000,7 @@ def get_advance_journal_entries( reference_condition = " and (" + " or ".join(conditions) + ")" if conditions else "" + # nosemgrep journal_entries = frappe.db.sql( """ select From fb9d640d8b161ccdee091f1ce9838a32b30ea23e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 12:29:35 +0000 Subject: [PATCH 871/951] fix: Remove "Values Out of Sync" validation (backport #30707) (#30718) This is an automatic backport of pull request #30707 done by [Mergify](https://mergify.com). Cherry-pick of 89fab780279ca1ac7e888584c15befb9c37f16e9 has failed: ``` On branch mergify/bp/version-13-hotfix/pr-30707 Your branch is up to date with 'origin/version-13-hotfix'. You are currently cherry-picking commit 89fab78027. (fix conflicts and run "git cherry-pick --continue") (use "git cherry-pick --skip" to skip this patch) (use "git cherry-pick --abort" to cancel the cherry-pick operation) Unmerged paths: (use "git add ..." to mark resolution) both modified: erpnext/accounts/doctype/journal_entry/journal_entry.py no changes added to commit (use "git add" and/or "git commit -a") ``` To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/github/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally ---
    Mergify commands and options
    More conditions and actions can be found in the [documentation](https://docs.mergify.com/). You can also trigger Mergify actions by commenting on this pull request: - `@Mergifyio refresh` will re-evaluate the rules - `@Mergifyio rebase` will rebase this PR on its base branch - `@Mergifyio update` will merge the base branch into this PR - `@Mergifyio backport ` will backport this PR on `` branch Additionally, on Mergify [dashboard](https://dashboard.mergify.com/) you can: - look at your merge queues - generate the Mergify configuration with the config editor. Finally, you can contact us on https://mergify.com
    --- .../doctype/journal_entry/journal_entry.py | 4 -- erpnext/accounts/utils.py | 45 ------------------- .../repost_item_valuation.py | 14 +++--- 3 files changed, 6 insertions(+), 57 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index bf399d1d1e8..8660c18bf95 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -19,7 +19,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category ) from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( - check_if_stock_and_account_balance_synced, get_account_currency, get_balance_on, get_stock_accounts, @@ -88,9 +87,6 @@ class JournalEntry(AccountsController): self.update_expense_claim() self.update_inter_company_jv() self.update_invoice_discounting() - check_if_stock_and_account_balance_synced( - self.posting_date, self.company, self.doctype, self.name - ) def on_cancel(self): from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b58b53c1cb6..0d1d0dc031f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -19,10 +19,6 @@ from erpnext.stock import get_warehouse_account_map from erpnext.stock.utils import get_stock_value_on -class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): - pass - - class FiscalYearError(frappe.ValidationError): pass @@ -1247,47 +1243,6 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): return matched -def check_if_stock_and_account_balance_synced( - posting_date, company, voucher_type=None, voucher_no=None -): - if not cint(erpnext.is_perpetual_inventory_enabled(company)): - return - - accounts = get_stock_accounts(company, voucher_type, voucher_no) - stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account") - - for account in accounts: - account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( - account, posting_date, company - ) - - if abs(account_bal - stock_bal) > 0.1: - precision = get_field_precision( - frappe.get_meta("GL Entry").get_field("debit"), - currency=frappe.get_cached_value("Company", company, "default_currency"), - ) - - diff = flt(stock_bal - account_bal, precision) - - error_reason = _( - "Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}." - ).format(stock_bal, account_bal, frappe.bold(account), posting_date) - error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}").format( - frappe.bold(diff), frappe.bold(posting_date) - ) - - frappe.msgprint( - msg="""{0}

    {1}

    """.format(error_reason, error_resolution), - raise_exception=StockValueAndAccountBalanceOutOfSync, - title=_("Values Out Of Sync"), - primary_action={ - "label": _("Make Journal Entry"), - "client_action": "erpnext.route_to_adjustment_jv", - "args": get_journal_entry(account, stock_adjustment_account, diff), - }, - ) - - def get_stock_accounts(company, voucher_type=None, voucher_no=None): stock_accounts = [ d.name 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 508f26b5f1d..173b9aa21e5 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -4,15 +4,12 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today +from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException import erpnext -from erpnext.accounts.utils import ( - check_if_stock_and_account_balance_synced, - update_gl_entries_after, -) +from erpnext.accounts.utils import update_gl_entries_after from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle @@ -224,6 +221,10 @@ def notify_error_to_stock_managers(doc, traceback): def repost_entries(): + """ + Reposts 'Repost Item Valuation' entries in queue. + Called hourly via hooks.py. + """ if not in_configured_timeslot(): return @@ -239,9 +240,6 @@ def repost_entries(): if riv_entries: return - for d in frappe.get_all("Company", filters={"enable_perpetual_inventory": 1}): - check_if_stock_and_account_balance_synced(today(), d.name) - def get_repost_item_valuation_entries(): return frappe.db.sql( From fb127da489786658d68ee5f3be888623d2f5c38b Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Thu, 14 Apr 2022 14:16:47 +0200 Subject: [PATCH 872/951] fix: update translation (#30716) * fix: update translation * fix: update translation * fix: update translation * fix: update translation (cherry picked from commit e6aa28ea1409dee499e5ada0d8b0f16ccf1c4d61) --- erpnext/translations/fr.csv | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 90456c5c4ee..03e8366a265 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -271,7 +271,7 @@ Assessment Report,Rapport d'Évaluation, Assessment Reports,Rapports d'évaluation, Assessment Result,Résultat de l'Évaluation, Assessment Result record {0} already exists.,Le Résultat d'Évaluation {0} existe déjà., -Asset,Atout, +Asset,Actif - Immo., Asset Category,Catégorie d'Actif, Asset Category is mandatory for Fixed Asset item,Catégorie d'Actif est obligatoire pour l'article Immobilisé, Asset Maintenance,Maintenance des actifs, @@ -3037,6 +3037,7 @@ To Date must be greater than From Date,La date de fin doit être supérieure à To Date should be within the Fiscal Year. Assuming To Date = {0},La Date Finale doit être dans l'exercice. En supposant Date Finale = {0}, To Datetime,À la Date, To Deliver,À Livrer, +{} To Deliver,{} à livrer To Deliver and Bill,À Livrer et Facturer, To Fiscal Year,À l'année fiscale, To GSTIN,GSTIN (Destination), @@ -9872,3 +9873,4 @@ Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les t Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" +Unit Of Measure (UOM),Unité de mesure (UDM), From 93fe840844081c60ce88d8fc1621fe260463657a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 18 Apr 2022 10:21:15 +0530 Subject: [PATCH 873/951] fix: Price changing on creating Sales retrun from Delivery Note (cherry picked from commit 9c081947ec65c86ed54b4bddf022ef50b16cbbec) --- erpnext/public/js/controllers/transaction.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9ed78a7fb69..fdc129b9116 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1500,6 +1500,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ return; } + // Target doc created from a mapped doc + if (this.frm.doc.__onload && this.frm.doc.__onload.ignore_price_list) { + return; + } + return this.frm.call({ method: "erpnext.accounts.doctype.pricing_rule.pricing_rule.apply_pricing_rule", args: { args: args, doc: me.frm.doc }, @@ -1616,7 +1621,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ me.remove_pricing_rule(frappe.get_doc(d.doctype, d.name)); } - if (d.free_item_data) { + if (d.free_item_data.length > 0) { me.apply_product_discount(d); } From 8e30af84cdcb408cb48bffb1f00482685dbeb549 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Apr 2022 17:18:53 +0530 Subject: [PATCH 874/951] chore: Add semantic releases (backport #30729) (#30732) * chore: Add sematic releases (cherry picked from commit 41249c57c436e8ceb4b1737a7de824f8b7e80dbd) * chore: Update branch name (cherry picked from commit cc1bdd426b188d450fac9f7604ad6e9b52696d59) * chore: block major releases (cherry picked from commit c12a36aed90c82794a1092d1f5a1d9ff25d40040) * ci: use latest ubuntu container (cherry picked from commit 6fc11cb4c57264574cd64ccf588feae88b1236e5) * chore: do not publish any assets (cherry picked from commit e0a9a69d764c015ea6be287b31686ebb764a7067) Co-authored-by: Deepesh Garg Co-authored-by: Ankush Menat --- .github/workflows/release.yml | 25 +++++++++++++++++++++++++ .releaserc | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .releaserc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..532485f21f9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Generate Semantic Release +on: + push: + branches: + - version-13 +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js v14 + uses: actions/setup-node@v2 + with: + node-version: 14 + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec --no-save + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 00000000000..8a758ed30a6 --- /dev/null +++ b/.releaserc @@ -0,0 +1,24 @@ +{ + "branches": ["version-13"], + "plugins": [ + "@semantic-release/commit-analyzer", { + "preset": "angular", + "releaseRules": [ + {"breaking": true, "release": false} + ] + }, + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", { + "prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py' + } + ], + [ + "@semantic-release/git", { + "assets": ["erpnext/__init__.py"], + "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +} \ No newline at end of file From bed9e09153e7869021e2dcb0949b6e48b65928f9 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Apr 2022 18:01:48 +0530 Subject: [PATCH 875/951] fix: Query filter fields from Website Item instead of Item master - tweak `filters.py` to correctly query filter field values from Website Item - Use Website Item for filter field options in Settings and Item Group Field Filter table --- .../e_commerce_settings/e_commerce_settings.js | 4 ++-- .../e_commerce/product_data_engine/filters.py | 16 +++++++++++----- erpnext/setup/doctype/item_group/item_group.js | 10 +++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js index 1084a75630e..a8966b07a79 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js +++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js @@ -31,10 +31,10 @@ frappe.ui.form.on("E Commerce Settings", { df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden ).map(df => ({ label: df.label, value: df.fieldname })); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'fieldtype', 'Select' ); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'options', valid_fields ); }); diff --git a/erpnext/e_commerce/product_data_engine/filters.py b/erpnext/e_commerce/product_data_engine/filters.py index 3ff1ab726c1..73d51f6281c 100644 --- a/erpnext/e_commerce/product_data_engine/filters.py +++ b/erpnext/e_commerce/product_data_engine/filters.py @@ -22,12 +22,14 @@ class ProductFiltersBuilder: fields, filter_data = [], [] filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings - # filter valid field filters i.e. those that exist in Item - item_meta = frappe.get_meta("Item", cached=True) - fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] + # filter valid field filters i.e. those that exist in Website Item + web_item_meta = frappe.get_meta("Website Item", cached=True) + fields = [ + web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field) + ] for df in fields: - item_filters, item_or_filters = {"published_in_website": 1}, [] + item_filters, item_or_filters = {"published": 1}, [] link_doctype_values = self.get_filtered_link_doctype_records(df) if df.fieldtype == "Link": @@ -50,9 +52,13 @@ class ProductFiltersBuilder: ] ) + # exclude variants if mentioned in settings + if frappe.db.get_single_value("E Commerce Settings", "hide_variants"): + item_filters["variant_of"] = ["is", "not set"] + # Get link field values attached to published items item_values = frappe.get_all( - "Item", + "Website Item", fields=[df.fieldname], filters=item_filters, or_filters=item_or_filters, diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js index f570c2faec6..cf96dc1a7d6 100644 --- a/erpnext/setup/doctype/item_group/item_group.js +++ b/erpnext/setup/doctype/item_group/item_group.js @@ -72,17 +72,17 @@ frappe.ui.form.on("Item Group", { }); } - frappe.model.with_doctype('Item', () => { - const item_meta = frappe.get_meta('Item'); + frappe.model.with_doctype('Website Item', () => { + const web_item_meta = frappe.get_meta('Website Item'); - const valid_fields = item_meta.fields.filter( + const valid_fields = web_item_meta.fields.filter( df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden ).map(df => ({ label: df.label, value: df.fieldname })); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'fieldtype', 'Select' ); - frm.fields_dict.filter_fields.grid.update_docfield_property( + frm.get_field("filter_fields").grid.update_docfield_property( 'fieldname', 'options', valid_fields ); }); From 34437a83df710e77090a21115fcea9c88fea5bc8 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Apr 2022 18:50:58 +0530 Subject: [PATCH 876/951] fix: Validate field filter wrt to Website Item & re-use validation in Item Group --- .../e_commerce_settings.py | 19 ++++++++++--------- .../setup/doctype/item_group/item_group.py | 2 ++ 2 files changed, 12 insertions(+), 9 deletions(-) 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 a0003436efc..bd7ac9cdb7a 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 @@ -26,7 +26,7 @@ class ECommerceSettings(Document): self.is_redisearch_loaded = is_search_module_loaded() def validate(self): - self.validate_field_filters() + self.validate_field_filters(self.filter_fields, self.enable_field_filters) self.validate_attribute_filters() self.validate_checkout() self.validate_search_index_fields() @@ -50,21 +50,22 @@ class ECommerceSettings(Document): define_autocomplete_dictionary() create_website_items_index() - def validate_field_filters(self): - if not (self.enable_field_filters and self.filter_fields): + @staticmethod + def validate_field_filters(filter_fields, enable_field_filters): + if not (enable_field_filters and filter_fields): return - item_meta = frappe.get_meta("Website Item") + web_item_meta = frappe.get_meta("Website Item") valid_fields = [ - df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] + df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] ] - for f in self.filter_fields: - if f.fieldname not in valid_fields: + for row in filter_fields: + if row.fieldname not in valid_fields: frappe.throw( _( - "Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'" - ).format(f.idx, f.fieldname) + "Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'" + ).format(row.idx, frappe.bold(row.fieldname)) ) def validate_attribute_filters(self): diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 890b18c37a9..769b2d88085 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -12,6 +12,7 @@ from frappe.website.render import clear_cache from frappe.website.website_generator import WebsiteGenerator from six.moves.urllib.parse import quote +from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ECommerceSettings from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder @@ -36,6 +37,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): self.make_route() self.validate_item_group_defaults() + ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True) def on_update(self): NestedSet.on_update(self) From f9d89c7ce6a877602d9a94b1b4496dd1a594db21 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Apr 2022 18:34:47 +0530 Subject: [PATCH 877/951] fix: SO analysis rpt will fetch SO's without Delivery note as well (cherry picked from commit e28e6726f17954ba292a57e0c90d4b28a143f5e9) # Conflicts: # erpnext/selling/report/sales_order_analysis/sales_order_analysis.py --- .../report/sales_order_analysis/sales_order_analysis.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 465bb2d7452..b284ffc0e05 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -77,7 +77,15 @@ def get_data(conditions, filters): `tabSales Order` so, `tabSales Order Item` soi LEFT JOIN `tabSales Invoice Item` sii +<<<<<<< HEAD ON sii.so_detail = soi.name and sii.docstatus = 1 +======= + ON sii.so_detail = soi.name and sii.docstatus = 1) + LEFT JOIN `tabDelivery Note Item` dni + on dni.so_detail = soi.name + LEFT JOIN `tabDelivery Note` dn + on dni.parent = dn.name and dn.docstatus = 1 +>>>>>>> e28e6726f1 (fix: SO analysis rpt will fetch SO's without Delivery note as well) WHERE soi.parent = so.name and so.status not in ('Stopped', 'Closed', 'On Hold') From 61ff1c22b366d167d56973ab12c50c3085a98339 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 16 Apr 2022 10:25:44 +0530 Subject: [PATCH 878/951] test: Sales order analysis report (cherry picked from commit 13487e240880306f2e5341eaba4dc350e1e97783) --- .../test_sales_order_analysis.py | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py new file mode 100644 index 00000000000..25cbb734499 --- /dev/null +++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py @@ -0,0 +1,166 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days + +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note, make_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.selling.report.sales_order_analysis.sales_order_analysis import execute +from erpnext.stock.doctype.item.test_item import create_item + +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"] + + +class TestSalesOrderAnalysis(FrappeTestCase): + def create_sales_order(self, transaction_date): + item = create_item(item_code="_Test Excavator", is_stock_item=0) + so = make_sales_order( + transaction_date=transaction_date, + item=item.item_code, + qty=10, + rate=100000, + do_not_save=True, + ) + so.po_no = "" + so.taxes_and_charges = "" + so.taxes = "" + so.items[0].delivery_date = add_days(transaction_date, 15) + so.save() + so.submit() + return item, so + + def create_sales_invoice(self, so): + sinv = make_sales_invoice(so.name) + sinv.posting_date = so.transaction_date + sinv.taxes_and_charges = "" + sinv.taxes = "" + sinv.insert() + sinv.submit() + return sinv + + def create_delivery_note(self, so): + dn = make_delivery_note(so.name) + dn.set_posting_time = True + dn.posting_date = add_days(so.transaction_date, 1) + dn.save() + dn.submit() + return dn + + def test_01_so_to_deliver_and_bill(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["To Deliver and Bill"], + } + ) + expected_value = { + "status": "To Deliver and Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 0, + "pending_qty": 10, + "qty_to_bill": 10, + "time_taken_to_deliver": 0, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_02_so_to_deliver(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + self.create_sales_invoice(so) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["To Deliver"], + } + ) + expected_value = { + "status": "To Deliver", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 0, + "pending_qty": 10, + "qty_to_bill": 0, + "time_taken_to_deliver": 0, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_03_so_to_bill(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + self.create_delivery_note(so) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["To Bill"], + } + ) + expected_value = { + "status": "To Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 10, + "pending_qty": 0, + "qty_to_bill": 10, + "time_taken_to_deliver": 86400, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_04_so_completed(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + self.create_sales_invoice(so) + self.create_delivery_note(so) + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "status": ["Completed"], + } + ) + expected_value = { + "status": "Completed", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 10, + "pending_qty": 0, + "qty_to_bill": 0, + "billed_qty": 10, + "time_taken_to_deliver": 86400, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_05_all_so_status(self): + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + } + ) + # SO's from first 4 test cases should be in output + self.assertEqual(len(data), 4) From 9506dbe43385f7e80b9d772a29de304814c9b51b Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Apr 2022 21:38:22 +0530 Subject: [PATCH 879/951] chore: Patch to copy custom fields (field filters) from Item to Website Item --- erpnext/patches.txt | 1 + ...py_custom_field_filters_to_website_item.py | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b2d0871a17c..5d836fc2d72 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -359,3 +359,4 @@ erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.set_return_against_in_pos_invoice_references +erpnext.patches.v13_0.copy_custom_field_filters_to_website_item diff --git a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py new file mode 100644 index 00000000000..5f2125144fe --- /dev/null +++ b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py @@ -0,0 +1,54 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + "Add Field Filters, that are not standard fields in Website Item, as Custom Fields." + settings = frappe.get_doc("E Commerce Settings") + + if not (settings.filter_fields or settings.field_filters): + return + + item_meta = frappe.get_meta("Item") + valid_item_fields = [ + df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] + ] + + web_item_meta = frappe.get_meta("Website Item") + valid_web_item_fields = [ + df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] + ] + + for row in settings.filter_fields: + # skip if illegal field + if row.fieldname not in valid_item_fields: + continue + + # if Item field is not in Website Item, add it as a custom field + if row.fieldname not in valid_web_item_fields: + df = item_meta.get_field(row.fieldname) + create_custom_field( + "Website Item", + dict( + owner="Administrator", + fieldname=df.fieldname, + label=df.label, + fieldtype=df.fieldtype, + options=df.options, + description=df.description, + read_only=df.read_only, + no_copy=df.no_copy, + insert_after="on_backorder", + ), + ) + + # map field values + frappe.db.sql( + """ + UPDATE `tabWebsite Item` wi, `tabItem` i + SET wi.{0} = i.{0} + WHERE wi.item_code = i.item_code + """.format( + row.fieldname + ) + ) From 38002e3fe265c91664c31a9dc4ab8fac9cb01631 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Apr 2022 12:39:28 +0530 Subject: [PATCH 880/951] fix(patch): check if column is present while fixing reverse linking (backport #30737) (#30739) Co-authored-by: Rucha Mahabal --- ...itional_salary_encashment_and_incentive.py | 98 ++++++++++--------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py index edd0a9706b9..45acf49205b 100644 --- a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py +++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py @@ -10,54 +10,58 @@ def execute(): frappe.reload_doc("hr", "doctype", "Leave Encashment") - additional_salaries = frappe.get_all( - "Additional Salary", - fields=["name", "salary_slip", "type", "salary_component"], - filters={"salary_slip": ["!=", ""]}, - group_by="salary_slip", - ) - leave_encashments = frappe.get_all( - "Leave Encashment", - fields=["name", "additional_salary"], - filters={"additional_salary": ["!=", ""]}, - ) - employee_incentives = frappe.get_all( - "Employee Incentive", - fields=["name", "additional_salary"], - filters={"additional_salary": ["!=", ""]}, - ) - - for incentive in employee_incentives: - frappe.db.sql( - """ UPDATE `tabAdditional Salary` - SET ref_doctype = 'Employee Incentive', ref_docname = %s - WHERE name = %s - """, - (incentive["name"], incentive["additional_salary"]), + if frappe.db.has_column("Leave Encashment", "additional_salary"): + leave_encashments = frappe.get_all( + "Leave Encashment", + fields=["name", "additional_salary"], + filters={"additional_salary": ["!=", ""]}, ) - - for leave_encashment in leave_encashments: - frappe.db.sql( - """ UPDATE `tabAdditional Salary` - SET ref_doctype = 'Leave Encashment', ref_docname = %s - WHERE name = %s - """, - (leave_encashment["name"], leave_encashment["additional_salary"]), - ) - - salary_slips = [sal["salary_slip"] for sal in additional_salaries] - - for salary in additional_salaries: - comp_type = "earnings" if salary["type"] == "Earning" else "deductions" - if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: + for leave_encashment in leave_encashments: frappe.db.sql( - """ - UPDATE `tabSalary Detail` - SET additional_salary = %s - WHERE parenttype = 'Salary Slip' - and parentfield = %s - and parent = %s - and salary_component = %s + """ UPDATE `tabAdditional Salary` + SET ref_doctype = 'Leave Encashment', ref_docname = %s + WHERE name = %s """, - (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]), + (leave_encashment["name"], leave_encashment["additional_salary"]), ) + + if frappe.db.has_column("Employee Incentive", "additional_salary"): + employee_incentives = frappe.get_all( + "Employee Incentive", + fields=["name", "additional_salary"], + filters={"additional_salary": ["!=", ""]}, + ) + + for incentive in employee_incentives: + frappe.db.sql( + """ UPDATE `tabAdditional Salary` + SET ref_doctype = 'Employee Incentive', ref_docname = %s + WHERE name = %s + """, + (incentive["name"], incentive["additional_salary"]), + ) + + if frappe.db.has_column("Additional Salary", "salary_slip"): + additional_salaries = frappe.get_all( + "Additional Salary", + fields=["name", "salary_slip", "type", "salary_component"], + filters={"salary_slip": ["!=", ""]}, + group_by="salary_slip", + ) + + salary_slips = [sal["salary_slip"] for sal in additional_salaries] + + for salary in additional_salaries: + comp_type = "earnings" if salary["type"] == "Earning" else "deductions" + if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: + frappe.db.sql( + """ + UPDATE `tabSalary Detail` + SET additional_salary = %s + WHERE parenttype = 'Salary Slip' + and parentfield = %s + and parent = %s + and salary_component = %s + """, + (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"]), + ) From baab3797ca1b151862d4deb388a85a33f3bbcbd3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 19 Apr 2022 15:28:56 +0530 Subject: [PATCH 881/951] fix: Update token to allow updates on protected branch (cherry picked from commit 6f332f3669d81dd08828077ba91334450531dcfa) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 532485f21f9..32ea02b1d71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,5 +21,5 @@ jobs: npm install @semantic-release/git @semantic-release/exec --no-save - name: Create Release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: npx semantic-release \ No newline at end of file From 2a29aefb1935f33aa16273a2924fc38a553f7f84 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 19 Apr 2022 15:28:56 +0530 Subject: [PATCH 882/951] fix: Update token to allow updates on protected branch (cherry picked from commit 6f332f3669d81dd08828077ba91334450531dcfa) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 532485f21f9..32ea02b1d71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,5 +21,5 @@ jobs: npm install @semantic-release/git @semantic-release/exec --no-save - name: Create Release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: npx semantic-release \ No newline at end of file From f266d765fdf1183cb7b1b126105cce838a83ddbe Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 19 Apr 2022 15:58:49 +0530 Subject: [PATCH 883/951] chore: add GH token --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32ea02b1d71..9a021ce5c63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,5 +21,6 @@ jobs: npm install @semantic-release/git @semantic-release/exec --no-save - name: Create Release env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - run: npx semantic-release \ No newline at end of file + run: npx semantic-release From 4389100fa1ec134eb121de275d88d0f1c9d1ec59 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 19 Apr 2022 10:46:00 +0000 Subject: [PATCH 884/951] chore(release): Bumped to Version 13.27.0 # [13.27.0](https://github.com/frappe/erpnext/compare/v13.26.0...v13.27.0) (2022-04-19) ### Bug Fixes * Depreciation Amount calculation for first row when Monthly Depreciation is allowed ([#30692](https://github.com/frappe/erpnext/issues/30692)) ([9cf790d](https://github.com/frappe/erpnext/commit/9cf790db9d81e809e34fd2c6ec8c3f63b29e3de2)) * Do not show disabled dimensions in reports ([46d773d](https://github.com/frappe/erpnext/commit/46d773df5dc6547319a162f78917566d4c4e39ce)) * Exchange gain and loss on advance jv allocation ([9c5f841](https://github.com/frappe/erpnext/commit/9c5f8415bf1bd29bf34d32f49ad4c0b84ed68a6e)) * Map correct company to PO made via Prod Plan (subcontract) ([44be67e](https://github.com/frappe/erpnext/commit/44be67e6ddf7556b6b929b6381675b7919d1c074)) * Map Production Plan company in subassembly WO created from it ([f604101](https://github.com/frappe/erpnext/commit/f604101feabaeb1766bcb3d0376d22352f124c64)) * **patch:** check if column is present while fixing reverse linking (backport [#30737](https://github.com/frappe/erpnext/issues/30737)) ([#30739](https://github.com/frappe/erpnext/issues/30739)) ([38002e3](https://github.com/frappe/erpnext/commit/38002e3fe265c91664c31a9dc4ab8fac9cb01631)) * Payment reco query with max invocie and payment amount limit ([4008c95](https://github.com/frappe/erpnext/commit/4008c95ac6afc3b06a0a602e3f469db61e1f0453)) * Price changing on creating Sales retrun from Delivery Note ([93fe840](https://github.com/frappe/erpnext/commit/93fe840844081c60ce88d8fc1621fe260463657a)) * process statement to_date override ([378d15d](https://github.com/frappe/erpnext/commit/378d15d388ae9cac7a5f897af83c99605403503a)) * Remove "Values Out of Sync" validation (backport [#30707](https://github.com/frappe/erpnext/issues/30707)) ([#30718](https://github.com/frappe/erpnext/issues/30718)) ([fb9d640](https://github.com/frappe/erpnext/commit/fb9d640d8b161ccdee091f1ce9838a32b30ea23e)) * Update token to allow updates on protected branch ([2a29aef](https://github.com/frappe/erpnext/commit/2a29aefb1935f33aa16273a2924fc38a553f7f84)) * update translation ([#30716](https://github.com/frappe/erpnext/issues/30716)) ([fb127da](https://github.com/frappe/erpnext/commit/fb127da489786658d68ee5f3be888623d2f5c38b)) ### Features * Item-wise provisional accounting for service items ([5776881](https://github.com/frappe/erpnext/commit/5776881f34d8cc44dae6f05cdb87a010843dcf96)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index b34176ed34a..42fdb0df5df 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,7 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = "13.26.0" +__version__ = "13.27.0" def get_default_company(user=None): """Get default company for user""" From 6291b28c37f02380001ab68b3a660b2804383ab2 Mon Sep 17 00:00:00 2001 From: "FinByz Tech Pvt. Ltd" Date: Tue, 19 Apr 2022 16:26:56 +0530 Subject: [PATCH 885/951] fix(india): transporter name is null while generating e-way bill (#30736) --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 2b5d17710eb..0036a2bf721 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -1073,7 +1073,7 @@ class GSPConnector: "Distance": cint(eway_bill_details.distance), "TransMode": eway_bill_details.mode_of_transport, "TransId": eway_bill_details.gstin, - "TransName": eway_bill_details.transporter, + "TransName": eway_bill_details.name, "TrnDocDt": eway_bill_details.document_date, "TrnDocNo": eway_bill_details.document_name, "VehNo": eway_bill_details.vehicle_no, From 77e8c542dd7ae4c4e6cdf8bd5b8b0a1fbb3ea51d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Apr 2022 17:33:15 +0530 Subject: [PATCH 886/951] chore: Update creds to allow updates on protected branch (#30749) (#30750) (cherry picked from commit e4265ce814bb417ff5acd79e1bedae1b6a6d617b) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32ea02b1d71..5a46002820c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + persist-credentials: false - name: Setup Node.js v14 uses: actions/setup-node@v2 with: @@ -21,5 +22,10 @@ jobs: npm install @semantic-release/git @semantic-release/exec --no-save - name: Create Release env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + GIT_AUTHOR_NAME: "Frappe PR Bot" + GIT_AUTHOR_EMAIL: "developers@frappe.io" + GIT_COMMITTER_NAME: "Frappe PR Bot" + GIT_COMMITTER_EMAIL: "developers@frappe.io" run: npx semantic-release \ No newline at end of file From e76220e819bceed03f8fc0a5618f583a8c172faf Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 20 Apr 2022 12:27:04 +0530 Subject: [PATCH 887/951] fix: Mistyped variable name in patch --- .../patches/v13_0/copy_custom_field_filters_to_website_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py index 5f2125144fe..3e7e52a401d 100644 --- a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py +++ b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py @@ -6,7 +6,7 @@ def execute(): "Add Field Filters, that are not standard fields in Website Item, as Custom Fields." settings = frappe.get_doc("E Commerce Settings") - if not (settings.filter_fields or settings.field_filters): + if not (settings.enable_field_filters or settings.filter_fields): return item_meta = frappe.get_meta("Item") From 0b4e3f1467662b4ad86ac9f101cd95620aaa1c26 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 20 Apr 2022 08:59:45 +0200 Subject: [PATCH 888/951] fix: monthly attendance sheet (#30748) --- .../monthly_attendance_sheet.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py index e3cb36e0daf..c6f5bf05891 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -57,11 +57,10 @@ def execute(filters=None): data = [] - leave_list = None + leave_types = None if filters.summarized_view: - leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) - leave_list = [d[0] + ":Float:120" for d in leave_types] - columns.extend(leave_list) + leave_types = frappe.get_all("Leave Type", pluck="name") + columns.extend([leave_type + ":Float:120" for leave_type in leave_types]) columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"]) if filters.group_by: @@ -81,13 +80,19 @@ def execute(filters=None): holiday_map, conditions, default_holiday_list, - leave_list=leave_list, + leave_types=leave_types, ) emp_att_map.update(emp_att_data) data += record else: record, emp_att_map = add_data( - emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list + emp_map, + att_map, + filters, + holiday_map, + conditions, + default_holiday_list, + leave_types=leave_types, ) data += record @@ -104,12 +109,10 @@ def get_chart_data(emp_att_map, days): {"name": "Leave", "values": []}, ] for idx, day in enumerate(days, start=0): - p = day.replace("::65", "") labels.append(day.replace("::65", "")) total_absent_on_day = 0 total_leave_on_day = 0 total_present_on_day = 0 - total_holiday = 0 for emp in emp_att_map.keys(): if emp_att_map[emp][idx]: if emp_att_map[emp][idx] == "A": @@ -134,9 +137,8 @@ def get_chart_data(emp_att_map, days): def add_data( - employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=None + employee_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_types=None ): - record = [] emp_att_map = {} for emp in employee_map: @@ -222,7 +224,7 @@ def add_data( else: leaves[d.leave_type] = d.count - for d in leave_list: + for d in leave_types: if d in leaves: row.append(leaves[d]) else: From b656ffa45e2494b0a4e64ad8284a7da85c386899 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Apr 2022 15:11:11 +0530 Subject: [PATCH 889/951] fix: Must not be able to start Job Card if it is related to Work Order that is not started yet (#29072) (#30755) * fix: Cannot start Job strat if related to Work Order not started yet * fix: Cannot start Job strat if related to Work Order not started yet * test * test * fix siders * PR review * chore: Code cleanup - Better short circuit for if condition (make it such that both conditions dont always have to be computed) - Remove `r.message` extraction by avoiding `then()` * chore: Remove unnecessary json change Co-authored-by: marination (cherry picked from commit 143786aaa077d2fb29d54836dcb8424ca247a8e8) Co-authored-by: HENRY Florian --- erpnext/manufacturing/doctype/job_card/job_card.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index c4541fa68e6..20bbbeaa8ea 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -73,7 +73,18 @@ frappe.ui.form.on('Job Card', { if (frm.doc.docstatus == 0 && !frm.is_new() && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { - frm.trigger("prepare_timer_buttons"); + + // if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started" + // and if stock mvt for WIP is required + if (frm.doc.work_order) { + frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => { + if (result.skip_transfer === 1 || result.status == 'In Process') { + frm.trigger("prepare_timer_buttons"); + } + }); + } else { + frm.trigger("prepare_timer_buttons"); + } } frm.trigger("setup_quality_inspection"); From dc2f6945475e53bb4dfb20edd1d4cf6c2c940671 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 20 Apr 2022 17:34:51 +0530 Subject: [PATCH 890/951] fix: Handle Multiselect field mapping separately - Map Multiselect child table to Website Item (copy rows) --- erpnext/patches.txt | 2 +- ...py_custom_field_filters_to_website_item.py | 56 ++++++++++++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5d836fc2d72..841c59b78a2 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -359,4 +359,4 @@ erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.set_return_against_in_pos_invoice_references -erpnext.patches.v13_0.copy_custom_field_filters_to_website_item +erpnext.patches.v13_0.copy_custom_field_filters_to_website_item \ No newline at end of file diff --git a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py index 3e7e52a401d..e8d0b593e6f 100644 --- a/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py +++ b/erpnext/patches/v13_0/copy_custom_field_filters_to_website_item.py @@ -4,6 +4,43 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field def execute(): "Add Field Filters, that are not standard fields in Website Item, as Custom Fields." + + def move_table_multiselect_data(docfield): + "Copy child table data (Table Multiselect) from Item to Website Item for a docfield." + table_multiselect_data = get_table_multiselect_data(docfield) + field = docfield.fieldname + + for row in table_multiselect_data: + # add copied multiselect data rows in Website Item + web_item = frappe.db.get_value("Website Item", {"item_code": row.parent}) + web_item_doc = frappe.get_doc("Website Item", web_item) + + child_doc = frappe.new_doc(docfield.options, web_item_doc, field) + + for field in ["name", "creation", "modified", "idx"]: + row[field] = None + + child_doc.update(row) + + child_doc.parenttype = "Website Item" + child_doc.parent = web_item + + child_doc.insert() + + def get_table_multiselect_data(docfield): + child_table = frappe.qb.DocType(docfield.options) + item = frappe.qb.DocType("Item") + + table_multiselect_data = ( # query table data for field + frappe.qb.from_(child_table) + .join(item) + .on(item.item_code == child_table.parent) + .select(child_table.star) + .where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1)) + ).run(as_dict=True) + + return table_multiselect_data + settings = frappe.get_doc("E Commerce Settings") if not (settings.enable_field_filters or settings.filter_fields): @@ -43,12 +80,15 @@ def execute(): ) # map field values - frappe.db.sql( - """ - UPDATE `tabWebsite Item` wi, `tabItem` i - SET wi.{0} = i.{0} - WHERE wi.item_code = i.item_code - """.format( - row.fieldname + if df.fieldtype == "Table MultiSelect": + move_table_multiselect_data(df) + else: + frappe.db.sql( # nosemgrep + """ + UPDATE `tabWebsite Item` wi, `tabItem` i + SET wi.{0} = i.{0} + WHERE wi.item_code = i.item_code + """.format( + row.fieldname + ) ) - ) From ba635145da5e4069da564764f8c853ed2622d402 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 20 Apr 2022 18:50:47 +0530 Subject: [PATCH 891/951] test: Field filter validation and Custom field as field filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test to block Item fields (which aren’t in Website Item) in E Commerce Settings as filters - Removed unnecessary function and setup in E Commerce Settings test - Removed commented useless test - Test to check custom field as filter --- .../test_e_commerce_settings.py | 44 +++++++---------- .../test_product_data_engine.py | 48 +++++++++++++++++++ 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py index b159135012e..2c3428190e8 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import unittest @@ -11,42 +11,32 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ( class TestECommerceSettings(unittest.TestCase): - def setUp(self): - frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """) - - def get_cart_settings(self): - return frappe.get_doc({"doctype": "E Commerce Settings", "company": "_Test Company"}) - - # NOTE: Exchangrate API has all enabled currencies that ERPNext supports. - # We aren't checking just currency exchange record anymore - # while validating price list currency exchange rate to that of company. - # The API is being used to fetch the rate which again almost always - # gives back a valid value (for valid currencies). - # This makes the test obsolete. - # Commenting because im not sure if there's a better test we can write - - # def test_exchange_rate_exists(self): - # frappe.db.sql("""delete from `tabCurrency Exchange`""") - - # cart_settings = self.get_cart_settings() - # cart_settings.price_list = "_Test Price List Rest of the World" - # self.assertRaises(ShoppingCartSetupError, cart_settings.validate_price_list_exchange_rate) - - # from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records as \ - # currency_exchange_records - # frappe.get_doc(currency_exchange_records[0]).insert() - # cart_settings.validate_price_list_exchange_rate() + def tearDown(self): + frappe.db.rollback() def test_tax_rule_validation(self): frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") - cart_settings = self.get_cart_settings() + cart_settings = frappe.get_doc("E Commerce Settings") cart_settings.enabled = 1 if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") + def test_invalid_filter_fields(self): + "Check if Item fields are blocked in E Commerce Settings filter fields." + from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + create_custom_field( + "Item", + dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"), + ) + settings = frappe.get_doc("E Commerce Settings") + settings.append("filter_fields", {"fieldname": "test_data"}) + + self.assertRaises(frappe.ValidationError, settings.save) + def setup_e_commerce_settings(values_dict): "Accepts a dict of values that updates E Commerce Settings." diff --git a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py index ab958d14866..c3b6ed5da25 100644 --- a/erpnext/e_commerce/product_data_engine/test_product_data_engine.py +++ b/erpnext/e_commerce/product_data_engine/test_product_data_engine.py @@ -277,6 +277,54 @@ class TestProductDataEngine(unittest.TestCase): # tear down setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0}) + def test_custom_field_as_filter(self): + "Test if custom field functions as filter correctly." + from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + create_custom_field( + "Website Item", + dict( + owner="Administrator", + fieldname="supplier", + label="Supplier", + fieldtype="Link", + options="Supplier", + insert_after="on_backorder", + ), + ) + + frappe.db.set_value( + "Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier" + ) + frappe.db.set_value( + "Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1" + ) + + settings = frappe.get_doc("E Commerce Settings") + settings.append("filter_fields", {"fieldname": "supplier"}) + settings.save() + + filter_engine = ProductFiltersBuilder() + field_filters = filter_engine.get_field_filters() + custom_filter = field_filters[1] + filter_values = custom_filter[1] + + self.assertEqual(custom_filter[0].options, "Supplier") + self.assertEqual(len(filter_values), 2) + self.assertIn("_Test Supplier", filter_values) + + # test if custom filter works in query + field_filters = {"supplier": "_Test Supplier 1"} + engine = ProductQuery() + result = engine.query( + attributes={}, fields=field_filters, search_term=None, start=0, item_group=None + ) + items = result.get("items") + + # check if only 'Raw Material' are fetched in the right order + self.assertEqual(len(items), 1) + self.assertEqual(items[0].get("item_code"), "Test 12I Laptop") + def create_variant_web_item(): "Create Variant and Template Website Items." From 269e1923c9edf144224c3b81584ec626df359d8c Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Wed, 20 Apr 2022 19:44:09 +0530 Subject: [PATCH 892/951] fix: Use right precision for asset value after full schedule (#30745) --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 21d90b5e48a..c255348a2bf 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -628,7 +628,7 @@ class Asset(AccountsController): asset_value_after_full_schedule = flt( flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule), - self.precision("gross_purchase_amount"), + row.precision("expected_value_after_useful_life"), ) if ( From f58d3b4d3dc5de06ea85a24e5ebe8f778a192195 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 21 Apr 2022 12:29:30 +0530 Subject: [PATCH 893/951] test: setup e commerce settings before running invalid filtrs test --- .../doctype/e_commerce_settings/test_e_commerce_settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py index 2c3428190e8..9372f80252f 100644 --- a/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py +++ b/erpnext/e_commerce/doctype/e_commerce_settings/test_e_commerce_settings.py @@ -28,6 +28,8 @@ class TestECommerceSettings(unittest.TestCase): "Check if Item fields are blocked in E Commerce Settings filter fields." from frappe.custom.doctype.custom_field.custom_field import create_custom_field + setup_e_commerce_settings({"enable_field_filters": 1}) + create_custom_field( "Item", dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"), From c339305e9c4c6752ff6b952e6d1827a46d199211 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 16:39:24 +0530 Subject: [PATCH 894/951] fix: batch_no filtering not working when batch no is also a number in scientific notation (#30770) (#30771) [skip ci] (cherry picked from commit ee3036651a8bf84a6666577c325ce6f3157a0d5b) Co-authored-by: Ankush Menat --- erpnext/stock/doctype/serial_no/serial_no.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 6d3969892f2..cfa5cee453a 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -802,11 +802,11 @@ def auto_fetch_serial_number( exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos))) if batch_nos: - batch_nos = safe_json_loads(batch_nos) - if isinstance(batch_nos, list): - filters.batch_no = batch_nos + batch_nos_list = safe_json_loads(batch_nos) + if isinstance(batch_nos_list, list): + filters.batch_no = batch_nos_list else: - filters.batch_no = [str(batch_nos)] + filters.batch_no = [batch_nos] if posting_date: filters.expiry_date = posting_date From 32cff94bde0e6c7b56df5ae71783b2189ae73ec7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 21 Apr 2022 18:31:35 +0530 Subject: [PATCH 895/951] chore: Remove conflicts from Sales Order Analysis report (#30761) * fix: Remove conflicts from Sales Order Analysis report * fix: change field to duration and fetch elapsed seconds * chore: Ignore linting check for sql query --- .../sales_order_analysis/sales_order_analysis.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index b284ffc0e05..609fe26d869 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -54,6 +54,7 @@ def get_conditions(filters): def get_data(conditions, filters): + # nosemgrep data = frappe.db.sql( """ SELECT @@ -65,6 +66,7 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, + IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver, IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, @@ -75,17 +77,13 @@ def get_data(conditions, filters): soi.description as description FROM `tabSales Order` so, - `tabSales Order Item` soi + (`tabSales Order Item` soi LEFT JOIN `tabSales Invoice Item` sii -<<<<<<< HEAD - ON sii.so_detail = soi.name and sii.docstatus = 1 -======= ON sii.so_detail = soi.name and sii.docstatus = 1) LEFT JOIN `tabDelivery Note Item` dni on dni.so_detail = soi.name LEFT JOIN `tabDelivery Note` dn on dni.parent = dn.name and dn.docstatus = 1 ->>>>>>> e28e6726f1 (fix: SO analysis rpt will fetch SO's without Delivery note as well) WHERE soi.parent = so.name and so.status not in ('Stopped', 'Closed', 'On Hold') @@ -272,6 +270,12 @@ def get_columns(filters): }, {"label": _("Delivery Date"), "fieldname": "delivery_date", "fieldtype": "Date", "width": 120}, {"label": _("Delay (in Days)"), "fieldname": "delay", "fieldtype": "Data", "width": 100}, + { + "label": _("Time Taken to Deliver"), + "fieldname": "time_taken_to_deliver", + "fieldtype": "Duration", + "width": 100, + }, ] ) if not filters.get("group_by_so"): From 198bdcfdc609e9a6280408f86471cedbc454da90 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 20 Apr 2022 14:14:29 +0530 Subject: [PATCH 896/951] fix(india): 401 & 403 client error while generating IRN (cherry picked from commit ee8047aba320a1fb36d2203bd117ed7cb4f9a4ba) # Conflicts: # erpnext/regional/india/e_invoice/utils.py --- erpnext/regional/india/e_invoice/utils.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 0036a2bf721..16f63c621b1 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -12,7 +12,11 @@ import traceback import frappe import jwt +<<<<<<< HEAD import six +======= +import requests +>>>>>>> ee8047aba3 (fix(india): 401 & 403 client error while generating IRN) from frappe import _, bold from frappe.core.page.background_jobs.background_jobs import get_info from frappe.integrations.utils import make_get_request, make_post_request @@ -828,14 +832,25 @@ class GSPConnector: return self.e_invoice_settings.auth_token def make_request(self, request_type, url, headers=None, data=None): - if request_type == "post": - res = make_post_request(url, headers=headers, data=data) - else: - res = make_get_request(url, headers=headers, data=data) + try: + if request_type == "post": + res = make_post_request(url, headers=headers, data=data) + else: + res = make_get_request(url, headers=headers, data=data) + + except requests.exceptions.HTTPError as e: + if e.response.status_code in [401, 403] and not hasattr(self, "token_auto_refreshed"): + self.auto_refresh_token() + headers = self.get_headers() + return self.make_request(request_type, url, headers, data) self.log_request(url, headers, data, res) return res + def auto_refresh_token(self): + self.fetch_auth_token() + self.token_auto_refreshed = True + def log_request(self, url, headers, data, res): headers.update({"password": self.credentials.password}) request_log = frappe.get_doc( From a6d093859171d1f987f06467c58de973312ead12 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Apr 2022 19:27:23 +0530 Subject: [PATCH 897/951] fix: dependent gle reposting (backport #30726) (#30772) * refactor: repost error handling (cherry picked from commit afc5a55a2308214e9c4a4ced94b99bfc5bcd8ec5) * test: dependent GL entry reposting (cherry picked from commit a2af2daca707a1b599d2d90b4e1ab8e5bb958403) # Conflicts: # erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py * fix: dependent GLE reposting (cherry picked from commit ecdb49314f2134f42afc7487464bb1e5107c8246) # Conflicts: # erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json # erpnext/stock/stock_ledger.py * test: repost queue progress (cherry picked from commit 8f519545b0b9dbf48415a49ec03ac3011d41c041) * test: use disposable item codes in tests dependency causes flake (cherry picked from commit d2882ea436d35997ccbc422403ff8a25d8eb57b5) * fix: correct sorting while updating bin (cherry picked from commit b24920c0e9fa642f823132d61e817f2331523aa4) # Conflicts: # erpnext/stock/doctype/bin/bin.py * fix: sort stock vouchers before reposting GLE (cherry picked from commit 700e864d901d4730cea2c0fc82f2b642b7c24acc) * test: discard local future SLE cache between tests (cherry picked from commit 9734329094b5275d52d08b1d8edc9eb5d980aa9d) * chore: conflicts Co-authored-by: Ankush Menat --- erpnext/accounts/test/test_utils.py | 28 ++++++++- erpnext/accounts/utils.py | 27 +++++++++ erpnext/stock/doctype/bin/bin.py | 27 +++++---- .../repost_item_valuation.json | 13 +++- .../repost_item_valuation.py | 57 ++++++++++++------ .../test_repost_item_valuation.py | 7 +++ .../test_stock_ledger_entry.py | 59 +++++++++++++++++++ .../test_stock_reconciliation.py | 15 ++--- erpnext/stock/stock_ledger.py | 35 ++++++++--- 9 files changed, 218 insertions(+), 50 deletions(-) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 0fe50083045..77c40bae2d9 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -1,10 +1,17 @@ import unittest +import frappe from frappe.test_runner import make_test_objects from erpnext.accounts.party import get_party_shipping_address -from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries +from erpnext.accounts.utils import ( + get_future_stock_vouchers, + get_voucherwise_gl_entries, + sort_stock_vouchers_by_posting_date, +) +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestUtils(unittest.TestCase): @@ -47,6 +54,25 @@ class TestUtils(unittest.TestCase): msg="get_voucherwise_gl_entries not returning expected GLes", ) + def test_stock_voucher_sorting(self): + vouchers = [] + + item = make_item().name + + stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10} + + se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry) + se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry) + se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry) + + for doc in (se1, se2, se3): + vouchers.append((doc.doctype, doc.name)) + + vouchers.append(("Stock Entry", "Wat")) + + sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers))) + self.assertEqual(sorted_vouchers, vouchers) + ADDRESS_RECORDS = [ { diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0d1d0dc031f..0bf2939336a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -3,6 +3,7 @@ from json import loads +from typing import List, Tuple import frappe import frappe.defaults @@ -1123,6 +1124,9 @@ def update_gl_entries_after( def repost_gle_for_stock_vouchers( stock_vouchers, posting_date, company=None, warehouse_account=None ): + if not stock_vouchers: + return + def _delete_gl_entries(voucher_type, voucher_no): frappe.db.sql( """delete from `tabGL Entry` @@ -1130,6 +1134,8 @@ def repost_gle_for_stock_vouchers( (voucher_type, voucher_no), ) + stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers) + if not warehouse_account: warehouse_account = get_warehouse_account_map(company) @@ -1150,6 +1156,27 @@ def repost_gle_for_stock_vouchers( _delete_gl_entries(voucher_type, voucher_no) +def sort_stock_vouchers_by_posting_date( + stock_vouchers: List[Tuple[str, str]] +) -> List[Tuple[str, str]]: + sle = frappe.qb.DocType("Stock Ledger Entry") + voucher_nos = [v[1] for v in stock_vouchers] + + sles = ( + frappe.qb.from_(sle) + .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) + .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) + .groupby(sle.voucher_type, sle.voucher_no) + ).run(as_dict=True) + sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] + + unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers) + if unknown_vouchers: + sorted_vouchers.extend(unknown_vouchers) + + return sorted_vouchers + + def get_future_stock_vouchers( posting_date, posting_time, for_warehouses=None, for_items=None, company=None ): diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 4e49ac800eb..573203a47a8 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -4,6 +4,8 @@ import frappe from frappe.model.document import Document +from frappe.query_builder import Order +from frappe.query_builder.functions import CombineDatetime from frappe.utils import flt @@ -134,24 +136,23 @@ def update_qty(bin_name, args): bin_details = get_bin_details(bin_name) # actual qty is already updated by processing current voucher - actual_qty = bin_details.actual_qty + actual_qty = bin_details.actual_qty or 0.0 + sle = frappe.qb.DocType("Stock Ledger Entry") # actual qty is not up to date in case of backdated transaction if future_sle_exists(args): - actual_qty = ( - frappe.db.get_value( - "Stock Ledger Entry", - filters={ - "item_code": args.get("item_code"), - "warehouse": args.get("warehouse"), - "is_cancelled": 0, - }, - fieldname="qty_after_transaction", - order_by="posting_date desc, posting_time desc, creation desc", - ) - or 0.0 + last_sle_qty = ( + frappe.qb.from_(sle) + .select(sle.qty_after_transaction) + .where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse"))) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc) + .orderby(sle.creation, order=Order.desc) + .run() ) + if last_sle_qty: + actual_qty = last_sle_qty[0][0] + ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty")) indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty")) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 0ba97d59a14..8c13149252a 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -23,6 +23,7 @@ "error_section", "error_log", "items_to_be_repost", + "affected_transactions", "distinct_item_and_warehouse", "current_index" ], @@ -172,12 +173,20 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "affected_transactions", + "fieldtype": "Code", + "hidden": 1, + "label": "Affected Transactions", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-01-18 10:57:33.450907", + "modified": "2022-04-18 14:08:08.821602", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", @@ -229,4 +238,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} 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 173b9aa21e5..b788fd1286b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -6,11 +6,14 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime from frappe.utils.user import get_users_with_role -from rq.timeouts import JobTimeoutException import erpnext -from erpnext.accounts.utils import update_gl_entries_after -from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle +from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers +from erpnext.stock.stock_ledger import ( + get_affected_transactions, + get_items_to_be_repost, + repost_future_sle, +) class RepostItemValuation(Document): @@ -129,12 +132,12 @@ def repost(doc): doc.set_status("Completed") - except (Exception, JobTimeoutException): + except Exception: frappe.db.rollback() traceback = frappe.get_traceback() frappe.log_error(traceback) - message = frappe.message_log.pop() + message = frappe.message_log.pop() if frappe.message_log else "" if traceback: message += "
    " + "Traceback:
    " + traceback frappe.db.set_value(doc.doctype, doc.name, "error_log", message) @@ -170,6 +173,7 @@ def repost_sl_entries(doc): ], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher, + doc=doc, ) @@ -177,27 +181,46 @@ def repost_gl_entries(doc): if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)): return + # directly modified transactions + directly_dependent_transactions = _get_directly_dependent_vouchers(doc) + repost_affected_transaction = get_affected_transactions(doc) + repost_gle_for_stock_vouchers( + directly_dependent_transactions + list(repost_affected_transaction), + doc.posting_date, + doc.company, + ) + + +def _get_directly_dependent_vouchers(doc): + """Get stock vouchers that are directly affected by reposting + i.e. any one item-warehouse is present in the stock transaction""" + + items = set() + warehouses = set() + if doc.based_on == "Transaction": ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() + items.update(doc_items) + warehouses.update(doc_warehouses) sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no) - sle_items = [sle.item_code for sle in sles] - sle_warehouse = [sle.warehouse for sle in sles] - - items = list(set(doc_items).union(set(sle_items))) - warehouses = list(set(doc_warehouses).union(set(sle_warehouse))) + sle_items = {sle.item_code for sle in sles} + sle_warehouses = {sle.warehouse for sle in sles} + items.update(sle_items) + warehouses.update(sle_warehouses) else: - items = [doc.item_code] - warehouses = [doc.warehouse] + items.add(doc.item_code) + warehouses.add(doc.warehouse) - update_gl_entries_after( - doc.posting_date, - doc.posting_time, - for_warehouses=warehouses, - for_items=items, + affected_vouchers = get_future_stock_vouchers( + posting_date=doc.posting_date, + posting_time=doc.posting_time, + for_warehouses=list(warehouses), + for_items=list(items), company=doc.company, ) + return affected_vouchers def notify_error_to_stock_managers(doc, traceback): 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 55117ceb2e3..3184f69aa45 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 @@ -186,3 +186,10 @@ class TestRepostItemValuation(FrappeTestCase): riv.db_set("status", "Skipped") riv.reload() riv.cancel() # it should cancel now + + def test_queue_progress_serialization(self): + # Make sure set/tuple -> list behaviour is retained. + self.assertEqual( + [["a", "b"], ["c", "d"]], + sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))), + ) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index eee6a3fb9ec..74775b98e39 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -8,6 +8,7 @@ from frappe.core.page.permission_manager.permission_manager import reset from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today +from frappe.utils.data import add_to_date from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note @@ -624,6 +625,64 @@ class TestStockLedgerEntry(FrappeTestCase): receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15) self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}]) + def test_dependent_gl_entry_reposting(self): + def _get_stock_credit(doc): + return frappe.db.get_value( + "GL Entry", + { + "voucher_no": doc.name, + "voucher_type": doc.doctype, + "is_cancelled": 0, + "account": "Stock In Hand - TCP1", + }, + "sum(credit)", + ) + + def _day(days): + return add_to_date(date=today(), days=days) + + item = make_item().name + A = "Stores - TCP1" + B = "Work In Progress - TCP1" + C = "Finished Goods - TCP1" + + make_stock_entry(item_code=item, to_warehouse=A, qty=5, rate=10, posting_date=_day(0)) + make_stock_entry(item_code=item, from_warehouse=A, to_warehouse=B, qty=5, posting_date=_day(1)) + depdendent_consumption = make_stock_entry( + item_code=item, from_warehouse=B, qty=5, posting_date=_day(2) + ) + self.assertEqual(50, _get_stock_credit(depdendent_consumption)) + + # backdated receipt - should trigger GL repost of all previous stock entries + bd_receipt = make_stock_entry( + item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1) + ) + self.assertEqual(100, _get_stock_credit(depdendent_consumption)) + + # cancelling receipt should reset it back + bd_receipt.cancel() + self.assertEqual(50, _get_stock_credit(depdendent_consumption)) + + bd_receipt2 = make_stock_entry( + item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2) + ) + # total as per FIFO -> 2 * 20 + 3 * 10 = 70 + self.assertEqual(70, _get_stock_credit(depdendent_consumption)) + + # transfer WIP material to final destination and consume it all + depdendent_consumption.cancel() + make_stock_entry(item_code=item, from_warehouse=B, to_warehouse=C, qty=5, posting_date=_day(3)) + final_consumption = make_stock_entry( + item_code=item, from_warehouse=C, qty=5, posting_date=_day(4) + ) + # exact amount gets consumed + self.assertEqual(70, _get_stock_credit(final_consumption)) + + # cancel original backdated receipt - should repost A -> B -> C + bd_receipt2.cancel() + # original amount + self.assertEqual(50, _get_stock_credit(final_consumption)) + def create_repack_entry(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index f06771888f2..45e840322b1 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( @@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase): def tearDown(self): frappe.flags.dont_execute_stock_reposts = None + frappe.local.future_sle = {} def test_reco_for_fifo(self): self._test_reco_sle_gle("FIFO") @@ -310,9 +311,8 @@ class TestStockReconciliation(FrappeTestCase): SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] PR3 | PR | 1 | 7 (posting date: today) # can't post future PR """ - item_code = "Backdated-Reco-Item" + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) pr1 = make_purchase_receipt( item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) @@ -394,9 +394,8 @@ class TestStockReconciliation(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = "Backdated-Reco-Item" + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) pr1 = make_purchase_receipt( item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2) @@ -443,9 +442,8 @@ class TestStockReconciliation(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = "Backdated-Reco-Cancellation-Item" + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) sr = create_stock_reconciliation( item_code=item_code, @@ -486,9 +484,8 @@ class TestStockReconciliation(FrappeTestCase): frappe.flags.dont_execute_stock_reposts = True frappe.db.rollback() - item_code = "Backdated-Reco-Cancellation-Item" + item_code = make_item().name warehouse = "_Test Warehouse - _TC" - create_item(item_code) sr = create_stock_reconciliation( item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 597e2e28f1b..d2c10018ba5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -3,6 +3,7 @@ import copy import json +from typing import Set, Tuple import frappe from frappe import _ @@ -211,6 +212,7 @@ def repost_future_sle( args = get_items_to_be_repost(voucher_type, voucher_no, doc) distinct_item_warehouses = get_distinct_item_warehouse(args, doc) + affected_transactions = get_affected_transactions(doc) i = get_current_index(doc) or 0 while i < len(args): @@ -226,6 +228,7 @@ def repost_future_sle( allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, ) + affected_transactions.update(obj.affected_transactions) distinct_item_warehouses[ (args[i].get("item_code"), args[i].get("warehouse")) @@ -245,26 +248,32 @@ def repost_future_sle( i += 1 if doc and i % 2 == 0: - update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + update_args_in_repost_item_valuation( + doc, i, args, distinct_item_warehouses, affected_transactions + ) if doc and args: - update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) + update_args_in_repost_item_valuation( + doc, i, args, distinct_item_warehouses, affected_transactions + ) -def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): - frappe.db.set_value( - doc.doctype, - doc.name, +def update_args_in_repost_item_valuation( + doc, index, args, distinct_item_warehouses, affected_transactions +): + doc.db_set( { "items_to_be_repost": json.dumps(args, default=str), "distinct_item_and_warehouse": json.dumps( {str(k): v for k, v in distinct_item_warehouses.items()}, default=str ), "current_index": index, - }, + "affected_transactions": frappe.as_json(affected_transactions), + } ) - frappe.db.commit() + if not frappe.flags.in_test: + frappe.db.commit() frappe.publish_realtime( "item_reposting_progress", @@ -301,6 +310,14 @@ def get_distinct_item_warehouse(args=None, doc=None): return distinct_item_warehouses +def get_affected_transactions(doc) -> Set[Tuple[str, str]]: + if not doc.affected_transactions: + return set() + + transactions = frappe.parse_json(doc.affected_transactions) + return {tuple(transaction) for transaction in transactions} + + def get_current_index(doc=None): if doc and doc.current_index: return doc.current_index @@ -348,6 +365,7 @@ class update_entries_after(object): self.new_items_found = False self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) + self.affected_transactions: Set[Tuple[str, str]] = set() self.data = frappe._dict() self.initialize_previous_data(self.args) @@ -506,6 +524,7 @@ class update_entries_after(object): # previous sle data for this warehouse self.wh_data = self.data[sle.warehouse] + self.affected_transactions.add((sle.voucher_type, sle.voucher_no)) if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock): # validate negative stock for serialized items, fifo valuation From b585262842294d89d8b7e96c3cd3d15429b27009 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Apr 2022 11:18:29 +0530 Subject: [PATCH 898/951] fix: update translation (#30725) (#30776) * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv (cherry picked from commit e088e65871ee68ae142c6273b231f7a2aa7a87b6) Co-authored-by: Vladislav --- erpnext/translations/ru.csv | 1238 +++++++++++++++++------------------ 1 file changed, 619 insertions(+), 619 deletions(-) diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 7fcb7b08f75..6703da60634 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -2,7 +2,7 @@ """Customer Provided Item"" cannot have Valuation Rate",«Предоставленный клиентом товар» не может иметь оценку, """Is Fixed Asset"" cannot be unchecked, as Asset record exists against the item","Нельзя отменить выбор ""Является основным средством"", поскольку по данному пункту имеется запись по активам", 'Based On' and 'Group By' can not be same,"""На основании"" и ""Группировка по"" не могут быть одинаковыми", -'Days Since Last Order' must be greater than or equal to zero,"""Дней с последнего Заказа"" должно быть больше или равно 0", +'Days Since Last Order' must be greater than or equal to zero,"""Дней с момента последнего заказа"" должно быть больше или равно 0", 'Entries' cannot be empty,"""Записи"" не могут быть пустыми", 'From Date' is required,"Поле ""С даты"" является обязательным для заполнения", 'From Date' must be after 'To Date',"Поле ""С даты"" должно быть после ""До даты""", @@ -70,20 +70,20 @@ Account {0}: Parent account {1} does not exist,Счет {0}: Родитель с Account {0}: You can not assign itself as parent account,Счёт {0}: Вы не можете назначить самого себя родительским счётом, Account: {0} can only be updated via Stock Transactions,Счет: {0} можно обновить только через перемещение по складу, Account: {0} with currency: {1} can not be selected,Счет: {0} с валютой: {1} не может быть выбран, -Accountant,бухгалтер, +Accountant,Бухгалтер, Accounting,Бухгалтерия, Accounting Entry for Asset,Учетная запись для активов, Accounting Entry for Stock,Бухгалтерская Проводка по Запасам, Accounting Entry for {0}: {1} can only be made in currency: {2},Бухгалтерская Проводка для {0}: {1} может быть сделана только в валюте: {2}, -Accounting Ledger,Главная книга, +Accounting Ledger,Бухгалтерская книга, Accounting journal entries.,Журнал бухгалтерских записей., Accounts,Счета, -Accounts Manager,Диспетчер учетных записей, +Accounts Manager,Диспетчер счетов, Accounts Payable,Счета к оплате, Accounts Payable Summary,Сводка кредиторской задолженности, Accounts Receivable,Дебиторская задолженность, Accounts Receivable Summary,Сводка дебиторской задолженности, -Accounts User,Пользователь Учетных записей, +Accounts User,Пользователь Счетов, Accounts table cannot be blank.,Таблица учета не может быть пустой., Accrual Journal Entry for salaries from {0} to {1},Запись журнала начислений для зарплат от {0} до {1}, Accumulated Depreciation,начисленной амортизации, @@ -115,7 +115,7 @@ Add Customers,Добавить клиентов, Add Employees,Добавить сотрудников, Add Item,Добавить продукт, Add Items,Добавить продукты, -Add Leads,Добавить Обращения, +Add Leads,Добавить лид, Add Multiple Tasks,Добавить несколько задач, Add Row,Добавить ряд, Add Sales Partners,Добавить партнеров по продажам, @@ -125,7 +125,7 @@ Add Suppliers,Добавить поставщиков, Add Time Slots,Добавление временных интервалов, Add Timesheets,Добавить табели, Add Timeslots,Добавить таймслоты, -Add Users to Marketplace,Добавить пользователей на рынок, +Add Users to Marketplace,Добавить пользователей на торговую площадку, Add a new address,добавить новый адрес, Add cards or custom sections on homepage,Добавить карты или пользовательские разделы на главной странице, Add more items or open full form,Добавить ещё продукты или открыть полную форму, @@ -144,8 +144,8 @@ Address Title,Название адреса, Address Type,Тип адреса, Administrative Expenses,Административные затраты, Administrative Officer,Администратор, -Administrator,администратор, -Admission,вход, +Administrator,Администратор, +Admission,Допуск, Admission and Enrollment,Прием и зачисление, Admissions for {0},Поступающим для {0}, Admit,Допустить, @@ -154,9 +154,9 @@ Advance Amount,Предварительная сумма, Advance Payments,Авансовые платежи, Advance account currency should be same as company currency {0},"Валюта авансового счета должна быть такой же, как и валюта компании {0}", Advance amount cannot be greater than {0} {1},"Предварительная сумма не может быть больше, чем {0} {1}", -Advertising,реклама, +Advertising,Реклама, Aerospace,авиационно-космический, -Against,против, +Against,Против, Against Account,Со счета, Against Journal Entry {0} does not have any unmatched {1} entry,Против Запись в журнале {0} не имеет никакого непревзойденную {1} запись, Against Journal Entry {0} is already adjusted against some other voucher,Против Запись в журнале {0} уже настроен против какой-либо другой ваучер, @@ -200,7 +200,7 @@ Allocated Leaves,Выделенные листы, Allocating leaves...,Выделенные разрешения, Already record exists for the item {0},Уже существует запись для элемента {0}, "Already set default in pos profile {0} for user {1}, kindly disabled default","Уже задан по умолчанию в pos-профиле {0} для пользователя {1}, любезно отключен по умолчанию", -Alternate Item,Альтернативный товар, +Alternate Item,Альтернативный продукт, Alternative item must not be same as item code,"Альтернативный элемент не должен быть таким же, как код позиции", Amended From,Измененный С, Amount,Сумма, @@ -232,7 +232,7 @@ Applicable For,Применимо для, "Applicable if the company is SpA, SApA or SRL","Применимо, если компания SpA, SApA или SRL", Applicable if the company is a limited liability company,"Применимо, если компания является обществом с ограниченной ответственностью", Applicable if the company is an Individual or a Proprietorship,"Применимо, если компания является частным лицом или собственником", -Applicant,заявитель, +Applicant,Заявитель, Applicant Type,Тип заявителя, Application of Funds (Assets),Применение средств (активов), Application period cannot be across two allocation records,Период применения не может быть через две записи распределения, @@ -295,11 +295,11 @@ Associate,Помощник, At least one mode of payment is required for POS invoice.,По крайней мере один способ оплаты требуется для POS счета., Atleast one item should be entered with negative quantity in return document,Как минимум один продукт должен быть введен с отрицательным количеством в возвратном документе, Atleast one of the Selling or Buying must be selected,По крайней мере один из продажи или покупки должен быть выбран, -Atleast one warehouse is mandatory,"По крайней мере, один склад является обязательным", +Atleast one warehouse is mandatory,"По крайней мере, один склад обязателен", Attach Logo,Прикрепить логотип, Attachment,Вложение, Attachments,Приложения, -Attendance,посещаемость, +Attendance,Посещаемость, Attendance From Date and Attendance To Date is mandatory,"""Начало учетного периода"" и ""Конец учетного периода"" обязательны к заполнению", Attendance can not be marked for future dates,Посещаемость не могут быть отмечены для будущих дат, Attendance date can not be less than employee's joining date,"Дата Посещаемость не может быть меньше, чем присоединение даты работника", @@ -310,13 +310,13 @@ Attendance not submitted for {0} as it is a Holiday.,"Посещение не о Attendance not submitted for {0} as {1} on leave.,Посещаемость не была отправлена {0} как {1} в отпуске., Attribute table is mandatory,Таблица атрибутов является обязательной, Attribute {0} selected multiple times in Attributes Table,Атрибут {0} выбран несколько раз в таблице атрибутов, -Author,автор, +Author,Автор, Authorized Signatory,Право подписи, Auto Material Requests Generated,"Запросы Авто материал, полученный", Auto Repeat,Автоматическое повторение, Auto repeat document updated,Автоматический повторный документ обновлен, Automotive,Автомобилестроение, -Available,имеется, +Available,Доступно, Available Leaves,Доступные листья, Available Qty,Доступное количество, Available Selling,Доступные продажи, @@ -329,7 +329,7 @@ Average Rate,Средняя оценка, Avg Daily Outgoing,Среднедневные исходящие, Avg. Buying Price List Rate,Avg. Цена прайс-листа, Avg. Selling Price List Rate,Avg. Цена прайс-листа, -Avg. Selling Rate,Средняя Цена Продажи, +Avg. Selling Rate,Средняя цена продажи, BOM,ВМ, BOM Browser,Браузер ВМ, BOM No,ВМ №, @@ -369,39 +369,39 @@ Base,База, Base URL,Базовый URL, Based On,На основании, Based On Payment Terms,На основании условий оплаты, -Basic,основной, -Batch,партия, +Basic,Основной, +Batch,Партия, Batch Entries,Пакетные записи, Batch ID is mandatory,Идентификатор партии является обязательным, Batch Inventory,Пакетная Инвентарь, Batch Name,Наименование партии, -Batch No,№ партии, +Batch No,Партия №, Batch number is mandatory for Item {0},Номер партии является обязательным для продукта {0}, Batch {0} of Item {1} has expired.,Партия {0} продукта {1} просрочена, Batch {0} of Item {1} is disabled.,Пакет {0} элемента {1} отключен., -Batch: ,Batch:, -Batches,Порции, +Batch: ,Партия: , +Batches,Партии, Become a Seller,Стать продавцом, -Beginner,начинающий, -Bill,Билл, -Bill Date,Дата оплаты, -Bill No,Номер накладной, +Beginner,Начинающий, +Bill,Счет, +Bill Date,Дата выставления счета, +Bill No,Номер счета, Bill of Materials,Ведомость материалов, Bill of Materials (BOM),Ведомость материалов (ВМ), Billable Hours,Оплачиваемые часы, -Billed,Выдавать счета, -Billed Amount,Счетов выдано количество, +Billed,Выставлен счет, +Billed Amount,Количество выставленных счетов, Billing,Выставление счетов, Billing Address,Адрес для выставления счетов, Billing Address is same as Shipping Address,Платежный адрес совпадает с адресом доставки, -Billing Amount,Биллинг Сумма, +Billing Amount,Количество счетов, Billing Status,Статус оплаты, Billing currency must be equal to either default company's currency or party account currency,Валюта платежа должна быть равна валюте валюты дефолта или валюте счета участника, -Bills raised by Suppliers.,Платежи Поставщикам, -Bills raised to Customers.,Платежи Заказчиков, +Bills raised by Suppliers.,Счета выставленные поставщиками, +Bills raised to Customers.,Счета выставленные клиентам, Biotechnology,Биотехнологии, Birthday Reminder,День рождения, -Black,черный, +Black,Черный, Blanket Orders from Costumers.,Заказы на одеяла от клиентов., Block Invoice,Блок-счет, Boms,Boms, @@ -414,13 +414,13 @@ Brokerage,Посредничество, Browse BOM,Просмотр спецификации, Budget Against,Бюджет против, Budget List,Бюджетный список, -Budget Variance Report,Бюджет Разница Сообщить, +Budget Variance Report,Отчет об отклонении бюджета, Budget cannot be assigned against Group Account {0},Бюджет не может быть назначен на учетную запись группы {0}, "Budget cannot be assigned against {0}, as it's not an Income or Expense account","Бюджет не может быть назначен на {0}, так как это не доход или расход счета", -Buildings,здания, +Buildings,Здания, Bundle items at time of sale.,Собирать продукты в момент продажи., Business Development Manager,Менеджер по развитию бизнеса, -Buy,купить, +Buy,Купить, Buying,Покупки, Buying Amount,Сумма покупки, Buying Price List,Ценовой список покупок, @@ -437,7 +437,7 @@ CRM,CRM, CWIP Account,CWIP-аккаунт, Calculated Bank Statement balance,Расчетный банк себе баланс, Calls,Звонки, -Campaign,кампания, +Campaign,Кампания, Can be approved by {0},Может быть одобрено {0}, "Can not filter based on Account, if grouped by Account","Не можете фильтровать на основе счета, если сгруппированы по Счет", "Can not filter based on Voucher No, if grouped by Voucher","Не можете фильтровать на основе ваучером Нет, если сгруппированы по ваучером", @@ -451,10 +451,10 @@ Cancel Material Visit {0} before cancelling this Warranty Claim,Отменить Cancel Material Visits {0} before cancelling this Maintenance Visit,Отменить Материал просмотров {0} до отмены этого обслуживания визит, Cancel Subscription,Отменить подписку, Cancel the journal entry {0} first,Сначала отменить запись журнала {0}, -Canceled,отменен, +Canceled,Отменен, "Cannot Submit, Employees left to mark attendance","Не могу отправить, Сотрудники оставили отмечать посещаемость", Cannot be a fixed asset item as Stock Ledger is created.,"Не может быть элементом фиксированного актива, так как создается складская книга.", -Cannot cancel because submitted Stock Entry {0} exists,"Нельзя отменить, так как проведена учетная запись по Запасам {0}", +Cannot cancel because submitted Stock Entry {0} exists,"Нельзя отменить, так как проведен счет по Запасам {0}", Cannot cancel transaction for Completed Work Order.,Невозможно отменить транзакцию для выполненного рабочего заказа., Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3},"Невозможно отменить {0} {1}, поскольку Serial No {2} не относится к складу {3}", Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,Невозможно изменить атрибуты после транзакции с акциями. Сделайте новый предмет и переведите запас на новый элемент, @@ -489,33 +489,33 @@ Cannot {0} {1} {2} without any negative outstanding invoice,Может не {0} Capital Equipments,Капитальные оборудование, Capital Stock,Капитал, Capital Work in Progress,Капитальная работа в процессе, -Cart,Тележка, +Cart,Корзина, Cart is Empty,Корзина Пусто, Case No(s) already in use. Try from Case No {0},Случай Нет (ы) уже используется. Попробуйте из дела № {0}, Cash,Наличные, Cash Flow Statement,О движении денежных средств, Cash Flow from Financing,Поток денежных средств от финансовой, -Cash Flow from Investing,Поток денежных средств от инвестиционной, +Cash Flow from Investing,Поток денежных средств от инвестиций, Cash Flow from Operations,Поток денежных средств от операций, -Cash In Hand,Наличность кассы, +Cash In Hand,Наличные на руках, Cash or Bank Account is mandatory for making payment entry,Наличными или банковский счет является обязательным для внесения записи платежей, Cashier Closing,Закрытие кассы, Casual Leave,Повседневная Оставить, Category,Категория, Category Name,Название категории, -Caution,предосторожность, +Caution,Предосторожность, Central Tax,Центральный налог, -Certification,сертификация, -Cess,налог, -Change Amount,Изменение Сумма, -Change Item Code,Изменить код товара, +Certification,Сертификация, +Cess,Налог, +Change Amount,Изменить сумму, +Change Item Code,Изменить код продукта, Change Release Date,Изменить дату выпуска, Change Template Code,Изменить шаблонный код, Changing Customer Group for the selected Customer is not allowed.,Изменение группы клиентов для выбранного Клиента запрещено., -Chapter,глава, +Chapter,Глава, Chapter information.,Информация о главе., Charge of type 'Actual' in row {0} cannot be included in Item Rate,Начисление типа «Актуальные 'в строке {0} не могут быть включены в пункт Оценить, -Chargeble,Chargeble, +Chargeble,Платный, Charges are updated in Purchase Receipt against each item,Расходы обновляются в приобретении получение против каждого пункта, "Charges will be distributed proportionately based on item qty or amount, as per your selection","Расходы будут распределяться пропорционально на основе количества или суммы продукта, согласно вашему выбору", Chart of Cost Centers,План МВЗ, @@ -536,10 +536,10 @@ Claimed Amount,Заявленная сумма, Clay,глина, Clear filters,Очистить фильтры, Clear values,Очистить значения, -Clearance Date,Клиренс Дата, -Clearance Date not mentioned,Клиренс Дата не упоминается, -Clearance Date updated,Зазор Дата обновления, -Client,клиент, +Clearance Date,Дата оформления, +Clearance Date not mentioned,Дата оформления не упоминается, +Clearance Date updated,Дата оформления обновлена, +Client,Клиент, Client ID,ID клиента, Client Secret,Секрет клиента, Clinical Procedure,Клиническая процедура, @@ -556,7 +556,7 @@ Closing Account {0} must be of type Liability / Equity,Закрытие счет Closing Balance,Конечное сальдо, Code,Код, Collapse All,Свернуть все, -Color,цвет, +Color,Цвет, Colour,Цвет, Combined invoice portion must equal 100%,Комбинированная часть счета должна равняться 100%, Commercial,Коммерческий сектор, @@ -576,11 +576,11 @@ Company name not same,Название компании не одинаково, Company {0} does not exist,Компания {0} не существует, Compensatory Off,Компенсационные Выкл, Compensatory leave request days not in valid holidays,Дни запроса на получение компенсационных отчислений не действительны, -Complaint,жалоба, +Complaint,Жалоба, Completion Date,Дата завершения, Computer,компьютер, Condition,Условия, -Configure,конфигурировать, +Configure,Конфигурировать, Configure {0},Настроить {0}, Confirmed orders from Customers.,Подтвержденные заказы от клиентов., Connect Amazon with ERPNext,Подключить Amazon к ERPNext, @@ -589,13 +589,13 @@ Connect to Quickbooks,Подключение к Quickbooks, Connected to QuickBooks,Подключено к QuickBooks, Connecting to QuickBooks,Подключение к QuickBooks, Consultation,Консультация, -Consultations,консультации, -Consulting,консалтинг, -Consumable,потребляемый, +Consultations,Консультации, +Consulting,Консалтинг, +Consumable,Потребляемый, Consumed,Потребляемый, Consumed Amount,Израсходованное количество, -Consumed Qty,Потребляемая Кол-во, -Consumer Products,Потребительские товары, +Consumed Qty,Потребляемое кол-во, +Consumer Products,Потребительские продукты, Contact,Контакты, Contact Details,Контактная информация, Contact Number,Контактный номер, @@ -604,8 +604,8 @@ Content,Содержимое, Content Masters,Мастера контента, Content Type,Тип контента, Continue Configuration,Продолжить настройку, -Contract,контракт, -Contract End Date must be greater than Date of Joining,"Конец контракта Дата должна быть больше, чем дата вступления", +Contract,Договор, +Contract End Date must be greater than Date of Joining,"Дата окончания договора должна быть позже, чем дата его заключения", Contribution %,Вклад%, Contribution Amount,Вклад Сумма, Conversion factor for default Unit of Measure must be 1 in row {0},Коэффициент пересчета для дефолтного Единица измерения должна быть 1 в строке {0}, @@ -623,7 +623,7 @@ Cost Centers,Центр затрат, Cost Updated,Стоимость Обновлено, Cost as on,"Стоимость, как на", Cost of Delivered Items,Затраты по поставленным продуктам, -Cost of Goods Sold,Себестоимость проданного товара, +Cost of Goods Sold,Себестоимость проданных продуктов, Cost of Issued Items,Стоимость выпущенных продуктов, Cost of New Purchase,Стоимость новой покупки, Cost of Purchased Items,Стоимость поставленных продуктов, @@ -631,13 +631,13 @@ Cost of Scrapped Asset,Стоимость списанных активов, Cost of Sold Asset,Себестоимость проданных активов, Cost of various activities,Стоимость различных видов деятельности, "Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again","Не удалось создать кредитную ноту автоматически, снимите флажок «Выдавать кредитную ноту» и отправьте снова", -Could not generate Secret,Не удалось создать секрет, +Could not generate Secret,Не удалось сгенерировать секретный ключ, Could not retrieve information for {0}.,Не удалось получить информацию для {0}., Could not solve criteria score function for {0}. Make sure the formula is valid.,"Не удалось решить функцию оценки критериев для {0}. Убедитесь, что формула действительна.", Could not solve weighted score function. Make sure the formula is valid.,"Не удалось решить функцию взвешенного балла. Убедитесь, что формула действительна.", -Could not submit some Salary Slips,Не удалось подтвердить некоторые зарплатные листки, -"Could not update stock, invoice contains drop shipping item.","Не удалось обновить запас, счет-фактура содержит падение пункт доставки.", -Country wise default Address Templates,Шаблоны Страна мудрый адрес по умолчанию, +Could not submit some Salary Slips,Не удалось отправить некоторые зарплатные ведомости, +"Could not update stock, invoice contains drop shipping item.","Не удалось обновить запасы, счет-фактура содержит продукт прямой доставки.", +Country wise default Address Templates,Шаблоны адресов по умолчанию для разных стран, Course,Курс, Course Code: ,Код курса:, Course Enrollment {0} does not exists,Зачисление на курс {0} не существует, @@ -658,8 +658,8 @@ Create Invoice,Создать счет, Create Invoices,Создать счета, Create Job Card,Создать вакансию, Create Journal Entry,Создать запись в журнале, -Create Lead,Создать лидерство, -Create Leads,Создать Обращения, +Create Lead,Создать обращение, +Create Leads,Создать лид, Create Maintenance Visit,Создать техническое посещение, Create Material Request,Создать заявку на материал, Create Multiple,Создать несколько, @@ -667,13 +667,13 @@ Create Opening Sales and Purchase Invoices,Создание начальных Create Payment Entries,Создать платежные записи, Create Payment Entry,Создать платежную запись, Create Print Format,Создание Формат печати, -Create Purchase Order,Создать заказ на поставку, -Create Purchase Orders,Создание заказов на поставку, -Create Quotation,Создание цитаты, -Create Salary Slip,Создание Зарплата Слип, -Create Salary Slips,Создать зарплатные листки, +Create Purchase Order,Создать заявку на поставку, +Create Purchase Orders,Создание заявки на поставку, +Create Quotation,Создать предложение, +Create Salary Slip,Создать зарплатную ведомость, +Create Salary Slips,Создать зарплатные ведомости, Create Sales Invoice,Создать счет на продажу, -Create Sales Order,Создать Сделку, +Create Sales Order,Создать заявку на продажу, Create Sales Orders to help you plan your work and deliver on-time,"Создавайте заказы на продажу, чтобы помочь вам спланировать свою работу и выполнить ее в срок", Create Sample Retention Stock Entry,Создать образец записи для удержания запаса, Create Student,Создать ученика, @@ -696,19 +696,19 @@ Creating Payment Entries......,Создание платежных записе Creating Salary Slips...,Создание зарплатных листков..., Creating student groups,Создание групп студентов, Creating {0} Invoice,Создание {0} счета-фактуры, -Credit,кредит, +Credit,Кредит, Credit ({0}),Кредит ({0}), Credit Account,Кредитный счет, -Credit Balance,Остаток кредита, +Credit Balance,Кредитный баланс, Credit Card,Кредитная карта, Credit Days cannot be a negative number,Кредитные дни не могут быть отрицательным числом, Credit Limit,{0}{/0} {1}Кредитный лимит {/1}, -Credit Note,Кредит-нота, +Credit Note,Кредитная запись , Credit Note Amount,Сумма кредитной записи, Credit Note Issued,Кредит выдается справка, -Credit Note {0} has been created automatically,Кредитная нота {0} создана автоматически, +Credit Note {0} has been created automatically,Кредитная запись {0} была создана автоматически, Credit limit has been crossed for customer {0} ({1}/{2}),Кредитный лимит был скрещен для клиента {0} ({1} / {2}), -Creditors,кредиторы, +Creditors,Кредиторы, Criteria weights must add up to 100%,Критерии веса должны составлять до 100%, Crop Cycle,Цикл урожая, Crops & Lands,Сельскохозяйственные культуры и земли, @@ -725,15 +725,15 @@ Current Assets,Оборотные активы, Current BOM and New BOM can not be same,"Текущий спецификации и Нью-BOM не может быть таким же,", Current Job Openings,Текущие вакансии Вакансии, Current Liabilities,Текущие обязательства, -Current Qty,Текущий Кол-во, +Current Qty,Текущее количество, Current invoice {0} is missing,Текущий счет-фактура {0} отсутствует, -Custom HTML,Особый HTML, -Custom?,Пользовательские?, +Custom HTML,Пользовательский HTML, +Custom?,Пользовательский?, Customer,Клиент, Customer Addresses And Contacts,Адреса клиентов и Контакты, Customer Contact,Контакты с клиентами, Customer Database.,База данных клиентов., -Customer Group,Группа Клиентов, +Customer Group,Группа клиентов, Customer LPO,Клиент LPO, Customer LPO No.,Номер клиента LPO, Customer Name,Имя клиента, @@ -764,19 +764,19 @@ Date of Commencement should be greater than Date of Incorporation,"Дата на Date of Joining,Дата вступления, Date of Joining must be greater than Date of Birth,Дата Присоединение должно быть больше Дата рождения, Date of Transaction,Дата транзакции, -Datetime,Datetime, +Datetime,Дата и время, Day,День, Debit,Дебет, Debit ({0}),Дебет ({0}), Debit A/C Number,Дебетовый номер кондиционера, Debit Account,Дебетовый счет, -Debit Note,Дебет-нота, -Debit Note Amount,Сумма дебетовой ноты, -Debit Note Issued,Дебет Примечание Выпущенный, +Debit Note,Дебетовая запись, +Debit Note Amount,Сумма дебетовой записи, +Debit Note Issued,Дата дебетовой записи, Debit To is required,Дебет требуется, Debit and Credit not equal for {0} #{1}. Difference is {2}.,Дебет и Кредит не равны для {0} # {1}. Разница {2}., -Debtors,Должники, -Debtors ({0}),Должники ({0}), +Debtors,Дебеторы, +Debtors ({0}),Дебеторы ({0}), Declare Lost,Объявить потерянным, Deduction,Вычет, Default Activity Cost exists for Activity Type - {0},По умолчанию активность Стоимость существует для вида деятельности - {0}, @@ -816,67 +816,67 @@ Delivery warehouse required for stock item {0},Склад Доставка тр Department,Отдел, Department Stores,Универмаги, Depreciation,Амортизация, -Depreciation Amount,Амортизация основных средств Сумма, -Depreciation Amount during the period,Амортизация Сумма за период, -Depreciation Date,Износ Дата, +Depreciation Amount,Сумма амортизации основных средств, +Depreciation Amount during the period,Сумма амортизации за период, +Depreciation Date,Дата амортизации, Depreciation Eliminated due to disposal of assets,Амортизация Дошел вследствие выбытия активов, Depreciation Entry,Износ Вход, -Depreciation Method,метод начисления износа, +Depreciation Method,Метод начисления износа, Depreciation Row {0}: Depreciation Start Date is entered as past date,Строка амортизации {0}: дата начала амортизации вводится как прошедшая дата, Depreciation Row {0}: Expected value after useful life must be greater than or equal to {1},Строка амортизации {0}: ожидаемое значение после полезного срока службы должно быть больше или равно {1}, Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date,"Строка амортизации {0}: следующая дата амортизации не может быть до даты, доступной для использования", Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date,Строка амортизации {0}: следующая дата амортизации не может быть до даты покупки, -Designer,дизайнер, +Designer,Дизайнер, Detailed Reason,Подробная причина, Details,Подробности, Details of Outward Supplies and inward supplies liable to reverse charge,"Сведения о расходных материалах и расходных материалах, подлежащих возврату", Details of the operations carried out.,Информация о выполненных операциях., -Diagnosis,диагностика, +Diagnosis,Диагностика, Did not find any item called {0},Не нашли какой-либо пункт под названием {0}, Diff Qty,Diff Qty, -Difference Account,Учетная запись, +Difference Account,Разница счета, "Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry","Разница аккаунт должен быть тип счета активов / пассивов, так как это со Примирение запись Открытие", -Difference Amount,Разница Сумма, -Difference Amount must be zero,Разница Сумма должна быть равна нулю, +Difference Amount,Разница, +Difference Amount must be zero,Разница должна быть равна нулю, Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM.,"Различные единицы измерения (ЕИ) продуктов приведут к некорректному (общему) значению массы нетто. Убедитесь, что вес нетто каждого продукта находится в одной ЕИ.", Direct Expenses,Прямые расходы, Direct Income,Прямая прибыль, Disable,Отключить, -Disabled template must not be default template,Шаблон для инвалидов не должно быть по умолчанию шаблон, +Disabled template must not be default template,Отключенный шаблон не может быть шаблоном по умолчанию, Disburse Loan,Выдавать кредит, Disbursed,Освоено, Disc,диск, Discharge,разрядка, -Discount,скидка, +Discount,Скидка, Discount Percentage can be applied either against a Price List or for all Price List.,"Процент скидки может применяться либо к Прайс-листу, либо ко всем Прайс-листам.", Discount must be less than 100,Скидка должна быть меньше 100, Diseases & Fertilizers,Болезни и удобрения, -Dispatch,отправка, +Dispatch,Отправка, Dispatch Notification,Уведомление о рассылке, Dispatch State,Состояние отправки, Distance,Дистанция, -Distribution,распределение, -Distributor,дистрибьютор, +Distribution,Дистрибьюция, +Distributor,Дистрибьютор, Dividends Paid,Оплачено дивидендов, -Do you really want to restore this scrapped asset?,Вы действительно хотите восстановить этот актив на слом?, +Do you really want to restore this scrapped asset?,Вы действительно хотите восстановить этот списанный актив?, Do you really want to scrap this asset?,Вы действительно хотите отказаться от этого актива?, Do you want to notify all the customers by email?,Вы хотите уведомить всех клиентов по электронной почте?, Doc Date,Дата документа, Doc Name,Имя документа, Doc Type,Тип документа, Docs Search,Поиск документов, -Document Name,название документа, +Document Name,Название документа, Document Status,Статус документа, -Document Type,тип документа, -Domain,Домен, +Document Type,Тип документа, +Domain,Направление, Domains,Направление деятельности, -Done,Сделать, -Donor,даритель, +Done,Готово, +Donor,Донор, Donor Type information.,Информация о доноре., Donor information.,Донорская информация., Download JSON,Скачать JSON, -Draft,Проект, -Drop Ship,Корабль падения, +Draft,Черновик, +Drop Ship,Прямая поставка, Drug,Лекарство, Due / Reference Date cannot be after {0},Из-за / Reference Дата не может быть в течение {0}, Due Date cannot be before Posting / Supplier Invoice Date,Срок оплаты не может быть раньше даты публикации / выставления счета поставщику, @@ -885,7 +885,7 @@ Duplicate Entry. Please check Authorization Rule {0},"Копия записи. Duplicate Serial No entered for Item {0},Дубликат Серийный номер вводится для Пункт {0}, Duplicate customer group found in the cutomer group table,Дубликат группа клиентов найти в таблице Cutomer группы, Duplicate entry,Дублировать запись, -Duplicate item group found in the item group table,Повторяющаяся группа находке в таблице группы товаров, +Duplicate item group found in the item group table,Дубликат группы продуктов в таблице групп продуктов, Duplicate roll number for student {0},Повторяющийся номер ролика для ученика {0}, Duplicate row {0} with same {1},Дубликат строка {0} с же {1}, Duplicate {0} found in the table,Дубликат {0} найден в таблице, @@ -894,10 +894,10 @@ Duties and Taxes,Пошлины и налоги, E-Invoicing Information Missing,Отсутствует информация об инвойсировании, ERPNext Demo,ERPNext Demo, ERPNext Settings,Настройки ERPNext, -Earliest,Старейшие, +Earliest,Самый ранний, Earnest Money,Задаток, Earning,Зарабатывание, -Edit,редактировать, +Edit,Ред., Edit Publishing Details,Редактировать информацию о публикации, "Edit in full page for more options like assets, serial nos, batches etc.","Редактируйте на полной странице дополнительные параметры, такие как активы, серийные номера, партии и т. Д.", Education,образование, @@ -906,7 +906,7 @@ Either target qty or target amount is mandatory,Либо целевой Коли Either target qty or target amount is mandatory.,Либо целевой Количество или целевое количество является обязательным., Electrical,электрический, Electronic Equipments,Электронные приборы, -Electronics,электроника, +Electronics,Электроника, Eligible ITC,Соответствующий ITC, Email Account,Электронная почта, Email Address,Адрес электронной почты, @@ -918,14 +918,14 @@ Email Template,Шаблон электронной почты, Email not found in default contact,Адрес электронной почты не найден в контакте по умолчанию, Email sent to {0},Письмо отправлено на адрес {0}, Employee,Сотрудник, -Employee A/C Number,Номер A / C сотрудника, +Employee A/C Number,A/C номер сотрудника, Employee Advances,Достижения сотрудников, Employee Benefits,Вознаграждения работникам, Employee Grade,Уровень персонала, Employee ID,ID сотрудника, Employee Lifecycle,Жизненный цикл сотрудников, Employee Name,Имя сотрудника, -Employee Promotion cannot be submitted before Promotion Date ,Продвижение сотрудника не может быть отправлено до даты акции, +Employee Promotion cannot be submitted before Promotion Date ,Повышение сотрудника не может быть выполнено до даты приступления к должности, Employee Referral,Перечень сотрудников, Employee Transfer cannot be submitted before Transfer Date ,Передача сотрудника не может быть отправлена до даты передачи, Employee cannot report to himself.,Сотрудник не может сообщить себе., @@ -937,9 +937,9 @@ Employee {0} is not active or does not exist,Сотрудник {0} не акт Employee {0} is on Leave on {1},Сотрудник {0} отправляется в {1}, Employee {0} of grade {1} have no default leave policy,Сотрудник {0} класса {1} не имеет политики отпуска по умолчанию, Employee {0} on Half day on {1},Сотрудник {0} на полдня на {1}, -Enable,Автоматическое обновление, -Enable / disable currencies.,Включение / отключение валюты., -Enabled,Включено, +Enable, Разрешить, +Enable / disable currencies.,Разрешить / запретить валюты., +Enabled,Разрешено, "Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart","Включение "Использовать для Корзине», как Корзина включена и должно быть по крайней мере один налог Правило Корзина", End Date,Дата окончания, End Date can not be less than Start Date,Дата окончания не может быть меньше даты начала, @@ -950,8 +950,8 @@ End on,Заканчивается, End time cannot be before start time,Время окончания не может быть раньше времени начала, Ends On date cannot be before Next Contact Date.,Конец дата не может быть до следующей даты контакта., Energy,Энергоэффективность, -Engineer,инженер, -Enough Parts to Build,Достаточно части для сборки, +Engineer,Инженер, +Enough Parts to Build,Достаточно деталей для сборки, Enroll,зачислять, Enrolling student,поступив студент, Enrolling students,Регистрация студентов, @@ -966,9 +966,9 @@ Equity,Ценные бумаги, Error Log,Журнал ошибок, Error evaluating the criteria formula,Ошибка оценки формулы критериев, Error in formula or condition: {0},Ошибка в формуле или условие: {0}, -Error: Not a valid id?,Ошибка: Не действует ID?, +Error: Not a valid id?,Ошибка: Не действительный ID?, Estimated Cost,Ориентировочная стоимость, -Evaluation,оценка, +Evaluation,Оценка, "Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:","Даже если существует несколько правил ценообразования с наивысшим приоритетом, применяются следующие внутренние приоритеты:", Event,Событие, Event Location,Место проведения мероприятия, @@ -979,13 +979,13 @@ Exchange Rate must be same as {0} {1} ({2}),"Курс должен быть та Excise Invoice,Акцизный Счет, Execution,Реализация, Executive Search,Executive Search, -Expand All,Расширить все, +Expand All,Развернуть все, Expected Delivery Date,Ожидаемая дата доставки, Expected Delivery Date should be after Sales Order Date,Ожидаемая дата доставки должна быть после даты Сделки, Expected End Date,Ожидаемая дата завершения, Expected Hrs,Ожидаемые часы, Expected Start Date,Ожидаемая дата начала, -Expense,расходы, +Expense,Расходы, Expense / Difference account ({0}) must be a 'Profit or Loss' account,"Расходов / Разница счет ({0}) должен быть ""прибыль или убыток» счета", Expense Account,Расходов счета, Expense Claim,Заявка на возмещение, @@ -996,7 +996,7 @@ Expense account is mandatory for item {0},Расходов счета являе Expenses,Расходы, Expenses Included In Asset Valuation,"Расходы, включенные в оценку активов", Expenses Included In Valuation,"Затрат, включаемых в оценке", -Expired Batches,Истекшие партии, +Expired Batches,Просроченные партии, Expires On,Годен до, Expiring On,Срок действия, Expiry (In Days),Срок действия (в днях), @@ -1004,7 +1004,7 @@ Explore,Обзор, Export E-Invoices,Экспорт электронных счетов, Extra Large,Очень большой, Extra Small,Очень маленький, -Fail,Потерпеть неудачу, +Fail,Неудача, Failed,Не выполнено, Failed to create website,Не удалось создать веб-сайт, Failed to install presets,Не удалось установить пресеты, @@ -1013,7 +1013,7 @@ Failed to setup company,Не удалось настроить компанию, Failed to setup defaults,Не удалось установить значения по умолчанию, Failed to setup post company fixtures,Не удалось настроить оборудование для компании, Fax,Факс, -Fee,плата, +Fee,Оплата, Fee Created,Плата создана, Fee Creation Failed,Не удалось создать сбор, Fee Creation Pending,Платежное создание ожидается, @@ -1039,8 +1039,8 @@ Financial Statements,Финансовые отчеты, Financial Year,Финансовый год, Finish,Завершить, Finished Good,Готово Хорошо, -Finished Good Item Code,Готовый товарный код, -Finished Goods,Готовая продукция, +Finished Good Item Code,Код готовых продуктов, +Finished Goods,Готовые продукты, Finished Item {0} must be entered for Manufacture type entry,Готовая единица {0} должна быть введена для Производственного типа записи, Finished product quantity {0} and For Quantity {1} cannot be different,Количество готового продукта {0} и для количества {1} не может быть разным, First Name,Имя, @@ -1079,7 +1079,7 @@ For row {0}: Enter Planned Qty,Для строки {0}: введите запл Forum Activity,Активность в форуме, Free item code is not selected,Бесплатный код товара не выбран, Freight and Forwarding Charges,Грузовые и экспедиторские Сборы, -Frequency,частота, +Frequency,Частота, Friday,Пятница, From,От, From Address 1,Из адреса 1, @@ -1132,7 +1132,7 @@ Generate Material Requests (MRP) and Work Orders.,Создание запрос Generate Secret,Создать секрет, Get Details From Declaration,Получить детали из декларации, Get Employees,Получить сотрудников, -Get Invocies,Получить призывы, +Get Invocies,Получить счета, Get Invoices,Получить счета, Get Invoices based on Filters,Получить счета на основе фильтров, Get Items from BOM,Получить продукты из спецификации, @@ -1153,7 +1153,7 @@ GoCardless payment gateway settings,Настройки шлюза без пла Goal and Procedure,Цель и процедура, Goals cannot be empty,Цели не могут быть пустыми, Goods In Transit,Товары в пути, -Goods Transferred,Товар перенесен, +Goods Transferred,Товар передан, Goods and Services Tax (GST India),Налог на товары и услуги (GST India), Goods are already received against the outward entry {0},Товар уже получен против выездной записи {0}, Government,Правительство, @@ -1169,19 +1169,19 @@ Gross Profit %,Валовая Прибыль%, Gross Profit / Loss,Валовая прибыль / убыток, Gross Purchase Amount,Валовая сумма покупки, Gross Purchase Amount is mandatory,Валовая сумма покупки является обязательным, -Group by Account,Группа по Счет, -Group by Party,Группа по партии, -Group by Voucher,Группа по ваучером, -Group by Voucher (Consolidated),Группа по ваучеру (консолидировано), -Group node warehouse is not allowed to select for transactions,"склад группы узлов не допускается, чтобы выбрать для сделок", +Group by Account,Сгруппировать по счетам, +Group by Party,Сгруппировать по партии, +Group by Voucher,Сгруппировать по ваучеру, +Group by Voucher (Consolidated),Сгруппировать по ваучеру (консолидировано), +Group node warehouse is not allowed to select for transactions,Склад узла группы не может выбирать для транзакций, Group to Non-Group,Группа не-группы, Group your students in batches,Группа ваших студентов в партиях, -Groups,группы, +Groups,Группы, Guardian1 Email ID,Идентификатор электронной почты Guardian1, -Guardian1 Mobile No,Guardian1 Mobile Нет, +Guardian1 Mobile No,Guardian1 Mobile №, Guardian1 Name,Имя Guardian1, Guardian2 Email ID,Идентификатор электронной почты Guardian2, -Guardian2 Mobile No,Guardian2 Mobile Нет, +Guardian2 Mobile No,Guardian2 Mobile №, Guardian2 Name,Имя Guardian2, Guest,Гость, HR Manager,Менеджер отдела кадров, @@ -1189,8 +1189,8 @@ HSN,HSN, HSN/SAC,HSN / SAC, Half Day,Полдня, Half Day Date is mandatory,Полдня Дата обязательна, -Half Day Date should be between From Date and To Date,Поаяся Дата должна быть в пределах от даты и до настоящего времени, -Half Day Date should be in between Work From Date and Work End Date,Половина дня должна находиться между Работой с даты и датой окончания работы, +Half Day Date should be between From Date and To Date,Дата половины дня должна быть между датой начала и датой окончания., +Half Day Date should be in between Work From Date and Work End Date,Дата половины дня должна находиться между датой начала работы и датой окончания работы, Half Yearly,Половина года, Half day date should be in between from date and to date,Половина дня должна быть между датой и датой, Half-Yearly,Раз в полгода, @@ -1217,8 +1217,8 @@ Holiday,Выходной, Holiday List,Список праздников, Hotel Rooms of type {0} are unavailable on {1},Номера отеля типа {0} недоступны в {1}, Hotels,Отели, -Hourly,почасовой, -Hours,часов, +Hourly,Почасовой, +Hours,Часов, House rent paid days overlapping with {0},Аренда дома оплачивается по дням с перекрытием {0}, House rented dates required for exemption calculation,"Даты аренды дома, необходимые для расчета освобождения", House rented dates should be atleast 15 days apart,Даты аренды дома должны быть как минимум на 15 дней друг от друга, @@ -1265,7 +1265,7 @@ Include Exploded Items,Включить раздробленные элемен Include POS Transactions,Включить POS-транзакции, Include UOM,Включить UOM, Included in Gross Profit,Включено в валовую прибыль, -Income,доход, +Income,Доход, Income Account,Счет Доходов, Income Tax,Подоходный налог, Incoming,Входящий, @@ -1286,7 +1286,7 @@ Installation date cannot be before delivery date for Item {0},Дата уста Installing presets,Установка пресетов, Institute Abbreviation,институт Аббревиатура, Institute Name,Название института, -Instructor,инструктор, +Instructor,Инструктор, Insufficient Stock,Недостаточный Stock, Insurance Start date should be less than Insurance End date,"Дата страхование начала должна быть меньше, чем дата страхование End", Integrated Tax,Интегрированный налог, @@ -1352,7 +1352,7 @@ Item Description,Описание продукта, Item Group,Продуктовая группа, Item Group Tree,Структура продуктовых групп, Item Group not mentioned in item master for item {0},Пункт Группа не упоминается в мастера пункт по пункту {0}, -Item Name,Имя элемента, +Item Name,Название продукта, Item Price added for {0} in Price List {1},Цена продукта {0} добавлена в прайс-лист {1}, "Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates.","Цена товара отображается несколько раз на основе Прайс-листа, Поставщика / Клиента, Валюты, Предмет, UOM, Кол-во и Даты.", Item Price updated for {0} in Price List {1},Цена продукта {0} обновлена в прайс-листе {1}, @@ -1397,7 +1397,7 @@ Job Card,Карточка работы, Job Description,Описание работы, Job Offer,Предложение работы, Job card {0} created,Карта работы {0} создана, -Jobs,работы, +Jobs,Работы, Join,Присоединиться, Journal Entries {0} are un-linked,Записи в журнале {0} не-связаны, Journal Entry,Запись в дневнике, @@ -1413,10 +1413,10 @@ Lab Test UOM,Лабораторная проверка UOM, Lab Tests and Vital Signs,Лабораторные тесты и жизненные знаки, Lab result datetime cannot be before testing datetime,Лабораторный результат datetime не может быть до тестирования даты и времени, Lab testing datetime cannot be before collection datetime,Лабораторное тестирование datetime не может быть до даты сбора данных, -Label,Имя поля, -Laboratory,лаборатория, +Label,Ярлык, +Laboratory,Лаборатория, Language Name,Название языка, -Large,большой, +Large,Большой, Last Communication,Последнее сообщение, Last Communication Date,Дата последнего общения, Last Name,Фамилия, @@ -1426,13 +1426,13 @@ Last Purchase Price,Последняя цена покупки, Last Purchase Rate,Последняя цена покупки, Latest,Последние, Latest price updated in all BOMs,Последняя цена обновлена во всех спецификациях, -Lead,Обращение, -Lead Count,Счет Обращения, -Lead Owner,Ответственный, -Lead Owner cannot be same as the Lead,Ответственным за Обращение не может быть сам обратившийся, -Lead Time Days,Время выполнения, -Lead to Quotation,Обращение в Предложение, -"Leads help you get business, add all your contacts and more as your leads",Обращения необходимы бизнесу. Добавьте все свои контакты в качестве Обращений, +Lead,Лид, +Lead Count,Количество лидов, +Lead Owner,Ответственный за лид, +Lead Owner cannot be same as the Lead,Ответственным за лид не может быть сам лид, +Lead Time Days,Время лида, +Lead to Quotation,Лид в предложение, +"Leads help you get business, add all your contacts and more as your leads",Лиды необходимы бизнесу. Добавьте все свои контакты в качестве лидов, Learn,Справка, Leave Approval Notification,Оставить уведомление об утверждении, Leave Blocked,Оставьте Заблокированные, @@ -1464,7 +1464,7 @@ Level,Уровень, Liability,Ответственность сторон, License,Лицензия, Lifecycle,Жизненный цикл, -Limit,предел, +Limit,Предел, Limit Crossed,предел Скрещенные, Link to Material Request,Ссылка на запрос материала, List of all share transactions,Список всех сделок с акциями, @@ -1482,7 +1482,7 @@ Local,Локальные, Log,Запись в журнале, Logs for maintaining sms delivery status,Журналы для просмотра статуса доставки СМС, Lost,Поражений, -Lost Reasons,Потерянные Причины, +Lost Reasons,Потерянные причины, Low,Низкий, Low Sensitivity,Низкая чувствительность, Lower Income,Низкий уровень дохода, @@ -1515,7 +1515,7 @@ Manage Sales Partners.,Управление партнерами по сбыту Manage Sales Person Tree.,Управление деревом менеджеров по продажам., Manage Territory Tree.,Управление деревом территорий., Manage your orders,Управляйте свои заказы, -Management,управление, +Management,Менеджмент, Manager,Менеджер, Managing Projects,Управление проектами, Managing Subcontracting,Управление субподрядом, @@ -1524,17 +1524,17 @@ Mandatory field - Academic Year,Обязательное поле — акаде Mandatory field - Get Students From,Обязательное поле — получить учащихся из, Mandatory field - Program,Обязательное поле — программа, Manufacture,Производство, -Manufacturer,производитель, +Manufacturer,Производитель, Manufacturer Part Number,Номер партии производителя, Manufacturing,Производство, Manufacturing Quantity is mandatory,Производство Количество является обязательным, -Mapping,картографирование, +Mapping,Картографирование, Mapping Type,Тип отображения, Mark Absent,Отметка отсутствует, Mark Attendance,Пометить посещаемость, Mark Half Day,Отметить Полдня, Mark Present,Марк Присутствует, -Marketing,маркетинг, +Marketing,Маркетинг, Marketing Expenses,Маркетинговые расходы, Marketplace,Торговая площадка, Marketplace Error,Ошибка рынка, @@ -1578,13 +1578,13 @@ Member Activity,Активность участника, Member ID,ID пользователя, Member Name,Имя участника, Member information.,Информация о членах., -Membership,членство, +Membership,Членство, Membership Details,Сведения о членстве, Membership ID,Идентификатор членства, Membership Type,Тип членства, Memebership Details,Меморандум, Memebership Type Details,Информация о типе памяти, -Merge,сливаться, +Merge,Объеденить, Merge Account,Объединить учетную запись, Merge with Existing Account,Слияние с существующей учетной записью, "Merging is only possible if following properties are same in both records. Is Group, Root Type, Company","Объединение возможно только, если следующие свойства такие же, как в отчетах. Есть группа, корневого типа, компания", @@ -1596,7 +1596,7 @@ Middle Name,Второе имя, Middle Name (Optional),Отчество (необязательно), Min Amt can not be greater than Max Amt,Min Amt не может быть больше Max Amt, Min Qty can not be greater than Max Qty,"Мин Кол-во не может быть больше, чем максимальное Кол-во", -Minimum Lead Age (Days),Минимальный срок Обращения (в днях), +Minimum Lead Age (Days),Минимальный срок лида (в днях), Miscellaneous Expenses,Прочие расходы, Missing Currency Exchange Rates for {0},Пропавших без вести Курсы валют на {0}, Missing email template for dispatch. Please set one in Delivery Settings.,Отсутствует шаблон электронной почты для отправки. Установите его в настройках доставки., @@ -1606,7 +1606,7 @@ Mode of Payments,Способ оплаты, Mode of Transport,Вид транспорта, Mode of Transportation,Способ транспортировки, Mode of payment is required to make a payment,Способ оплаты требуется произвести оплату, -Model,модель, +Model,Модель, Moderate Sensitivity,Умеренная Чувствительность, Monday,Понедельник, Monthly,Ежемесячно, @@ -1625,13 +1625,13 @@ Multiple Loyalty Program found for the Customer. Please select manually.,Про "Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}","Несколько Цена Правила существует с теми же критериями, пожалуйста разрешить конфликт путем присвоения приоритета. Цена Правила: {0}", Multiple Variants,Несколько вариантов, Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year,"Несколько финансовых лет существуют на дату {0}. Пожалуйста, установите компанию в финансовый год", -Music,музыка, +Music,Музыка, My Account,Мой аккаунт, Name error: {0},Ошибка Имя: {0}, Name of new Account. Note: Please don't create accounts for Customers and Suppliers,"Название нового счёта. Примечание: Пожалуйста, не создавайте счета для клиентов и поставщиков", Name or Email is mandatory,Имя или адрес электронной почты является обязательным, Nature Of Supplies,Природа поставок, -Navigating,навигационный, +Navigating,Навигационный, Needs Analysis,Анализ потребностей, Negative Quantity is not allowed,Отрицательное количество недопустимо, Negative Valuation Rate is not allowed,Отрицательный Оценка курс не допускается, @@ -1659,7 +1659,7 @@ New BOM,Новая ВМ, New Batch ID (Optional),Новый идентификатор партии (необязательно), New Batch Qty,Новое количество партий, New Company,Новая Компания, -New Cost Center Name,Новый Центр Стоимость Имя, +New Cost Center Name,Название нового центра затрат, New Customer Revenue,Новый Выручка клиентов, New Customers,новые клиенты, New Department,Новый отдел, @@ -1676,7 +1676,7 @@ New {0} pricing rules are created,Новые {0} правила ценообра Newsletters,Информационная рассылка, Newspaper Publishers,Информационное издательство, Next,Далее, -Next Contact By cannot be same as the Lead Email Address,Следующий контакт не может совпадать с адресом электронной почты Обращения, +Next Contact By cannot be same as the Lead Email Address,Следующий контакт не может совпадать с адресом электронной почты лида, Next Contact Date cannot be in the past,Дата следующего контакта не может быть в прошлом, Next Steps,Следующие шаги, No Action,Бездействие, @@ -1692,7 +1692,7 @@ No Items to pack,Нет продуктов для упаковки, No Items with Bill of Materials to Manufacture,Нет предметов с Биллом материалов не Manufacture, No Items with Bill of Materials.,Нет предметов с ведомостью материалов., No Permission,Нет разрешения, -No Remarks,Нет Замечания, +No Remarks,Нет замечаний, No Result to submit,Нет результатов для отправки, No Salary Structure assigned for Employee {0} on given date {1},"Нет структуры заработной платы, назначенной для сотрудника {0} в данную дату {1}", No Staffing Plans found for this Designation,Никаких кадровых планов для этого обозначения, @@ -1727,16 +1727,16 @@ No values,Нет значений, No {0} found for Inter Company Transactions.,Нет {0} найдено для транзакций Inter Company., Non GST Inward Supplies,Не входящие в GST поставки, Non Profit,Некоммерческое предприятие, -Non Profit (beta),Непрофит (бета), +Non Profit (beta),Некоммерческое предприятие (бета), Non-GST outward supplies,Внешние поставки без GST, Non-Group to Group,Non-группы к группе, None,Никто, None of the items have any change in quantity or value.,Ни одному продукту не изменено количество или объём., -Nos,кол-во, -Not Available,Не доступен, -Not Marked,без маркировки, +Nos,Кол-во, +Not Available,Недоступен, +Not Marked,Без маркировки, Not Paid and Not Delivered,Не оплачен и не доставлен, -Not Permitted,Не Допустимая, +Not Permitted,Нет допуска, Not Started,Не начато, Not active,Не действует, Not allow to set alternative item for the item {0},Не разрешить установку альтернативного элемента для элемента {0}, @@ -1766,12 +1766,12 @@ Number of Order,Номер заказа, "Number of new Account, it will be included in the account name as a prefix","Номер новой учетной записи, она будет включена в имя учетной записи в качестве префикса", "Number of new Cost Center, it will be included in the cost center name as a prefix","Количество нового МВЗ, оно будет включено в название МВЗ в качестве префикса", Number of root accounts cannot be less than 4,Количество корневых учетных записей не может быть меньше 4, -Odometer,одометр, +Odometer,Одометр, Office Equipments,Оборудование офиса, Office Maintenance Expenses,Эксплуатационные расходы на офис, Office Rent,Аренда площади для офиса, On Hold,На удерживании, -On Net Total,On Net Всего, +On Net Total,Чистая сумма, One customer can be part of only single Loyalty Program.,Один клиент может быть частью единой программы лояльности., Online Auctions,Аукционы в Интернете, Only Leave Applications with status 'Approved' and 'Rejected' can be submitted,Только оставьте приложения со статусом «Одобрено» и «Отклонено» могут быть представлены, @@ -1782,7 +1782,7 @@ Open Item {0},Открыть продукт {0}, Open Notifications,Открытые уведомления, Open Orders,Открытые заказы, Open a new ticket,Открыть новый билет, -Opening,открытие, +Opening,Открытие, Opening (Cr),Начальное сальдо (кредит), Opening (Dr),Начальное сальдо (дебет), Opening Accounting Balance,Начальный бухгалтерский баланс, @@ -1808,11 +1808,11 @@ Operation Time must be greater than 0 for Operation {0},"Время работы Operations,Эксплуатация, Operations cannot be left blank,"Операции, не может быть оставлено пустым", Opp Count,Счетчик Opp, -Opp/Lead %,"Выявления/Обращения, %", +Opp/Lead %,"Выявления/Лиды, %", Opportunities,Возможности, -Opportunities by lead source,Выявления из источника обращения, -Opportunity,Выявление, -Opportunity Amount,Количество Выявления, +Opportunities by lead source,Возможность из источника лидов, +Opportunity,Возможность, +Opportunity Amount,Сумма возможности, Optional Holiday List not set for leave period {0},Необязательный список праздников не установлен для периода отпуска {0}, "Optional. Sets company's default currency, if not specified.","Необязательный. Устанавливает по умолчанию валюту компании, если не указано.", Optional. This setting will be used to filter in various transactions.,Факультативно. Эта установка будет использоваться для фильтрации в различных сделок., @@ -1821,13 +1821,13 @@ Order Count,Количество заказов, Order Entry,Порядок въезда, Order Value,Ценность заказа, Order rescheduled for sync,Заказ перенесен на синхронизацию, -Order/Quot %,Заказ / Котировка%, +Order/Quot %,Заказ/Котировка %, Ordered,В обработке, Ordered Qty,Заказал кол-во, "Ordered Qty: Quantity ordered for purchase, but not received.","Заказал Количество: Количество заказал для покупки, но не получил.", -Orders,заказы, +Orders,Заказы, Orders released for production.,"Заказы, выпущенные для производства.", -Organization,организация, +Organization,Организация, Organization Name,Название организации, Other,Другое, Other Reports,Другие отчеты, @@ -1900,7 +1900,7 @@ Payment Entry is already created,Оплата запись уже создан, Payment Failed. Please check your GoCardless Account for more details,"Платеж не прошел. Пожалуйста, проверьте свою учетную запись GoCardless для получения более подробной информации.", Payment Gateway,Платежный шлюз, "Payment Gateway Account not created, please create one manually.","Payment Gateway Account не создан, создайте его вручную.", -Payment Gateway Name,Имя платежного шлюза, +Payment Gateway Name,Название платежного шлюза, Payment Mode,Режим платежа, Payment Receipt Note,Оплата Получение Примечание, Payment Request,Платежная заявка, @@ -1937,7 +1937,7 @@ Periodicity,периодичность, Personal Details,Личные Данные, Pharmaceutical,Фармацевтический, Pharmaceuticals,Фармацевтика, -Physician,врач, +Physician,Врач, Piecework,Сдельная работа, Pincode,Pincode, Place Of Supply (State/UT),Место поставки (штат / UT), @@ -1946,16 +1946,16 @@ Plan Name,Название плана, Plan for maintenance visits.,Запланируйте для посещения технического обслуживания., Planned Qty,Планируемое кол-во, "Planned Qty: Quantity, for which, Work Order has been raised, but is pending to be manufactured.","Запланированное кол-во: количество, для которого было задано рабочее задание, но ожидается его изготовление.", -Planning,планирование, +Planning,Планирование, Plants and Machineries,Растения и Механизмов, Please Set Supplier Group in Buying Settings.,Установите группу поставщиков в разделе «Настройки покупок»., Please add a Temporary Opening account in Chart of Accounts,"Пожалуйста, добавьте временный вступительный счет в План счетов", -Please add the account to root level Company - ,"Пожалуйста, добавьте учетную запись на корневой уровень компании -", +Please add the account to root level Company - ,"Пожалуйста, добавьте счет на корневой уровень компании -", Please add the remaining benefits {0} to any of the existing component,Добавьте оставшиеся преимущества {0} к любому из существующих компонентов, Please check Multi Currency option to allow accounts with other currency,"Пожалуйста, проверьте мультивалютный вариант, позволяющий счета другой валюте", -Please click on 'Generate Schedule',"Пожалуйста, нажмите на кнопку ""Generate Расписание""", -Please click on 'Generate Schedule' to fetch Serial No added for Item {0},"Пожалуйста, нажмите на кнопку ""Generate Расписание"", чтобы принести Серийный номер добавлен для Пункт {0}", -Please click on 'Generate Schedule' to get schedule,"Пожалуйста, нажмите на кнопку ""Generate Расписание"", чтобы получить график", +Please click on 'Generate Schedule',"Пожалуйста, нажмите на кнопку ""Создать расписание""", +Please click on 'Generate Schedule' to fetch Serial No added for Item {0},"Пожалуйста, нажмите на кнопку ""Создать расписание"", чтобы принести Серийный номер добавлен для Пункт {0}", +Please click on 'Generate Schedule' to get schedule,"Пожалуйста, нажмите на кнопку ""Создать расписание"", чтобы получить график", Please confirm once you have completed your training,"Пожалуйста, подтвердите, как только вы закончили обучение", Please create purchase receipt or purchase invoice for the item {0},Создайте квитанцию о покупке или фактуру покупки для товара {0}, Please define grade for Threshold 0%,"Пожалуйста, определите оценку для Threshold 0%", @@ -2007,7 +2007,7 @@ Please mention Basic and HRA component in Company,"Пожалуйста, ука Please mention Round Off Account in Company,"Пожалуйста, укажите округлить счет в компании", Please mention Round Off Cost Center in Company,"Пожалуйста, укажите округлить МВЗ в компании", Please mention no of visits required,"Пожалуйста, укажите кол-во посещений, необходимых", -Please mention the Lead Name in Lead {0},"Пожалуйста, укажите Имя в Обращении {0}", +Please mention the Lead Name in Lead {0},"Пожалуйста, укажите имя в лиде {0}", Please pull items from Delivery Note,Пожалуйста вытяните продукты из транспортной накладной, Please register the SIREN number in the company information file,"Пожалуйста, зарегистрируйте номер SIREN в файле информации о компании", Please remove this Invoice {0} from C-Form {1},"Пожалуйста, удалите этот счет {0} из C-Form {1}", @@ -2067,9 +2067,9 @@ Please select the document type first,"Пожалуйста, выберите т Please select weekly off day,"Пожалуйста, выберите в неделю выходной", Please select {0},"Пожалуйста, выберите {0}", Please select {0} first,"Пожалуйста, выберите {0} первый", -Please set 'Apply Additional Discount On',"Пожалуйста, установите "Применить Дополнительная Скидка On '", +Please set 'Apply Additional Discount On',"Пожалуйста, установите "Применить дополнительную скидку на '", Please set 'Asset Depreciation Cost Center' in Company {0},"Пожалуйста, установите "активов Амортизация затрат по МВЗ" в компании {0}", -Please set 'Gain/Loss Account on Asset Disposal' in Company {0},"Пожалуйста, установите "прибыль / убыток Счет по обращению с отходами актива в компании {0}", +Please set 'Gain/Loss Account on Asset Disposal' in Company {0},"Пожалуйста, установите "прибыль / убыток Счет по лиду с отходами актива в компании {0}", Please set Account in Warehouse {0} or Default Inventory Account in Company {1},Укажите учетную запись в хранилище {0} или учетную запись инвентаризации по умолчанию в компании {1}, Please set B2C Limit in GST Settings.,Установите B2C Limit в настройках GST., Please set Company,Укажите компанию, @@ -2133,9 +2133,9 @@ Posting timestamp must be after {0},Время публикации должно Potential opportunities for selling.,Потенциальные возможности для продажи., Practitioner Schedule,Расписание практикующих, Pre Sales,Предпродажа, -Preference,предпочтение, +Preference,Предпочтение, Prescribed Procedures,Предписанные процедуры, -Prescription,давность, +Prescription,Рецепт, Prescription Dosage,Дозировка по рецепту, Prescription Duration,Продолжительность рецепта, Prescriptions,Предписания, @@ -2152,8 +2152,8 @@ Price List master.,Мастер Прайс-лист., Price List must be applicable for Buying or Selling,Прайс-лист должен быть применим для покупки или продажи, Price List {0} is disabled or does not exist,Прайс-лист {0} отключен или не существует, Price or product discount slabs are required,Требуется цена или скидка на продукцию, -Pricing,ценообразование, -Pricing Rule,Цены Правило, +Pricing,Ценообразование, +Pricing Rule,Правила ценообразования, "Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.","Правило ценообразования сначала выбирается на основе поля «Применить на», значением которого может быть Позиция, Группа Позиций, Торговая Марка.", "Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.","Правило ценообразования делается для того, чтобы изменить Прайс-лист / определить процент скидки, основанный на некоторых критериях.", Pricing Rule {0} is updated,Правило ценообразования {0} обновлено, @@ -2171,8 +2171,8 @@ Print taxes with zero amount,Печать налогов с нулевой су Printing and Branding,Печать и брендинг, Private Equity,Частные капиталовложения, Privilege Leave,Привилегированный Оставить, -Probation,испытательный срок, -Probationary Period,Испытательный срок, +Probation,Испытательный срок, +Probationary Period,Испытательный период, Procedure,Процедура, Process Day Book Data,Обработка данных дневника, Process Master Data,Обработка основных данных, @@ -2193,12 +2193,12 @@ Profit for the year,Прибыль за год, Program,программа, Program in the Fee Structure and Student Group {0} are different.,Программа в структуре вознаграждения и студенческой группе {0} отличается., Program {0} does not exist.,Программа {0} не существует., -Program: ,Программа:, +Program: ,Программа: , Progress % for a task cannot be more than 100.,Готовность задачи не может превышать 100%., Project Collaboration Invitation,Сотрудничество Приглашение проекта, Project Id,Идентификатор проекта, Project Manager,Менеджер проектов, -Project Name,название проекта, +Project Name,Название проекта, Project Start Date,Дата начала проекта, Project Status,Статус проекта, Project Summary for {0},Краткое описание проекта для {0}, @@ -2239,12 +2239,12 @@ Purchase Order {0} is not submitted,Заказ на закупку {0} не пр Purchase Orders are not allowed for {0} due to a scorecard standing of {1}.,"Заказы на поставку не допускаются для {0} из-за того, что система показателей имеет значение {1}.", Purchase Orders given to Suppliers.,"Заказы, выданные поставщикам.", Purchase Price List,Прайс-лист закупки, -Purchase Receipt,Товарный чек, +Purchase Receipt,Квитанция о покупке, Purchase Receipt {0} is not submitted,Приход закупки {0} не проведен, Purchase Tax Template,Налог на покупку шаблон, Purchase User,Специалист поставок, Purchase orders help you plan and follow up on your purchases,Заказы помогут вам планировать и следить за ваши покупки, -Purchasing,покупка, +Purchasing,Покупка, Purpose must be one of {0},Цель должна быть одна из {0}, Qty,Кол-во, Qty To Manufacture,Кол-во для производства, @@ -2276,8 +2276,8 @@ Query Options,Параметры запроса, Queued for replacing the BOM. It may take a few minutes.,Очередь на замену спецификации. Это может занять несколько минут., Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Очередь для обновления последней цены во всех Биллях материалов. Это может занять несколько минут., Quick Journal Entry,Быстрый журнал запись, -Quot Count,Количество котировок, -Quot/Lead %,"Предложения/Обращения, %", +Quot Count,Количество предложений, +Quot/Lead %,"Предложения/Лиды, %", Quotation,Предложение, Quotation {0} is cancelled,Предложение {0} отменено, Quotation {0} not of type {1},Предложение {0} не типа {1}, @@ -2285,7 +2285,7 @@ Quotations,Предложения, "Quotations are proposals, bids you have sent to your customers","Предложения - это коммерческие предложения, которые вы отправили своим клиентам", Quotations received from Suppliers.,"Предложения, полученные от Поставщиков.", Quotations: ,Предложения:, -Quotes to Leads or Customers.,Предложения Обращениям или Клиентам., +Quotes to Leads or Customers.,Предложения в Лиды или Клиентов., RFQs are not allowed for {0} due to a scorecard standing of {1},"Запросы не допускаются для {0} из-за того, что значение показателя {1}", Range,Диапазон, Rate,Цена, @@ -2303,7 +2303,7 @@ Reason For Putting On Hold,Причина удержания, Reason for Hold,Причина удержания, Reason for hold: ,Причина удержания:, Receipt,Квитанция, -Receipt document must be submitted,Документ о получении должен быть проведен, +Receipt document must be submitted,Документ о получении должен быть исполнен, Receivable,Дебиторская задолженность, Receivable Account,Счет Дебиторской задолженности, Received,Получено, @@ -2316,7 +2316,7 @@ Reconcile,Согласовать, "Record of all communications of type email, phone, chat, visit, etc.","Запись всех способов коммуникации — электронной почты, звонков, чатов, посещений и т. п.", Records,Записи, Redirect URL,Перенаправление URL, -Ref,ссылка, +Ref,Ссылка, Ref Date,Дата ссылки, Reference,Справка, Reference #{0} dated {1},Ссылка №{0} от {1}, @@ -2333,17 +2333,17 @@ Reference Owner,Владелец ссылки, Reference Type,Тип ссылки, "Reference: {0}, Item Code: {1} and Customer: {2}","Ссылка: {0}, Код товара: {1} и Заказчик: {2}", References,Рекомендации, -Refresh Token,токен обновления, +Refresh Token,Токен обновления, Region,Область, -Register,регистр, -Reject,отклонять, +Register,Регистр, +Reject,Отклонить, Rejected,Отклоненные, Related,Связанный, Relation with Guardian1,Связь с Guardian1, Relation with Guardian2,Связь с Guardian2, Release Date,Дата выпуска, Reload Linked Analysis,Обновить связанный анализ, -Remaining,осталось, +Remaining,Осталось, Remaining Balance,Остаток средств, Remarks,Примечания, Reminder to update GSTIN Sent,Напоминание об обновлении отправленного GSTIN, @@ -2373,9 +2373,9 @@ Requested Qty,Запрашиваемое кол-во, "Requested Qty: Quantity requested for purchase, but not ordered.","Запрашиваемые Кол-во: Количество просил для покупки, но не заказали.", Requesting Site,Запрашивающий сайт, Requesting payment against {0} {1} for amount {2},Запрос платеж против {0} {1} на сумму {2}, -Requestor,Requestor, -Required On,Обязательно На, -Required Qty,Обязательные Кол-во, +Requestor,Заявитель, +Required On,Требуется на, +Required Qty,Требуемое количество, Required Quantity,Необходимое количество, Reschedule,Перепланирование, Research,Исследования, @@ -2393,7 +2393,7 @@ Reserved for sale,Зарезервировано для продажи, Reserved for sub contracting,Зарезервировано для субподряда, Resistant,резистентный, Resolve error and upload again.,Устраните ошибку и загрузите снова., -Responsibilities,обязанности, +Responsibilities,Обязанности, Rest Of The World,Остальной мир, Restart Subscription,Перезапустить подписку, Restaurant,Ресторан, @@ -2411,18 +2411,18 @@ Return / Credit Note,Возвращение / Кредит Примечание, Return / Debit Note,Возврат / дебетовые Примечание, Returns,Возвращает, Reverse Journal Entry,Обратная запись журнала, -Review Invitation Sent,Отправлено приглашение на просмотр, +Review Invitation Sent,Отправлено приглашение на рассмотрение, Review and Action,Обзор и действие, Role,Роль, Rooms Booked,Забронированные номера, -Root Company,Корневая Компания, +Root Company,Родительская компания, Root Type,Корневая Тип, Root Type is mandatory,Корневая Тип является обязательным, Root cannot be edited.,Корневая не могут быть изменены., Root cannot have a parent cost center,Корневая не может иметь родителей МВЗ, Round Off,Округлять, -Rounded Total,Округлые Всего, -Route,маршрут, +Rounded Total,Итого с округлением, +Route,Маршрут, Row # {0}: ,Ряд # {0}:, Row # {0}: Batch No must be same as {1} {2},"Ряд # {0}: Пакетное Нет должно быть таким же, как {1} {2}", Row # {0}: Cannot return more than {1} for Item {2},Ряд # {0}: Невозможно вернуть более {1} для п {2}, @@ -2507,7 +2507,7 @@ Salary,Зарплата, Salary Slip ID,Зарплата скольжения ID, Salary Slip of employee {0} already created for this period,Зарплата Скольжение работника {0} уже создано за этот период, Salary Slip of employee {0} already created for time sheet {1},Зарплата Скольжение работника {0} уже создан для табеля {1}, -Salary Slip submitted for period from {0} to {1},"Зарплатный сальс, представленный на период от {0} до {1}", +Salary Slip submitted for period from {0} to {1},"Зарплатная ведомость отправлена за период с {0} по {1}", Salary Structure Assignment for Employee already exists,Присвоение структуры зарплаты сотруднику уже существует, Salary Structure Missing,Структура заработной платы Отсутствующий, Salary Structure must be submitted before submission of Tax Ememption Declaration,Структура заработной платы должна быть представлена до подачи декларации об освобождении от налогов, @@ -2583,7 +2583,7 @@ Securities and Deposits,Ценные бумаги и депозиты, See All Articles,Просмотреть все статьи, See all open tickets,Просмотреть все открытые билеты, See past orders,Посмотреть прошлые заказы, -See past quotations,Посмотреть прошлые цитаты, +See past quotations,Посмотреть прошлые предложения, Select,Выбрать, Select Alternate Item,Выбрать альтернативный элемент, Select Attribute Values,Выберите значения атрибута, @@ -2624,16 +2624,16 @@ Select to add Serial Number.,"Выберите, чтобы добавить се Select your Domains,Выберите свои домены, Selected Price List should have buying and selling fields checked.,Выбранный прейскурант должен иметь поля для покупки и продажи., Sell,Продажа, -Selling,продажа, -Selling Amount,Продажа Сумма, +Selling,Продажа, +Selling Amount,Сумма продажа, Selling Price List,Продажа прайс-листа, Selling Rate,Стоимость продажи, "Selling must be checked, if Applicable For is selected as {0}","Продажа должна быть проверена, если выбран Применимо для как {0}", Send Grant Review Email,Отправить отзыв по электронной почте, -Send Now,Отправить Сейчас, +Send Now,Отправить сейчас, Send SMS,Отправить смс, Send mass SMS to your contacts,Отправить массовое СМС по списку контактов, -Sensitivity,чувствительность, +Sensitivity,Чувствительность, Sent,Отправлено, Serial No and Batch,Серийный номер и партия, Serial No is mandatory for Item {0},Серийный номер является обязательным для п. {0}, @@ -2673,12 +2673,12 @@ Set Details,Установить детали, Set New Release Date,Установите новую дату выпуска, Set Project and all Tasks to status {0}?,Установить проект и все задачи в статус {0}?, Set Status,Установить статус, -Set Tax Rule for shopping cart,Установите Налоговый Правило корзине, -Set as Closed,Установить как Закрыт, +Set Tax Rule for shopping cart,Установить налоговое правило для корзины, +Set as Closed,Установить как "Закрыт", Set as Completed,Сделать завершенным, Set as Default,Установить по умолчанию, -Set as Lost,Установить как Остаться в живых, -Set as Open,Установить как Open, +Set as Lost,Установить как "Потерянный", +Set as Open,Установить как "Открытый", Set default inventory account for perpetual inventory,Установить учетную запись по умолчанию для вечной инвентаризации, Set this if the customer is a Public Administration company.,"Установите это, если клиент является компанией государственного управления.", Set {0} in asset category {1} or company {2},Установите {0} в категории активов {1} или компании {2}, @@ -2687,9 +2687,9 @@ Setting defaults,Установка значений по умолчанию, Setting up Email,Настройка электронной почты, Setting up Email Account,Настройка учетной записи электронной почты, Setting up Employees,Настройка сотрудников, -Setting up Taxes,Настройка Налоги, +Setting up Taxes,Настройка налога, Setting up company,Создание компании, -Settings,настройки, +Settings,Настройки, "Settings for online shopping cart such as shipping rules, price list etc.","Настройки для онлайн корзины, такие как правилами перевозок, прайс-лист и т.д.", Settings for website homepage,Настройки для сайта домашнюю страницу, Settings for website product listing,Настройки для списка товаров на сайте, @@ -2701,11 +2701,11 @@ Setup default values for POS Invoices,Настройка значений по Setup mode of POS (Online / Offline),Режим настройки POS (Online / Offline), Setup your Institute in ERPNext,Установите свой институт в ERPNext, Share Balance,Баланс акций, -Share Ledger,Share Ledger, +Share Ledger,Поделиться записями, Share Management,Управление долями, Share Transfer,Передача акций, Share Type,Share Тип, -Shareholder,акционер, +Shareholder,Акционер, Ship To State,Корабль в штат, Shipments,Поставки, Shipping,Доставка, @@ -2715,17 +2715,17 @@ Shipping rule only applicable for Buying,Правило доставки при Shipping rule only applicable for Selling,Правило доставки применимо только для продажи, Shopify Supplier,Покупатель, Shopping Cart,Корзина, -Shopping Cart Settings,Корзина Настройки, -Short Name,Короткое Имя, +Shopping Cart Settings,Настройки корзины, +Short Name,Короткое имя, Shortage Qty,Нехватка Кол-во, -Show Completed,Показать выполнено, +Show Completed,Показать завершенные, Show Cumulative Amount,Показать суммарную сумму, Show Employee,Показать сотрудника, -Show Open,Показать открыт, +Show Open,Показать открытые, Show Opening Entries,Показать вступительные записи, Show Payment Details,Показать данные платежа, Show Return Entries,Показать возвращенные записи, -Show Salary Slip,Показать Зарплата скольжению, +Show Salary Slip,Показать зарплатную ведомость, Show Variant Attributes,Показать атрибуты варианта, Show Variants,Показать варианты, Show closed,Показать закрыто, @@ -2787,14 +2787,14 @@ Stock,Склад, Stock Adjustment,Регулирование запасов, Stock Analytics,Аналитика запасов, Stock Assets,Капитал запасов, -Stock Available,Имеется в наличии, +Stock Available,Есть в наличии, Stock Balance,Баланс запасов, Stock Entries already created for Work Order ,"Записи запаса, уже созданные для рабочего заказа", Stock Entry,Движения на складе, Stock Entry {0} created,Создана складская запись {0}, Stock Entry {0} is not submitted,Складской акт {0} не проведен, Stock Expenses,Расходы по Запасам, -Stock In Hand,Товарная наличность, +Stock In Hand,Запасы на руках, Stock Items,Позиции на складе, Stock Ledger,Книга учета Запасов, Stock Ledger Entries and GL Entries are reposted for the selected Purchase Receipts,Записи складской книги и записи GL запасов отправляются для выбранных покупок, @@ -2816,7 +2816,7 @@ Stock transactions before {0} are frozen,Перемещения по склад Stop,Стоп, Stopped,Приостановлено, "Stopped Work Order cannot be cancelled, Unstop it first to cancel","Прекращенный рабочий заказ не может быть отменен, отмените его сначала, чтобы отменить", -Stores,магазины, +Stores,Магазины, Structures have been assigned successfully,Структуры были успешно назначены, Student,Студент, Student Activity,Студенческая деятельность, @@ -2833,7 +2833,7 @@ Student Group: ,Студенческая группа:, Student ID,Студенческий билет, Student ID: ,Студенческий билет:, Student LMS Activity,Студенческая LMS Активность, -Student Mobile No.,Student Mobile No., +Student Mobile No.,Мобильный номер студента, Student Name,Имя ученика, Student Name: ,Имя ученика:, Student Report Card,Студенческая отчетная карточка, @@ -2845,7 +2845,7 @@ Student {0} exist against student applicant {1},Student {0} существует Sub Assemblies,Sub сборки, Sub Type,Подтип, Sub-contracting,Суб-сжимания, -Subcontract,субподряд, +Subcontract,Субподряд, Subject,Тема, Submit,Провести, Submit Proof,Подтвердить, @@ -2891,7 +2891,7 @@ Supply Type,Тип поставки, Support,Поддержка, Support Analytics,Аналитика поддержки, Support Settings,Настройки поддержки, -Support Tickets,Билеты на поддержку, +Support Tickets,Заявки на поддержку, Support queries from customers.,Поддержка запросов от клиентов., Susceptible,восприимчивый, Sync has been temporarily disabled because maximum retries have been exceeded,"Синхронизация временно отключена, поскольку превышены максимальные повторные попытки", @@ -2900,21 +2900,21 @@ Syntax error in formula or condition: {0},Синтаксическая ошиб System Manager,Менеджер системы, TDS Rate %,TDS Rate%, Tap items to add them here,"Выберите продукты, чтобы добавить их", -Target,цель, +Target,Цель, Target ({}),Цель ({}), Target On,Целевая На, Target Warehouse,Склад готовой продукции, Target warehouse is mandatory for row {0},Целевая склад является обязательным для ряда {0}, -Task,задача, +Task,Задача, Tasks,Задачи, Tasks have been created for managing the {0} disease (on row {1}),Задачи были созданы для управления {0} болезнью (в строке {1}), -Tax,налог, +Tax,Налог, Tax Assets,Налоговые активы, Tax Category,Налоговая категория, Tax Category for overriding tax rates.,Налоговая категория для переопределения налоговых ставок., "Tax Category has been changed to ""Total"" because all the Items are non-stock items","Налоговая категория была изменена на «Итого», потому что все элементы не являются складскими запасами", Tax ID,ИНН, -Tax Id: ,Идентификатор налога:, +Tax Id: ,Идентификатор налога: , Tax Rate,Размер налога, Tax Rule Conflicts with {0},Налоговый Правило конфликты с {0}, Tax Rule for transactions.,Налоговый Правило для сделок., @@ -2924,13 +2924,13 @@ Tax template for buying transactions.,Налоговый шаблон для п Tax template for item tax rates.,Налоговый шаблон для налоговых ставок., Tax template for selling transactions.,Налоговый шаблон для продажи сделок., Taxable Amount,Налогооблагаемая сумма, -Taxes,налоги, +Taxes,Налоги, Team Updates,Команда обновления, Technology,Технология, Telecommunications,Телекоммуникации, Telephone Expenses,Телефон Расходы, -Television,телевидение, -Template Name,Имя Шаблона, +Television,Телевидение, +Template Name,Название шаблона, Template of terms or contract.,Шаблон терминов или договором., Templates of supplier scorecard criteria.,Шаблоны критериев оценки поставщиков., Templates of supplier scorecard variables.,Шаблоны переменных показателей поставщика., @@ -2974,7 +2974,7 @@ The shares don't exist with the {0},Акций не существует с {0}, "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage",Задача была поставлена в качестве фонового задания. В случае возникновения каких-либо проблем с обработкой в фоновом режиме система добавит комментарий об ошибке в этой сверке запасов и вернется к этапу черновика., "Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.","Затем Правила ценообразования отфильтровываются на основе Клиента, Группы клиентов, Территории, Поставщика, Типа поставщика, Кампании, Партнера по продажам и т. д.", "There are inconsistencies between the rate, no of shares and the amount calculated","Существуют несоответствия между ставкой, количеством акций и рассчитанной суммой", -There are more holidays than working days this month.,"Есть больше праздников, чем рабочих дней в этом месяце.", +There are more holidays than working days this month.,"В этом месяце праздников больше, чем рабочих дней.", There can be multiple tiered collection factor based on the total spent. But the conversion factor for redemption will always be same for all the tier.,"Может быть многоуровневый коэффициент сбора, основанный на общей затрате. Но коэффициент пересчета для погашения всегда будет одинаковым для всех уровней.", There can only be 1 Account per Company in {0} {1},Там может быть только 1 аккаунт на компанию в {0} {1}, "There can only be one Shipping Rule Condition with 0 or blank value for ""To Value""","Там может быть только один Правило Начальные с 0 или пустое значение для ""To Размер""", @@ -2992,18 +2992,18 @@ This Week's Summary,Резюме этой недели, This action will stop future billing. Are you sure you want to cancel this subscription?,Это действие остановит будущий биллинг. Вы действительно хотите отменить эту подписку?, This covers all scorecards tied to this Setup,"Это охватывает все оценочные карточки, привязанные к этой настройке", This document is over limit by {0} {1} for item {4}. Are you making another {3} against the same {2}?,Этот документ находится над пределом {0} {1} для элемента {4}. Вы делаете другой {3} против того же {2}?, -This is a root account and cannot be edited.,Это корень счета и не могут быть изменены., +This is a root account and cannot be edited.,Это корень счетов и не может быть изменен., This is a root customer group and cannot be edited.,Это корневая группа клиентов и не могут быть изменены., This is a root department and cannot be edited.,Это корневой отдел и не может быть отредактирован., This is a root healthcare service unit and cannot be edited.,Это корневая служба здравоохранения и не может быть отредактирована., -This is a root item group and cannot be edited.,Это корень группу товаров и не могут быть изменены., -This is a root sales person and cannot be edited.,Это корень продавец и не могут быть изменены., +This is a root item group and cannot be edited.,Это корень группы продуктов и не может быть изменен., +This is a root sales person and cannot be edited.,Это корневой продавец и не может быть изменен., This is a root supplier group and cannot be edited.,Это группа поставщиков корней и не может быть отредактирована., -This is a root territory and cannot be edited.,Это корень территории и не могут быть изменены., +This is a root territory and cannot be edited.,Это корневая территория и не может быть изменена., This is an example website auto-generated from ERPNext,Это пример сайт автоматически сгенерированный из ERPNext, -This is based on logs against this Vehicle. See timeline below for details,Это основано на бревнах против этого транспортного средства. См график ниже для получения подробной информации, +This is based on logs against this Vehicle. See timeline below for details,Это основано на журналах для этого транспортного средства. Смотрите график ниже для деталей, This is based on stock movement. See {0} for details,Это основано на фондовом движении. См {0} для получения более подробной, -This is based on the Time Sheets created against this project,"Это основано на табелей учета рабочего времени, созданных против этого проекта", +This is based on the Time Sheets created against this project,"Это основано на табелях учета рабочего времени, созданных по этому проекту", This is based on the attendance of this Employee,Это основано на посещаемости этого сотрудника, This is based on the attendance of this Student,Это основано на посещаемости этого студента, This is based on transactions against this Customer. See timeline below for details,Это основано на операциях против этого клиента. См график ниже для получения подробной информации, @@ -3018,15 +3018,15 @@ Time Tracking,Отслеживание времени, "Time slot skiped, the slot {0} to {1} overlap exisiting slot {2} to {3}","Временной интервал пропущен, слот {0} - {1} перекрывает существующий слот {2} до {3}", Time slots added,Добавлены временные интервалы, Time(in mins),Время (в мин), -Timer,таймер, +Timer,Таймер, Timer exceeded the given hours.,Таймер превысил указанные часы., -Timesheet,табель, +Timesheet,Табель, Timesheet for tasks.,Табель для задач., Timesheet {0} is already completed or cancelled,Табель {0} уже заполнен или отменен, Timesheets,Табели, "Timesheets help keep track of time, cost and billing for activites done by your team","Timesheets поможет отслеживать время, стоимость и выставление счетов для Активности сделанной вашей команды", Titles for print templates e.g. Proforma Invoice.,"Титулы для шаблонов печати, например, счет-проформа.", -To,к, +To,К, To Address 1,Адрес 1, To Address 2,Адрес 2, To Bill,Укомплектован, @@ -3034,7 +3034,7 @@ To Date,До, To Date cannot be before From Date,На сегодняшний день не может быть раньше от даты, To Date cannot be less than From Date,"Дата не может быть меньше, чем с даты", To Date must be greater than From Date,"До даты должно быть больше, чем с даты", -To Date should be within the Fiscal Year. Assuming To Date = {0},Чтобы Дата должна быть в пределах финансового года. Предполагая To Date = {0}, +To Date should be within the Fiscal Year. Assuming To Date = {0},Дата должна быть в пределах финансового года. Предположим, до даты = {0}, To Datetime,Для DateTime, To Deliver,Для доставки, To Deliver and Bill,Для доставки и оплаты, @@ -3061,7 +3061,7 @@ To make Customer based incentive schemes.,Создание схем стимул To view logs of Loyalty Points assigned to a Customer.,"Просмотр журналов лояльности, назначенных Клиенту.", To {0},Для {0}, To {0} | {1} {2},Для {0} | {1} {2}, -Toggle Filters,Toggle Filters, +Toggle Filters,Изменить фильтры, Too many columns. Export the report and print it using a spreadsheet application.,Слишком много столбцов. Экспортируйте отчет и распечатайте его с помощью приложения для электронных таблиц., Tools,Инструменты, Total (Credit),Итого (кредит), @@ -3079,10 +3079,10 @@ Total Commission,Всего комиссия, Total Contribution Amount: {0},Общая сумма вклада: {0}, Total Credit/ Debit Amount should be same as linked Journal Entry,"Общая сумма кредита / дебетовой суммы должна быть такой же, как связанная запись журнала", Total Debit must be equal to Total Credit. The difference is {0},"Всего Дебет должна быть равна общей выработке. Разница в том, {0}", -Total Deduction,Всего Вычет, -Total Invoiced Amount,Всего Сумма по счетам, +Total Deduction,Общий вычет, +Total Invoiced Amount,Общая сумма по счетам, Total Leaves,Всего Листья, -Total Order Considered,Итоговый заказ считается, +Total Order Considered,Всего рассмотренных заказов, Total Order Value,Общая стоимость заказа, Total Outgoing,Всего исходящих, Total Outstanding,Всего выдающихся, @@ -3092,12 +3092,12 @@ Total Paid Amount,Всего уплаченной суммы, Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total,Общая сумма платежа в Графе платежей должна быть равна Grand / Rounded Total, Total Payments,Всего платежей, Total Present,Итого Текущая, -Total Qty,Всего кол-во, +Total Qty,Общее количество, Total Quantity,Общая численность, Total Revenue,Общий доход, Total Student,Всего учеников, Total Target,Всего Target, -Total Tax,Совокупная налоговая, +Total Tax,Совокупный налог, Total Taxable Amount,Общая сумма налогооблагаемой суммы, Total Taxable Value,Общая налогооблагаемая стоимость, Total Unpaid: {0},Общая сумма невыплаченных: {0}, @@ -3121,33 +3121,33 @@ Total(Amt),Всего (сумма), Total(Qty),Всего (кол-во), Traceability,прослеживаемость, Traceback,Диагностика, -Track Leads by Lead Source.,Отслеживать Обращения по Источнику., +Track Leads by Lead Source.,Отслеживать лидов по источнику., Training,Обучение, Training Event,Учебное мероприятие, Training Events,Учебные мероприятия, Training Feedback,Обучение Обратная связь, Training Result,Результат обучения, Transaction,Транзакция, -Transaction Date,Сделка Дата, +Transaction Date,Дата транзакции, Transaction Type,Тип операции, Transaction currency must be same as Payment Gateway currency,"Валюта сделки должна быть такой же, как платежный шлюз валюты", Transaction not allowed against stopped Work Order {0},Транзакция не разрешена против прекращенного рабочего заказа {0}, Transaction reference no {0} dated {1},Референция сделка не {0} от {1}, -Transactions,операции, +Transactions,Транзакции, Transactions can only be deleted by the creator of the Company,Сделки могут быть удалены только создателем компании, -Transfer,Переложить, +Transfer,Передача, Transfer Material,О передаче материала, Transfer Type,Тип передачи, Transfer an asset from one warehouse to another,Передача актива с одного склада на другой, Transfered,Все передаваемые, Transferred Quantity,Переданное количество, Transport Receipt Date,Дата получения транспортного сообщения, -Transport Receipt No,Транспортная квитанция Нет, +Transport Receipt No,Транспортная квитанция №, Transportation,Транспортировка, Transporter ID,Идентификатор транспортника, Transporter Name,Название транспорта, Travel,Путешествия, -Travel Expenses,Командировочные Pасходы, +Travel Expenses,Командировочные расходы, Tree Type,Дерево Тип, Tree of Bill of Materials,Дерево Билла материалов, Tree of Item Groups.,Структура продуктовых групп, @@ -3181,11 +3181,11 @@ Unsubscribe from this Email Digest,Отписаться от этого дайд Unsubscribed,Отписался, Until,До, Unverified Webhook Data,Непроверенные данные Webhook, -Update Account Name / Number,Обновить имя учетной записи / номер, -Update Account Number / Name,Обновить номер / имя учетной записи, -Update Cost,Обновление Стоимость, +Update Account Name / Number,Обновить имя / номер счета, +Update Account Number / Name,Обновить номер / имя счета, +Update Cost,Обновить стоимость, Update Items,Обновить элементы, -Update Print Format,Обновление Формат печати, +Update Print Format,Обновить формат печати, Update Response,Обновить ответ, Update bank payment dates with journals.,Обновление банк платежные даты с журналов., Update in progress. It might take a while.,Идет обновление. Это может занять некоторое время., @@ -3196,7 +3196,7 @@ Upload your letter head and logo. (you can edit them later).,Загрузить Upper Income,Высокий уровень дохода, Use Sandbox,Использовать «песочницу», Used Leaves,Используемые листы, -User,пользователь, +User,Пользователь, User ID,ID пользователя, User ID not set for Employee {0},ID пользователя не установлен для сотрудника {0}, User Remark,Примечание Пользователь, @@ -3225,7 +3225,7 @@ Value for Attribute {0} must be within the range of {1} to {2} in the increments Value missing,Недостающее значение, Value must be between {0} and {1},Значение должно быть между {0} и {1}, "Values of exempt, nil rated and non-GST inward supplies","Значения льготных, нулевых и не связанных с GST внутренних поставок", -Variable,переменная, +Variable,Переменная, Variance,Дисперсия, Variance ({}),Дисперсия ({}), Variant,Вариант, @@ -3242,7 +3242,7 @@ View Chart of Accounts,Просмотр схемы счетов, View Fees Records,Посмотреть рекорды, View Form,Посмотреть форму, View Lab Tests,Просмотр лабораторных тестов, -View Leads,Посмотреть Обращения, +View Leads,Посмотреть лиды, View Ledger,Посмотреть Леджер, View Now,Просмотр сейчас, View a list of all the help videos,Просмотреть список всех справочных видео, @@ -3250,12 +3250,12 @@ View in Cart,Смотрите в корзину, Visit report for maintenance call.,Посетите отчет за призыв обслуживания., Visit the forums,Посетите форумы, Vital Signs,Жизненно важные признаки, -Volunteer,доброволец, +Volunteer,Волонтер, Volunteer Type information.,Информация о волонтере., Volunteer information.,Информация о волонтерах., Voucher #,Ваучер #, Voucher No,Ваучер №, -Voucher Type,Ваучер Тип, +Voucher Type,Тип ваучера, WIP Warehouse,WIP Склад, Walk In,Прогулка в, Warehouse can not be deleted as stock ledger entry exists for this warehouse.,"Склад не может быть удалён, так как существует запись в складкой книге этого склада.", @@ -3263,7 +3263,7 @@ Warehouse cannot be changed for Serial No.,Склад не может быть Warehouse is mandatory,Склад является обязательным, Warehouse is mandatory for stock Item {0} in row {1},Склад является обязательным для Запаса {0} в строке {1}, Warehouse not found in the system,Склад не найден в системе, -"Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}","Требуется хранилище в строке «Нет» {0}, пожалуйста, установите для хранилища по умолчанию для товара {1} для компании {2}", +"Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}","Требуется хранилище в строке № {0}, пожалуйста, установите для хранилища по умолчанию для товара {1} для компании {2}", Warehouse required for stock Item {0},Требуется Склад для Запаса {0}, Warehouse {0} can not be deleted as quantity exists for Item {1},Склад {0} не может быть удален как существует количество для Пункт {1}, Warehouse {0} does not belong to company {1},Склад {0} не принадлежит компания {1}, @@ -3299,7 +3299,7 @@ Welcome to ERPNext,Добро пожаловать в ERPNext, What do you need help with?,Как я могу вам помочь?, What does it do?,Что оно делает?, Where manufacturing operations are carried.,Где производственные операции проводятся., -White,белый, +White,Белый, Wire Transfer,Банковский перевод, WooCommerce Products,Продукты WooCommerce, Work In Progress,Незавершенная работа, @@ -3313,11 +3313,11 @@ Work Order {0} must be submitted,Порядок работы {0} должен б Work Orders Created: {0},Созданы рабочие задания: {0}, Work Summary for {0},Резюме работы для {0}, Work-in-Progress Warehouse is required before Submit,Работа-в-Прогресс Склад требуется перед Отправить, -Workflow,Поток, +Workflow, Рабочий процесс, Working,Работающий, Working Hours,Часы работы, -Workstation,рабочая станция, -Workstation is closed on the following dates as per Holiday List: {0},Рабочая станция закрыта в следующие сроки согласно Список праздников: {0}, +Workstation,Рабочая станция, +Workstation is closed on the following dates as per Holiday List: {0},Рабочая станция закрыта в следующие даты согласно списка праздников: {0}, Wrapping up,Завершение, Wrong Password,Неправильный пароль, Year start date or end date is overlapping with {0}. To avoid please set company,"Год дата начала или дата окончания перекрывается с {0}. Чтобы избежать, пожалуйста, установите компанию", @@ -3353,7 +3353,7 @@ Your Organization,Ваша организация, Your cart is Empty,Ваша корзина пуста, Your email address...,Ваш адрес электронной почты..., Your order is out for delivery!,Ваш заказ для доставки!, -Your tickets,Ваши билеты, +Your tickets,Ваши заявки, ZIP Code,Почтовый индекс, [Error],[Ошибка], [{0}](#Form/Item/{0}) is out of stock,[{0}](#Form/Item/{0}) нет в наличии, @@ -3364,10 +3364,10 @@ disabled user,отключенный пользователь, "e.g. ""Build tools for builders""","например ""Построить инструменты для строителей """, "e.g. ""Primary School"" or ""University""","например, "Начальная школа" или "Университет"", "e.g. Bank, Cash, Credit Card","например банк, наличные, кредитная карта", -hidden,Скрытый, +hidden,скрытый, modified,модифицированный, old_parent,old_parent, -on,Вкл, +on,вкл, {0} '{1}' is disabled,{0} '{1}' отключен, {0} '{1}' not in Fiscal Year {2},{0} '{1}' не в {2} Финансовом году, {0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3},{0} ({1}) не может быть больше запланированного количества ({2}) в рабочем порядке {3}, @@ -3389,7 +3389,7 @@ on,Вкл, {0} applicable after {1} working days,{0} применимо после {1} рабочих дней, {0} asset cannot be transferred,{0} актив не может быть перемещён, {0} can not be negative,{0} не может быть отрицательным, -{0} created,Создано {0}, +{0} created,{0} создано, "{0} currently has a {1} Supplier Scorecard standing, and Purchase Orders to this supplier should be issued with caution.","{0} в настоящее время имеет {1} систему показателей поставщика, и Заказы на поставку этому поставщику должны выдаваться с осторожностью.", "{0} currently has a {1} Supplier Scorecard standing, and RFQs to this supplier should be issued with caution.","{0} в настоящее время имеет {1} систему показателей поставщика, и RFQ для этого поставщика должны выдаваться с осторожностью.", {0} does not belong to Company {1},{0} не принадлежит компании {1}, @@ -3469,8 +3469,8 @@ on,Вкл, Assigned To,Назначено для, Chat,Чат, Completed By,Завершено, -Conditions,условия, -County,округ, +Conditions,Условия, +County,Округ, Day of Week,День недели, "Dear System Manager,","Уважаемый Менеджер системы,", Default Value,Значение по умолчанию, @@ -3483,11 +3483,11 @@ Help Articles,Статьи помощи, ID,ID, Images,Изображении, Import,Импорт, -Language,язык, -Likes,Понравившееся, +Language,Язык, +Likes,Лайки, Merge with existing,Слияние с существующими, Office,Офис, -Orientation,ориентация, +Orientation,Ориентация, Parent,Родитель, Passive,Пассивный, Payment Failed,Платеж не прошел, @@ -3499,25 +3499,25 @@ Post,Опубликовать, Postal,Почтовый, Postal Code,Почтовый индекс, Previous,Предыдущая, -Provider,поставщик, +Provider,Поставщик, Read Only,Только чтения, Recipient,Сторона-реципиент, Reviews,Отзывы, Sender,Отправитель, Shop,Магазин, -Sign Up,Подписаться, +Sign Up,Регистрация, Subsidiary,Филиал, There is some problem with the file url: {0},Существует некоторая проблема с файловой URL: {0}, There were errors while sending email. Please try again.,"При отправке электронной почты возникли ошибки. Пожалуйста, попробуйте ещё раз.", -Values Changed,Значения Изменено, +Values Changed,Значения изменено, or,или, Ageing Range 4,Диапазон старения 4, Allocated amount cannot be greater than unadjusted amount,Выделенная сумма не может быть больше нескорректированной, Allocated amount cannot be negative,Выделенная сумма не может быть отрицательной, "Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry","Разница счета должна быть учетной записью типа актива / пассива, так как эта запись акции является вводной", Error in some rows,Ошибка в некоторых строках, -Import Successful,Импорт успешен, -Please save first,"Пожалуйста, сохраните сначала", +Import Successful,Импорт успешно завершен, +Please save first,"Пожалуйста, сначала сохраните", Price not found for item {0} in price list {1},Цена не найдена для товара {0} в прайс-листе {1}, Warehouse Type,Тип склада, 'Date' is required,Требуется дата, @@ -3535,7 +3535,7 @@ Make Stock Entry,Сделать складской запас, Quality Feedback,Отзыв о качестве, Quality Feedback Template,Шаблон обратной связи по качеству, Rules for applying different promotional schemes.,Правила применения разных рекламных схем., -Shift,сдвиг, +Shift,Сдвиг, Show {0},Показать {0}, "Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Специальные символы, кроме "-", "#", ".", "/", "{" И "}", не допускаются в именных сериях", Target Details,Детали цели, @@ -3546,21 +3546,21 @@ Approved,Утверждено, Change,Изменение, Contact Email,Эл.почта для связи, Export Type,Тип экспорта, -From Date,С, +From Date,С даты, Group By,Группа по, Importing {0} of {1},Импорт {0} из {1}, Invalid URL,неправильный адрес, -Landscape,Пейзаж, +Landscape,Альбомный, Last Sync On,Последняя синхронизация, Naming Series,Идентификация по Имени, No data to export,Нет данных для экспорта, -Portrait,Портрет, +Portrait,Портретный, Print Heading,Распечатать Заголовок, Scheduler Inactive,Планировщик неактивен, Scheduler is inactive. Cannot import data.,Планировщик неактивен. Невозможно импортировать данные., Show Document,Показать документ, Show Traceback,Показать трассировку, -Video,видео, +Video,Видео, Webhook Secret,Webhook Secret, % Of Grand Total,% От общего итога, 'employee_field_value' and 'timestamp' are required.,'employee_field_value' и 'timestamp' являются обязательными., @@ -3583,7 +3583,7 @@ Accounting Period overlaps with {0},Отчетный период перекры Activity,Активность, Add / Manage Email Accounts.,Добавление / Управление учетными записями электронной почты, Add Child,Добавить потомка, -Add Loan Security,Добавить кредит безопасности, +Add Loan Security,Добавить обеспечение по кредиту, Add Multiple,Добавить несколько, Add Participants,Добавить участников, Add to Featured Item,Добавить в избранное, @@ -3641,12 +3641,12 @@ Book,Книга, Book Appointment,Назначение книги, Brand,Бренд, Browse,Обзор, -Call Connected,Call Connected, +Call Connected,Вызов подключен, Call Disconnected,Вызов отключен, Call Missed,Звонок пропущен, Call Summary,Сводка вызовов, Call Summary Saved,Сводка вызовов сохранена, -Cancelled,отменен, +Cancelled,Отменен, Cannot Calculate Arrival Time as Driver Address is Missing.,"Невозможно рассчитать время прибытия, так как отсутствует адрес водителя.", Cannot Optimize Route as Driver Address is Missing.,"Не удается оптимизировать маршрут, так как отсутствует адрес драйвера.", Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.,"Невозможно выполнить задачу {0}, поскольку ее зависимая задача {1} не завершена / не отменена.", @@ -3666,19 +3666,19 @@ Company,Организация, Company of asset {0} and purchase document {1} doesn't matches.,Компания актива {0} и документ покупки {1} не совпадают., Compare BOMs for changes in Raw Materials and Operations,Сравните спецификации для изменений в сырье и операциях, Compare List function takes on list arguments,Функция сравнения списка принимает аргументы списка, -Complete,полный, -Completed,Завершено, +Complete,Завершенно, +Completed,Завершенный, Completed Quantity,Завершенное количество, Connect your Exotel Account to ERPNext and track call logs,Подключите свою учетную запись Exotel к ERPNext и отслеживайте журналы вызовов, Connect your bank accounts to ERPNext,Подключите свои банковские счета к ERPNext, Contact Seller,Связаться с продавцом, -Continue,Продолжать, +Continue,Продолжить, Cost Center: {0} does not exist,МВЗ: {0} не существует, Couldn't Set Service Level Agreement {0}.,Не удалось установить соглашение об уровне обслуживания {0}., Country,Страна, Country Code in File does not match with country code set up in the system,"Код страны в файле не совпадает с кодом страны, установленным в системе", Create New Contact,Создать новый контакт, -Create New Lead,Создать новое руководство, +Create New Lead,Создать новый лид, Create Pick List,Создать список выбора, Create Quality Inspection for Item {0},Создать проверку качества для позиции {0}, Creating Accounts...,Создание аккаунтов ..., @@ -3716,7 +3716,7 @@ Download Template,Скачать шаблон, Dr,Доктор, Due Date,Дата выполнения, Duplicate,Дублировать, -Duplicate Project with Tasks,Дублирующий проект с задачами, +Duplicate Project with Tasks,Дублировать проект с задачами, Duplicate project has been created,Дублированный проект создан, E-Way Bill JSON can only be generated from a submitted document,E-Way Bill JSON может быть создан только из представленного документа, E-Way Bill JSON can only be generated from submitted document,E-Way Bill JSON может быть создан только из представленного документа, @@ -3750,10 +3750,10 @@ Expire Allocation,Expire Allocation, Expired,Истек срок действия, Export,Экспорт, Export not allowed. You need {0} role to export.,Экспорт не допускается. Вам нужно {0} роль для экспорта., -Failed to add Domain,Не удалось добавить домен, +Failed to add Domain,Не удалось вид деятельности, Fetch Items from Warehouse,Получить товары со склада, -Fetching...,Fetching ..., -Field,поле, +Fetching...,Получение..., +Field,Поле, File Manager,Файловый менеджер, Filters,Фильтры, Finding linked payments,Поиск связанных платежей, @@ -3777,7 +3777,7 @@ Get Outstanding Documents,Получить выдающиеся документ Goal,Цель, Greater Than Amount,"Больше, чем сумма", Green,Зеленый, -Group,группа, +Group,Группа, Group By Customer,Группировать по клиенту, Group By Supplier,Группа по поставщикам, Group Node,Узел Группа, @@ -3785,7 +3785,7 @@ Group Warehouses cannot be used in transactions. Please change the value of {0}, Help,Помощь, Help Article,Статья помощи, "Helps you keep tracks of Contracts based on Supplier, Customer and Employee","Помогает вам отслеживать контракты на основе поставщика, клиента и сотрудника", -Helps you manage appointments with your leads,Помогает вам управлять назначениями с вашими лидами, +Helps you manage appointments with your leads,Помогает вам управлять назначениями с вашими обращениями, Home,Главная, IBAN is not valid,IBAN недействителен, Import Data from CSV / Excel files.,Импорт данных из файлов CSV / Excel., @@ -3811,7 +3811,7 @@ Latest Age,Поздняя стадия, Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay,Заявка на отпуск связана с распределением отпуска {0}. Заявка на отпуск не может быть установлена как отпуск без оплаты, Leaves Taken,Листья взяты, Less Than Amount,Меньше чем сумма, -Liabilities,пассивы, +Liabilities,Обязательства, Loading...,Загрузка..., Loan Amount exceeds maximum loan amount of {0} as per proposed securities,Сумма кредита превышает максимальную сумму кредита {0} в соответствии с предлагаемыми ценными бумагами, Loan Applications from customers and employees.,Кредитные заявки от клиентов и сотрудников., @@ -3827,7 +3827,7 @@ Loan Security Value,Ценность займа, Loan Type for interest and penalty rates,Тип кредита для процентов и пеней, Loan amount cannot be greater than {0},Сумма кредита не может превышать {0}, Loan is mandatory,Кредит обязателен, -Loans,кредитование, +Loans,Кредиты, Loans provided to customers and employees.,"Кредиты, предоставленные клиентам и сотрудникам.", Location,Местоположение, Log Type is required for check-ins falling in the shift: {0}.,Тип регистрации необходим для регистрации заезда в смену: {0}., @@ -3844,11 +3844,11 @@ Missing Values Required,Не заполнены обязательные пол Mobile No,Мобильный номер, Mobile Number,Мобильный номер, Month,Mесяц, -Name,имя, +Name,Имя, Near you,Возле тебя, Net Profit/Loss,Чистая прибыль / убыток, -New Expense,Новый Расход, -New Invoice,Новый Счет, +New Expense,Новый расход, +New Invoice,Новый счет, New Payment,Новый платеж, New release date should be in the future,Дата нового релиза должна быть в будущем, Newsletter,Рассылка новостей, @@ -3857,7 +3857,7 @@ No Employee found for the given employee field value. '{}': {},Сотрудни No Leaves Allocated to Employee: {0} for Leave Type: {1},Сотрудникам не выделено ни одного листа: {0} для типа отпуска: {1}, No communication found.,Связь не найдена., No correct answer is set for {0},Не указан правильный ответ для {0}, -No description,без описания, +No description,Без описания, No issue has been raised by the caller.,Никакая проблема не была поднята вызывающим абонентом., No items to publish,Нет материалов для публикации, No outstanding invoices found,Не найдено неоплаченных счетов, @@ -3870,7 +3870,7 @@ Not Allowed,Не разрешено, Not allowed to create accounting dimension for {0},Не разрешено создавать учетное измерение для {0}, Not permitted. Please disable the Lab Test Template,"Не разрешено Пожалуйста, отключите шаблон лабораторного теста", Note,Заметки, -Notes: ,Заметки:, +Notes: ,Заметки: , On Converting Opportunity,О возможности конвертации, On Purchase Order Submission,При подаче заказа на поставку, On Sales Order Submission,На подаче заказа клиента, @@ -3879,9 +3879,9 @@ On {0} Creation,На {0} создании, Only .csv and .xlsx files are supported currently,В настоящее время поддерживаются только файлы .csv и .xlsx, Only expired allocation can be cancelled,Только истекшее распределение может быть отменено, Only users with the {0} role can create backdated leave applications,Только пользователи с ролью {0} могут создавать оставленные приложения с задним сроком действия, -Open,Создано, +Open,Открыт, Open Contact,Открытый контакт, -Open Lead,Открытое руководство, +Open Lead,Открытое обращение, Opening and Closing,Открытие и Закрытие, Operating Cost as per Work Order / BOM,Эксплуатационные расходы согласно заказу на работу / спецификации, Order Amount,Сумма заказа, @@ -3891,13 +3891,13 @@ Parent Company must be a group company,Материнская компания Passing Score value should be between 0 and 100,Проходной балл должен быть от 0 до 100, Password policy cannot contain spaces or simultaneous hyphens. The format will be restructured automatically,Политика паролей не может содержать пробелов или дефисов одновременно. Формат будет реструктурирован автоматически, Patient History,История пациента, -Pause,пауза, -Pay,Платить, +Pause,Пауза, +Pay,Оплатить, Payment Document Type,Тип платежного документа, Payment Name,Название платежа, Penalty Amount,Сумма штрафа, Pending,В ожидании, -Performance,Спектакль, +Performance,Производительность, Period based On,Период на основе, Perpetual inventory required for the company {0} to view this report.,"Постоянная инвентаризация требуется для компании {0}, чтобы просмотреть этот отчет.", Phone,Телефон, @@ -3943,8 +3943,8 @@ Priority,Приоритет, Priority has been changed to {0}.,Приоритет был изменен на {0}., Priority {0} has been repeated.,Приоритет {0} был повторен., Processing XML Files,Обработка файлов XML, -Profitability,рентабельность, -Project,проект, +Profitability,Рентабельность, +Project,Проект, Proposed Pledges are mandatory for secured Loans,Предлагаемые залоги являются обязательными для обеспеченных займов, Provide the academic year and set the starting and ending date.,Укажите учебный год и установите дату начала и окончания., Public token is missing for this bank,Публичный токен отсутствует для этого банка, @@ -3959,7 +3959,7 @@ Purchase Invoice cannot be made against an existing asset {0},Счет-факт Purchase Invoices,Счета на покупку, Purchase Orders,Заказы, Purchase Receipt doesn't have any Item for which Retain Sample is enabled.,"В квитанции о покупке нет ни одного предмета, для которого включена функция сохранения образца.", -Purchase Return,Покупка Вернуться, +Purchase Return,Возврат покупки, Qty of Finished Goods Item,Кол-во готовых товаров, Qty or Amount is mandatroy for loan security,Кол-во или сумма является мандатрой для обеспечения кредита, Quality Inspection required for Item {0} to submit,Инспекция по качеству требуется для отправки элемента {0}, @@ -3977,11 +3977,11 @@ Reconcile this account,Примирить этот аккаунт, Reconciled,Примирение, Recruitment,Набор персонала, Red,Красный, -Refreshing,освежение, +Refreshing,Обновление, Release date must be in the future,Дата релиза должна быть в будущем, Relieving Date must be greater than or equal to Date of Joining,Дата освобождения должна быть больше или равна дате присоединения, Rename,Переименовать, -Rename Not Allowed,Переименовать не разрешено, +Rename Not Allowed,Переименовывать запрещено, Repayment Method is mandatory for term loans,Метод погашения обязателен для срочных кредитов, Repayment Start Date is mandatory for term loans,Дата начала погашения обязательна для срочных кредитов, Report Item,Элемент отчета, @@ -3993,7 +3993,7 @@ Resetting Service Level Agreement.,Сброс соглашения об уров Return amount cannot be greater unclaimed amount,Возврат суммы не может быть больше невостребованной суммы, Review,Обзор, Room,Комната, -Room Type,Тип номера, +Room Type,Тип комнаты, Row # ,Ряд #, Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same,Строка # {0}: принятый склад и склад поставщика не могут быть одинаковыми, Row #{0}: Cannot delete item {1} which has already been billed.,"Строка # {0}: невозможно удалить элемент {1}, для которого уже выставлен счет.", @@ -4023,9 +4023,9 @@ Save Item,Сохранить элемент, Saved Items,Сохраненные предметы, Search Items ...,Поиск предметов ..., Search for a payment,Поиск платежа, -Search for anything ...,Ищите что-нибудь ..., +Search for anything ...,Искать что угодно ..., Search results for,Результаты поиска, -Select All,Выбрать Все, +Select All,Выбрать все, Select Difference Account,Выберите учетную запись разницы, Select a Default Priority.,Выберите приоритет по умолчанию., Select a company,Выберите компанию, @@ -4060,7 +4060,7 @@ Something went wrong while evaluating the quiz.,Что-то пошло не та Sr,Sr, Start,Начать, Start Date cannot be before the current date,Дата начала не может быть раньше текущей даты, -Start Time,Время, +Start Time,Время начала, Status,Статус, Status must be Cancelled or Completed,Статус должен быть отменен или завершен, Stock Balance Report,Отчет об остатках на складе, @@ -4069,7 +4069,7 @@ Stock Ledger ID,ID главной книги, Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,Стоимость запаса ({0}) и остаток на счете ({1}) не синхронизированы для счета {2} и связанных хранилищ., Stores - {0},Магазины - {0}, Student with email {0} does not exist,Студент с электронной почтой {0} не существует, -Submit Review,добавить отзыв, +Submit Review,Добавить отзыв, Submitted,Проведенный, Supplier Addresses And Contacts,Адреса и контакты поставщика, Synchronize this account,Синхронизировать этот аккаунт, @@ -4101,7 +4101,7 @@ This employee already has a log with the same timestamp.{0},У этого сот This page keeps track of items you want to buy from sellers.,"На этой странице отслеживаются товары, которые вы хотите купить у продавцов.", This page keeps track of your items in which buyers have showed some interest.,"Эта страница отслеживает ваши товары, к которым покупатели проявили определенный интерес.", Thursday,Четверг, -Timing,тайминг, +Timing,Сроки, Title,Заголовок, "To allow over billing, update ""Over Billing Allowance"" in Accounts Settings or the Item.","Чтобы разрешить чрезмерную оплату, обновите «Разрешение на чрезмерную оплату» в настройках учетных записей или элемента.", "To allow over receipt / delivery, update ""Over Receipt/Delivery Allowance"" in Stock Settings or the Item.","Чтобы разрешить перерасход / доставку, обновите параметр «Сверх квитанция / доставка» в настройках запаса или позиции.", @@ -4132,7 +4132,7 @@ Update Details,Обновить данные, Update Taxes for Items,Обновить налоги на товары, "Upload a bank statement, link or reconcile a bank account","Загрузить выписку из банковского счета, связать или сверить банковский счет", Upload a statement,Загрузить заявление, -Use a name that is different from previous project name,"Используйте имя, которое отличается от предыдущего названия проекта", +Use a name that is different from previous project name,"Используйте название, которое отличается от предыдущего названия проекта", User {0} is disabled,Пользователь {0} отключен, Users and Permissions,Пользователи и Права, Vacancies cannot be lower than the current openings,Вакансии не могут быть ниже текущих вакансий, @@ -4141,7 +4141,7 @@ Valuation Rate required for Item {0} at row {1},Коэффициент оцен Values Out Of Sync,Значения не синхронизированы, Vehicle Type is required if Mode of Transport is Road,"Тип транспортного средства требуется, если вид транспорта - дорога", Vendor Name,Имя продавца, -Verify Email,подтвердить электронную почту, +Verify Email,Подтвердить Email, View,Посмотреть, View all issues from {0},Просмотреть все проблемы от {0}, View call log,Просмотр журнала звонков, @@ -4157,9 +4157,9 @@ Yearly,Ежегодно, You,Вы, You are not allowed to enroll for this course,Вы не можете записаться на этот курс, You are not enrolled in program {0},Вы не зарегистрированы в программе {0}, -You can Feature upto 8 items.,Вы можете добавить до 8 предметов., +You can Feature upto 8 items.,Вы можете добавить до 8 элементов., You can also copy-paste this link in your browser,Ещё можно скопировать эту ссылку в браузер, -You can publish upto 200 items.,Вы можете опубликовать до 200 пунктов., +You can publish upto 200 items.,Вы можете опубликовать до 200 элементов., You have to enable auto re-order in Stock Settings to maintain re-order levels.,"Вы должны включить автоматический повторный заказ в настройках запаса, чтобы поддерживать уровни повторного заказа.", You must be a registered supplier to generate e-Way Bill,Вы должны быть зарегистрированным поставщиком для создания электронного билля, You need to login as a Marketplace User before you can add any reviews.,"Вам необходимо войти в систему как пользователь Marketplace, чтобы добавить какие-либо отзывы.", @@ -4193,22 +4193,22 @@ Total Income This Year,Общий доход в этом году, Barcode,Штрих-код, Bold,Жирный, Center,Центр, -Clear,ясно, +Clear,Отчистить, Comment,Комментарий, Comments,Комментарии, DocType,DocType, Download,Скачать, Left,Слева, -Link,Ссылка на сайт, -New,новый, +Link,Ссылка, +New,Новый, Not Found,Не найдено, Print,Распечатать, Reference Name,Имя ссылки, Refresh,Обновить, -Success,успех, +Success,Успешно, Time,Время, Value,Значение, -Actual,фактический, +Actual,Актуальность, Add to Cart,добавить в корзину, Days Since Last Order,Дней с последнего заказа, In Stock,В наличии, @@ -4222,15 +4222,15 @@ Received From,Получено от, Sales Person,Продавец, To date cannot be before From date,На сегодняшний день не может быть раньше От даты, Write Off,Списать, -{0} Created,Создано {0}, +{0} Created,{0} Создано, Email Id,Email ID, -No,нет, +No,№, Reference Doctype,Ссылка DocType, User Id,Идентификатор пользователя, -Yes,да, +Yes,Да, Actual ,Фактически, Add to cart,Добавить в корзину, -Budget,бюджет, +Budget,Бюджет, Chart of Accounts,План счетов, Customer database.,База данных клиентов., Days Since Last order,Дни с последнего Заказать, @@ -4244,7 +4244,7 @@ Item name,Название продукта, Loan amount is mandatory,Сумма кредита обязательна, Minimum Qty,Минимальное количество, More details,Больше параметров, -Nature of Supplies,Природа поставок, +Nature of Supplies,Характер поставок, No Items found.,Ничего не найдено., No employee found,Сотрудник не найден, No students found,Нет студентов не найдено, @@ -4252,10 +4252,10 @@ Not in stock,Нет в наличии, Not permitted,Не разрешено, Open Issues ,Открыть вопросы, Open Projects ,Открыть проекты, -Open To Do ,Открыть список дел, +Open To Do ,Открыть список задач, Operation Id,Код операции, Partially ordered,Частично заказанно, -Please select company first,"Пожалуйста, выберите КОМПАНИЯ Первый", +Please select company first,Сначала выберите компанию, Please select patient,Выберите пациента, Printed On ,Напечатано на, Projected qty,Прогнозируемое кол-во, @@ -4268,14 +4268,14 @@ To date cannot be before from date,До даты не может быть ран Total Taxable value,Общая налогооблагаемая стоимость, Upcoming Calendar Events ,Предстоящие события календаря, Value or Qty,Значение или кол-во, -Variance ,отклонение, +Variance ,Расхождение , Variant of,Вариант, Write off,Списать, hours,часов, received from,получено от, to,для, Cards,Карты, -Percentage,процент, +Percentage,Процент, Failed to setup defaults for country {0}. Please contact support@erpnext.com,"Не удалось установить значения по умолчанию для страны {0}. Пожалуйста, свяжитесь с support@erpnext.com", Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.,Строка # {0}: элемент {1} не является сериализованным / пакетным элементом. Он не может иметь серийный номер / пакетный номер против него., Please set {0},"Пожалуйста, установите {0}", @@ -4319,7 +4319,7 @@ Vimeo,Vimeo, Publish Date,Дата публикации, Duration,Продолжительность, Advanced Settings,Расширенные настройки, -Path,Дорожка, +Path,Путь, Components,Компоненты, Verified By,Утверждено, Invalid naming series (. missing) for {0},Недопустимая серия имен (. Отсутствует) для {0}, @@ -4363,7 +4363,7 @@ No leave record found for employee {0} on {1},Для сотрудника {0} {1 Row {0}: {1} is required in the expenses table to book an expense claim.,Строка {0}: {1} требуется в таблице расходов для регистрации претензии по расходам., Set the default account for the {0} {1},Установите учетную запись по умолчанию для {0} {1}, (Half Day),(Полдня), -Income Tax Slab,Плита подоходного налога, +Income Tax Slab,Подоходный налог, Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary,Строка № {0}: невозможно установить сумму или формулу для компонента заработной платы {1} с переменной на основе налогооблагаемой заработной платы., Row #{}: {} of {} should be {}. Please modify the account or select a different account.,Строка № {}: {} из {} должно быть {}. Измените учетную запись или выберите другую учетную запись., Row #{}: Please asign task to a member.,Строка № {}: назначьте задачу участнику., @@ -4438,12 +4438,12 @@ Patient Appointments,Запись на прием к пациенту, Item with Item Code {0} already exists,Товар с кодом товара {0} уже существует, Registration Fee cannot be negative or zero,Регистрационный взнос не может быть отрицательным или нулевым., Configure a service Item for {0},Настроить сервисный элемент для {0}, -Temperature: ,Температура:, -Pulse: ,Пульс:, -Respiratory Rate: ,Частота дыхания:, -BP: ,АД:, -BMI: ,ИМТ:, -Note: ,Примечание:, +Temperature: ,Температура: , +Pulse: ,Пульс: , +Respiratory Rate: ,Частота дыхания: , +BP: ,АД :, +BMI: ,ИМТ: , +Note: ,Примечание: , Check Availability,Проверить наличие свободных мест, Please select Patient first,"Пожалуйста, сначала выберите пациента", Please select a Mode of Payment first,"Пожалуйста, сначала выберите способ оплаты", @@ -4476,9 +4476,9 @@ Clinical Procedure ({0}):,Клиническая процедура ({0}):, Please set Customer in Patient {0},Установите клиента в пациенте {0}, Item {0} is not active,Пункт {0} не активен, Therapy Plan {0} created successfully.,План терапии {0} успешно создан., -Symptoms: ,Симптомы:, +Symptoms: ,Симптомы: , No Symptoms,Нет симптомов, -Diagnosis: ,Диагноз:, +Diagnosis: ,Диагноз: , No Diagnosis,Нет диагноза, Drug(s) Prescribed.,Выписанные лекарства., Test(s) Prescribed.,Предписанные испытания., @@ -4589,7 +4589,7 @@ Bank Transaction Entries,Записи банковских транзакций, New Transactions,Новые транзакции, Match Transaction to Invoices,Сопоставление транзакций с счетами-фактурами, Create New Payment/Journal Entry,Создать новую запись о платеже / журнале, -Submit/Reconcile Payments,Отправить / Согласовать платежи, +Submit/Reconcile Payments,Утвердить/Согласовать платежи, Matching Invoices,Сопоставление счетов-фактур, Payment Invoice Items,Платежные счета, Reconciled Transactions,Согласованные транзакции, @@ -4631,7 +4631,7 @@ ACC-CF-.YYYY.-,ACC-CF-.YYYY.-, C-Form No,C-образный Нет, Received Date,Дата получения, Quarter,Квартал, -I,Я, +I,I, II,II, III,III, IV,IV, @@ -4653,7 +4653,7 @@ Is Finance Cost Adjustment,Корректировка финансовых ра Is Income Tax Liability,Ответственность подоходного налога, Is Income Tax Expense,Расходы на подоходный налог, Cash Flow Mapping Accounts,Учетные записи денежных потоков, -account,Аккаунт, +account,аккаунт, Cash Flow Mapping Template,Шаблон сопоставления денежных потоков, Cash Flow Mapping Template Details,Подробное описание шаблонов движения денежных средств, POS-CLO-,POS-ClO-, @@ -4669,7 +4669,7 @@ Cheque Print Template,Чеками печати шаблона, Has Print Format,Имеет формат печати, Primary Settings,Основные настройки, Cheque Size,Cheque Размер, -Regular,регулярное, +Regular,Обычный, Starting position from top edge,Исходное положение от верхнего края, Cheque Width,Cheque Ширина, Cheque Height,Cheque Высота, @@ -4691,7 +4691,7 @@ Cost Center Name,Название учетного отдела, Parent Cost Center,Родитель МВЗ, lft,LFT, rgt,РТГ, -Coupon Code,код купона, +Coupon Code,Код купона, Coupon Name,Название купона, "e.g. ""Summer Holiday 2019 Offer 20""","например, "Летние каникулы 2019 Предложение 20"", Coupon Type,Тип купона, @@ -4913,7 +4913,7 @@ POS Customer Group,POS Группа клиентов, POS Field,POS Field, POS Item Group,POS Item Group, Company Address,Адрес компании, -Update Stock,Обновить склад, +Update Stock,Обновить остатки, Ignore Pricing Rule,Игнорировать правило ценообразования, Applicable for Users,Применимо для пользователей, Sales Invoice Payment,Накладная Оплата, @@ -5039,7 +5039,7 @@ Terms and Conditions1,Сроки и условиях1, Group same items,Сгруппировать похожие продукты, Print Language,Язык печати, "Once set, this invoice will be on hold till the set date",После этого этот счет будет приостановлен до установленной даты, -Credit To,Кредитная Для, +Credit To,Кредит для, Party Account Currency,Партия Валюта счета, Against Expense Account,Со счета расходов, Inter Company Invoice Reference,Справочная информация для Inter Company, @@ -5068,8 +5068,8 @@ Landed Cost Voucher Amount,Земельные стоимости путевки Raw Materials Supplied Cost,Стоимость поставленного сырья, Accepted Warehouse,Принимающий склад, Serial No,Серийный номер, -Rejected Serial No,Отклонен Серийный номер, -Expense Head,Расходов Глава, +Rejected Serial No,Отклоненный Серийный номер, +Expense Head,Глава расходов, Is Fixed Asset,Фиксирована Asset, Asset Location,Месторасположение активов, Deferred Expense,Отложенные расходы, @@ -5114,8 +5114,8 @@ Update Billed Amount in Sales Order,Обновить Выставленную С Customer PO Details,Детали заказа клиента, Customer's Purchase Order,Заказ клиента, Customer's Purchase Order Date,Клиентам Дата Заказ, -Customer Address,Клиент Адрес, -Shipping Address Name,Адрес доставки Имя, +Customer Address,Адрес клиента, +Shipping Address Name,Название адрес доставки, Company Address Name,Название компании, Rate at which Customer Currency is converted to customer's base currency,Курс по которому валюта Покупателя конвертируется в базовую валюту покупателя, Rate at which Price list currency is converted to customer's base currency,Курс по которому валюта Прайс листа конвертируется в базовую валюту покупателя, @@ -5123,9 +5123,9 @@ Set Source Warehouse,Установить исходный склад, Packing List,Список упаковки, Packed Items,Упакованные продукты, Product Bundle Help,Продукт Связка Помощь, -Time Sheet List,Список времени лист, -Time Sheets,Time Sheets, -Total Billing Amount,Всего счетов Сумма, +Time Sheet List,Список табелей учета рабочего времени, +Time Sheets,Табель учета рабочего времени, +Total Billing Amount,Общая сумма счета, Sales Taxes and Charges Template,Продажи Налоги и сборы шаблона, Sales Taxes and Charges,Налоги и сборы с продаж, Loyalty Points Redemption,Выкуп лояльности очков, @@ -5170,19 +5170,19 @@ Available Qty at Warehouse,Доступное Кол-во на складе, Delivery Note Item,Доставляемый продукт, Base Amount (Company Currency),Базовая сумма (Компания Валюта), Sales Invoice Timesheet,Счет по табелю, -Time Sheet,Время Sheet, -Billing Hours,Платежная часы, -Timesheet Detail,Timesheet Деталь, +Time Sheet,Табель учета рабочего времени, +Billing Hours,Оплачеваемые часы, +Timesheet Detail,Сведения о расписании, Tax Amount After Discount Amount (Company Currency),Сумма налога после скидки Сумма (Компания валют), -Item Wise Tax Detail,Пункт Мудрый Налоговый Подробно, +Item Wise Tax Detail,Подробная информация о налоге на товар, Parenttype,ParentType, "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like ""Shipping"", ""Insurance"", ""Handling"" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on ""Previous Row Total"" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.","Стандартный шаблон налог, который может быть применен ко всем сделок купли-продажи. Этот шаблон может содержать перечень налоговых руководителей, а также других глав расходы / доходы, как ""Shipping"", ""Insurance"", ""Обращение"" и т.д. \n\n #### Примечание \n\n ставка налога на Вы Определить здесь будет стандартная ставка налога на прибыль для всех ** деталей **. Если есть ** товары **, которые имеют различные цены, они должны быть добавлены в ** деталь налога ** стол в ** деталь ** мастера.\n\n #### Описание колонок \n\n 1. Расчет Тип: \n - Это может быть ** Чистый Всего ** (то есть сумма основной суммы).\n - ** На предыдущей строке Total / сумма ** (по совокупности налогов и сборов). Если вы выбираете эту опцию, налог будет применяться в процентах от предыдущего ряда (в налоговом таблицы) суммы или объема.\n - ** ** Фактический (как уже упоминалось).\n 2. Счет Руководитель: лицевому счету, при которых этот налог будут забронированы \n 3. Центр Стоимость: Если налог / налог на заряд доход (как перевозка груза) или расходов это должен быть забронирован на МВЗ.\n 4. Описание: Описание налога (которые будут напечатаны в счетах-фактурах / кавычек).\n 5. Оценить: Налоговая ставка.\n 6. Количество: Сумма налога.\n 7. Всего: Суммарное к этой точке.\n 8. Введите Row: Если на базе ""Предыдущая сумма по строке"" вы можете выбрать номер строки которой будет приниматься в качестве основы для такого расчета (по умолчанию предыдущего ряда).\n 9. Это налог Включено в основной ставке ?: Если вы посмотрите, это значит, что этот налог не будет показано ниже в таблице элементов, но будет включен в основной ставке в основной таблице элементов. Это полезно, если вы хотите дать квартира Цена (включая все налоги) цену к клиентам.", * Will be calculated in the transaction.,* Будет рассчитана в сделке., -From No,От Нет, -To No,Нет, -Is Company,Является ли компания, +From No,От №, +To No,К №, +Is Company,Это компания, Current State,Текущее состояние, -Purchased,купленный, +Purchased,Купленный, From Shareholder,От акционеров, From Folio No,Из Folio No, To Shareholder,Акционеру, @@ -5223,7 +5223,7 @@ Days Until Due,Дни до срока, Number of days that the subscriber has to pay invoices generated by this subscription,"Количество дней, в течение которых абонент должен оплатить счета, сгенерированные этой подпиской", Cancel At End Of Period,Отмена на конец периода, Generate Invoice At Beginning Of Period,Сформировать счет в начале периода, -Plans,планы, +Plans,Планы, Discounts,Скидки, Additional DIscount Percentage,Процент Дополнительной Скидки, Additional DIscount Amount,Сумма Дополнительной Скидки, @@ -5307,7 +5307,7 @@ Detected Diseases,Обнаруженные заболевания, List of diseases detected on the field. When selected it'll automatically add a list of tasks to deal with the disease ,"Список заболеваний, обнаруженных на поле. При выборе он автоматически добавит список задач для борьбы с болезнью", Detected Disease,Обнаруженная болезнь, LInked Analysis,Анализ LInked, -Disease,болезнь, +Disease,Болезнь, Tasks Created,Созданные задачи, Common Name,Распространенное имя, Treatment Task,Лечебная задача, @@ -5326,10 +5326,10 @@ Plant Analysis Criterias,Критерий анализа оборудовани Plant Analysis Criteria,Критерии анализа производства, Minimum Permissible Value,Минимальное допустимое значение, Maximum Permissible Value,Максимально допустимое значение, -Ca/K,Са / К, -Ca/Mg,Са / Mg, -Mg/K,Мг / К, -(Ca+Mg)/K,(Са + Mg) / К, +Ca/K,Са/К, +Ca/Mg,Са/Mg, +Mg/K,Мг/К, +(Ca+Mg)/K,(Са+Mg)/К, Ca/(K+Ca+Mg),Са / (К + Са + Mg), Soil Analysis Criterias,Критерий анализа почвы, Soil Analysis Criteria,Критерии оценки почвы, @@ -5379,7 +5379,7 @@ Depreciation Schedule,Амортизация Расписание, Depreciation Schedules,Амортизационные Расписания, Insurance details,Детали страхования, Policy number,Номер полиса, -Insurer,страхователь, +Insurer,Страхователь, Insured value,Страховое значение, Insurance Start Date,Дата начала страхования, Insurance End Date,Дата окончания страхования, @@ -5453,8 +5453,8 @@ Parent Location,Расположение родителей, Is Container,Контейнер, Check if it is a hydroponic unit,"Проверьте, является ли это гидропонной единицей", Location Details,Информация о местоположении, -Latitude,широта, -Longitude,долгота, +Latitude,Широта, +Longitude,Долгота, Area,Площадь, Area UOM,Область UOM, Tree Details,Детали Дерево, @@ -5515,13 +5515,13 @@ Request for Quotation Item,Запрос на предложение продук Required Date,Требуемая дата, Request for Quotation Supplier,Запрос на предложение поставщика, Send Email,Отправить письмо, -Quote Status,Статус цитаты, +Quote Status,Статус предложения, Download PDF,Скачать PDF, Supplier of Goods or Services.,Поставщик товаров или услуг., Name and Type,Наименование и тип, SUP-.YYYY.-,SUP-.YYYY.-, Default Bank Account,По умолчанию Банковский счет, -Is Transporter,Является Transporter, +Is Transporter,Является перевозчиком, Represents Company,Представляет компанию, Supplier Type,Тип поставщика, Allow Purchase Invoice Creation Without Purchase Order,Разрешить создание счета-фактуры без заказа на покупку, @@ -5574,16 +5574,16 @@ Supplier Scorecard Period,Период оценки поставщика, PU-SSP-.YYYY.-,PU-SSP-.YYYY.-, Period Score,Период, Calculations,вычисления, -Criteria,критерии, -Variables,переменные, +Criteria,Критерии, +Variables,Переменные, Supplier Scorecard Setup,Поставщик Scorecard Setup, Supplier Scorecard Scoring Criteria,Критерии оценки поставщика, Score,Балл, Supplier Scorecard Scoring Standing,Поставщик Scorecard Scoring Standing, Standing Name,Постоянное имя, Purple,Пурпурный, -Yellow,жёлтый, -Orange,оранжевый, +Yellow,Жёлтый, +Orange,Оранжевый, Min Grade,Min Grade, Max Grade,Макс. Класс, Warn Purchase Orders,Предупреждать заказы на поставку, @@ -5598,16 +5598,16 @@ Supplier Scorecard Variable,Переменная поставщика Scorecard, Call Log,Журнал вызовов, Received By,Получено, Caller Information,Информация о звонящем, -Contact Name,Имя Контакта, -Lead ,вести, -Lead Name,Имя, -Ringing,звонкий, +Contact Name,Имя контакта, +Lead ,Лид , +Lead Name,Имя лида, +Ringing,Звонок, Missed,Пропущенный, Call Duration in seconds,Продолжительность звонка в секундах, Recording URL,Запись URL, Communication Medium,Связь Средний, Communication Medium Type,Тип средств связи, -Voice,голос, +Voice,Голос, Catch All,Поймать все, "If there is no assigned timeslot, then communication will be handled by this group","Если нет назначенного временного интервала, то связь будет обрабатываться этой группой", Timeslots,Временные интервалы, @@ -5615,10 +5615,10 @@ Communication Medium Timeslot,Коммуникационный средний т Employee Group,Группа сотрудников, Appointment,"Деловое свидание, встреча", Scheduled Time,Назначенное время, -Unverified,непроверенный, +Unverified,Непроверенный, Customer Details,Данные клиента, Phone Number,Телефонный номер, -Skype ID,Скайп ай-ди, +Skype ID,Skype ID, Linked Documents,Связанные документы, Appointment With,Встреча с, Calendar Event,Календарь событий, @@ -5638,22 +5638,22 @@ Success Redirect URL,URL-адрес успешного перенаправле "Leave blank for home.\nThis is relative to site URL, for example ""about"" will redirect to ""https://yoursitename.com/about""","Оставьте пустым для дома. Это относительно URL сайта, например, «about» будет перенаправлен на «https://yoursitename.com/about»", Appointment Booking Slots,Назначение Бронирование Слоты, Day Of Week,День недели, -From Time ,С, +From Time ,С , Campaign Email Schedule,Расписание рассылки кампании, Send After (days),Отправить после (дней), Signed,подписанный, Party User,Пользователь Party, -Unsigned,неподписанный, +Unsigned,Неподписанный, Fulfilment Status,Статус выполнения, -N/A,N / A, +N/A,N/A, Unfulfilled,невыполненный, Partially Fulfilled,Частично выполнено, Fulfilled,Исполненная, Lapsed,Просроченные, Contract Period,Контрактный период, Signee Details,Информация о подписчике, -Signee,грузополучатель, -Signed On,Подпись, +Signee,Грузополучатель, +Signed On,Подписано, Contract Details,Информация о контракте, Contract Template,Шаблон контракта, Contract Terms,Условия договора, @@ -5662,7 +5662,7 @@ Requires Fulfilment,Требуется выполнение, Fulfilment Deadline,Срок выполнения, Fulfilment Terms,Условия выполнения, Contract Fulfilment Checklist,Контрольный список выполнения контракта, -Requirement,требование, +Requirement,Требование, Contract Terms and Conditions,Условия договора, Fulfilment Terms and Conditions,Сроки и условия выполнения, Contract Template Fulfilment Terms,Условия заключения контрактов, @@ -5683,7 +5683,7 @@ Next Contact Date,Дата следующего контакта, Ends On,Заканчивается, Address & Contact,Адрес и контакт, Mobile No.,Мобильный, -Lead Type,Тип Обращения, +Lead Type,Тип лидов, Channel Partner,Партнер, Consultant,Консультант, Market Segment,Сегмент рынка, @@ -5705,9 +5705,9 @@ Lost Reason Detail,Потерянная причина подробно, Opportunity Lost Reason,Возможность потерянной причины, Potential Sales Deal,Сделка потенциальных продаж, CRM-OPP-.YYYY.-,CRM-ОПП-.YYYY.-, -Opportunity From,Выявление из, -Customer / Lead Name,Имя Клиента / Обращения, -Opportunity Type,Тип Выявления, +Opportunity From,Возможность из, +Customer / Lead Name,Имя Клиента / Лида, +Opportunity Type,Тип возможности, Converted By,Преобразовано, Sales Stage,Этап продажи, Lost Reason,Забыли Причина, @@ -5716,7 +5716,7 @@ To Discuss,Для обсуждения, With Items,С продуктами, Probability (%),Вероятность (%), Contact Info,Контактная информация, -Customer / Lead Address,Адрес Клиента / Обращения, +Customer / Lead Address,Адрес Клиента / Лида, Contact Mobile No,Связаться Мобильный Нет, Enter name of campaign if source of enquiry is campaign,"Введите имя кампании, если источником исследования является кампания", Opportunity Date,Дата Выявления, @@ -5747,7 +5747,7 @@ Parent Assessment Group,Родительская группа по оценке, Assessment Name,Название оценки, Grading Scale,Оценочная шкала, Examiner,экзаменатор, -Examiner Name,Имя Examiner, +Examiner Name,Имя экзаменатора, Supervisor,Руководитель, Supervisor Name,Имя супервизора, Evaluate,оценивать, @@ -5765,7 +5765,7 @@ Last Activity ,Последняя активность, Content Question,Содержание вопроса, Question Link,Ссылка на вопрос, Course Name,Название курса, -Topics,темы, +Topics,Темы, Hero Image,Образ героя, Default Grading Scale,Шкала оценок, Education Manager,Менеджер по образованию, @@ -5775,7 +5775,7 @@ Activity Date,Дата деятельности, Course Assessment Criteria,Критерии оценки курса, Weightage,Добавка, Course Content,Содержание курса, -Quiz,викторина, +Quiz,Викторина, Program Enrollment,Программа подачи заявок, Enrollment Date,Дата поступления, Instructor Name,Имя инструктора, @@ -5785,7 +5785,7 @@ Course Start Date,Дата начала курса, To TIme,До, Course End Date,Дата окончания курса, Course Topic,Тема курса, -Topic,тема, +Topic,Тема, Topic Name,Название темы, Education Settings,Настройки образования, Current Academic Year,Текущий академический год, @@ -5845,11 +5845,11 @@ Guardian Student,Хранитель Студент, EDU-INS-.YYYY.-,EDU-INS-.YYYY.-, Instructor Log,Журнал инструктора, Other details,Другие детали, -Option,вариант, +Option,Вариант, Is Correct,Верно, Program Name,Название программы, Program Abbreviation,Программа Аббревиатура, -Courses,курсы, +Courses,Курсы, Is Published,Опубликовано, Allow Self Enroll,Разрешить самостоятельную регистрацию, Is Featured,Показано, @@ -5910,7 +5910,7 @@ A+,A+, A-,A-, B+,B+, B-,B-, -O+,O +, +O+,O+, O-,О-, AB+,AB+, AB-,AB-, @@ -5991,17 +5991,17 @@ Market Place ID,Идентификатор рынка, AE,AE, AU,AU, BR,BR, -CA,Калифорния, +CA,CA, CN,CN, -DE,Делавэр, +DE,DE, ES,ES, FR,FR, -IN,В, +IN,IN, JP,JP, -IT,ЭТО, +IT,IT, MX,MX, -UK,Великобритания, -US,НАС, +UK,UK, +US,US, Customer Type,Тип клиента, Market Place Account Group,Группа учета рынка, After Date,После даты, @@ -6018,19 +6018,19 @@ Max Retry Limit,Максимальный лимит регрессии, Exotel Settings,Настройки Exotel, Account SID,SID аккаунта, API Token,API-токен, -GoCardless Mandate,Безрукий мандат, -Mandate,мандат, -GoCardless Customer,Без комиссии, -GoCardless Settings,Настройки бездорожья, -Webhooks Secret,Секретные клипы, -Plaid Settings,Настройки пледа, +GoCardless Mandate,GoCardless мандат, +Mandate,Мандат, +GoCardless Customer,Клиент GoCardless, +GoCardless Settings,Настройки GoCardless, +Webhooks Secret,Webhooks Secret, +Plaid Settings,Настройки Plaid, Synchronize all accounts every hour,Синхронизировать все учетные записи каждый час, Plaid Client ID,Идентификатор клиента, -Plaid Secret,Плед секрет, -Plaid Environment,Плед среды, +Plaid Secret,Plaid Secret, +Plaid Environment,Plaid Environment, sandbox,песочница, -development,развитие, -production,производство, +development,development, +production,production, QuickBooks Migrator,QuickBooks Migrator, Application Settings,Настройки приложения, Token Endpoint,Конечная точка маркера, @@ -6044,9 +6044,9 @@ Default Shipping Account,Учетная запись по умолчанию, Default Warehouse,Склад по умолчанию, Default Cost Center,По умолчанию Центр Стоимость, Undeposited Funds Account,Учет нераспределенных средств, -Shopify Log,Shopify Вход, +Shopify Log,Логи Shopify, Request Data,Запросить данные, -Shopify Settings,Изменить настройки, +Shopify Settings,Shopify настройки, status html,статус html, Enable Shopify,Включить Shopify, App Type,Тип приложения, @@ -6068,7 +6068,7 @@ Import Delivery Notes from Shopify on Shipment,Импорт уведомлени Delivery Note Series,Идентификаторы документов доставки, Import Sales Invoice from Shopify if Payment is marked,"Импортировать счет-фактуру продавца, чтобы узнать, отмечен ли платеж", Sales Invoice Series,Идентификаторы счетов продаж, -Shopify Tax Account,Уточнить налоговый счет, +Shopify Tax Account,Shopify налоговый счет, Shopify Tax/Shipping Title,Изменить название налога / доставки, ERPNext Account,Учетная запись ERPNext, Shopify Webhook Detail,Узнайте подробности веб-камеры, @@ -6193,7 +6193,7 @@ Allow Overlap,Разрешить перекрытие, Inpatient Occupancy,Стационарное размещение, Occupancy Status,Статус занятости, Vacant,Вакантно, -Occupied,занятый, +Occupied,Занято, Item Details,Детальная информация о товаре, UOM Conversion in Hours,Преобразование UOM в часы, Rate / UOM,Скорость / UOM, @@ -6296,10 +6296,10 @@ Personal and Social History,Личная и социальная история, Marital Status,Семейное положение, Married,Замужем, Divorced,Разведенный, -Widow,вдова, +Widow,Вдова, Patient Relation,Отношение пациентов, "Allergies, Medical and Surgical History","Аллергии, медицинская и хирургическая история", -Allergies,аллергии, +Allergies,Аллергии, Medication,медикаментозное лечение, Medical History,История болезни, Surgical History,Хирургическая история, @@ -6344,7 +6344,7 @@ Encounter Impression,Впечатление от Encounter, Symptoms,Симптомы, In print,В печати, Medical Coding,Медицинское кодирование, -Procedures,процедуры, +Procedures,Поцедуры, Therapies,Терапии, Review Details,Обзорная информация, Patient Encounter Diagnosis,Диагностика встречи с пациентом, @@ -6352,8 +6352,8 @@ Patient Encounter Symptom,Симптом встречи с пациентом, HLC-PMR-.YYYY.-,HLC-PMR-.YYYY.-, Attach Medical Record,Прикрепите медицинскую карту, Reference DocType,Ссылка DocType, -Spouse,супруга, -Family,семья, +Spouse,Супруга, +Family,Семья, Schedule Details,Детали расписания, Schedule Name,Название расписания, Time Slots,Временные интервалы, @@ -6390,9 +6390,9 @@ Furry,пушистый, Cuts,Порезы, Abdomen,Брюшная полость, Bloated,Раздутый, -Fluid,жидкость, +Fluid,Жидкость, Constipated,Запор, -Reflexes,рефлексы, +Reflexes,Рефлексы, Hyper,Hyper, Very Hyper,Очень Hyper, One Sided,Односторонняя, @@ -6419,7 +6419,7 @@ Hotel Room Pricing Package,Пакет услуг для гостиничных Hotel Room Reservation,Бронирование номеров в гостинице, Guest Name,Имя гостя, Late Checkin,Поздняя регистрация, -Booked,бронирования, +Booked,Забронировано, Hotel Reservation User,Бронирование отеля, Hotel Room Reservation Item,Бронирование номера в гостинице, Hotel Settings,Отель, @@ -6441,7 +6441,7 @@ Job Applicant,Соискатель работы, Applicant Name,Имя заявителя, Appointment Date,Назначенная дата, Appointment Letter Template,Шаблон письма о назначении, -Body,тело, +Body,Тело, Closing Notes,Заметки, Appointment Letter content,Письмо о назначении, Appraisal,Оценка, @@ -6456,12 +6456,12 @@ Key Responsibility Area,Ключ Ответственность Площадь, Weightage (%),Weightage (%), Score (0-5),Оценка (0-5), Score Earned,Оценка Заработано, -Appraisal Template Title,Оценка шаблона Название, -Appraisal Template Goal,Оценка шаблона Гол, +Appraisal Template Title,Название шаблона оценки, +Appraisal Template Goal,Цель шаблона оценки, KRA,КРА, -Key Performance Area,Ключ Площадь Производительность, +Key Performance Area,Ключевая область производительности, HR-ATT-.YYYY.-,HR-ATT-.YYYY.-, -On Leave,в отпуске, +On Leave,В отпуске, Work From Home,Работа из дома, Leave Application,Заявление на отпуск, Attendance Date,Посещаемость Дата, @@ -6476,10 +6476,10 @@ Leave Allocation,Распределение отпусков, Worked On Holiday,Работал на отдыхе, Work From Date,Работа с даты, Work End Date,Дата окончания работы, -Email Sent To,Е-мейл отправлен, +Email Sent To,Email отправлен, Select Users,Выберите пользователей, Send Emails At,Отправить электронные письма на, -Reminder,напоминание, +Reminder,Напоминание, Daily Work Summary Group User,Ежедневная рабочая группа, email,Эл. адрес, Parent Department,Родительский отдел, @@ -6495,7 +6495,7 @@ Designation Skill,Обозначение навыка, Skill,Умение, Driver,Водитель, HR-DRI-.YYYY.-,HR-DRI-.YYYY.-, -Suspended,подвешенный, +Suspended,Приостановленный, Transporter,Транспортер, Applicable for external driver,Применимо для внешнего драйвера, Cellphone Number,номер мобильного телефона, @@ -6614,7 +6614,7 @@ Under Graduate,Под Выпускник, Year of Passing,Год прохождения, Class / Percentage,Класс / в процентах, Major/Optional Subjects,Основные / факультативных предметов, -Employee External Work History,Сотрудник Внешний Работа История, +Employee External Work History,История внешней работы сотрудника, Total Experience,Суммарный опыт, Default Leave Policy,Политика по умолчанию, Default Salary Structure,Структура заработной платы по умолчанию, @@ -6628,7 +6628,7 @@ Employee Internal Work History,Сотрудник внутреннего Раб Employee Onboarding,Сотрудник по бортовому, Notify users by email,Уведомить пользователей по электронной почте, Employee Onboarding Template,Шаблон рабочего стола, -Activities,мероприятия, +Activities,Мероприятия, Employee Onboarding Activity,Деятельность бортового персонала, Employee Other Income,Другой доход сотрудника, Employee Promotion,Продвижение сотрудников, @@ -6640,7 +6640,7 @@ Employee Separation,Разделение сотрудников, Employee Separation Template,Шаблон разделения сотрудников, Exit Interview Summary,Выйти из интервью, Employee Skill,Навыки сотрудников, -Proficiency,умение, +Proficiency,Умения, Evaluation Date,Дата оценки, Employee Skill Map,Карта навыков сотрудников, Employee Skills,Навыки сотрудников, @@ -6683,7 +6683,7 @@ Total Advance Amount,Общая сумма аванса, Total Claimed Amount,Всего заявленной суммы, Total Amount Reimbursed,Общая сумма возмещаются, Vehicle Log,Автомобиль Вход, -Employees Email Id,Идентификаторы Электронных почт сотрудников, +Employees Email Id,Идентификаторы почты сотрудников, More Details,Подробнее, Expense Claim Account,Счет Авансового Отчета, Expense Claim Advance,Претензия на увеличение расходов, @@ -6701,7 +6701,7 @@ Clear Table,Очистить таблицу, HR Settings,Настройки HR, Employee Settings,Работники Настройки, Retirement Age,Пенсионный возраст, -Enter retirement age in years,Введите возраст выхода на пенсию в ближайшие годы, +Enter retirement age in years,Введите возраст выхода на пенсию, Stop Birthday Reminders,Стоп День рождения Напоминания, Expense Approver Mandatory In Expense Claim,"Утверждение о расходах, обязательный для покрытия расходов", Payroll Settings,Настройки по заработной плате, @@ -6723,7 +6723,7 @@ Leave Status Notification Template,Оставить шаблон уведомл Role Allowed to Create Backdated Leave Application,"Роль, разрешенная для создания приложения с задним сроком выхода", Leave Approver Mandatory In Leave Application,Утвердить заявление на отпуск, Show Leaves Of All Department Members In Calendar,Показать листы всех членов Департамента в календаре, -Auto Leave Encashment,Авто оставить инкассо, +Auto Leave Encashment,Автоматический выход из инкассации, Hiring Settings,Настройки найма, Check Vacancies On Job Offer Creation,Проверьте вакансии на создание предложения о работе, Identification Document Type,Тип идентификационного документа, @@ -6757,7 +6757,7 @@ Staffing Plan,План кадрового обеспечения, Planned number of Positions,Планируемое количество позиций, "Job profile, qualifications required etc.","Профиль работы, необходимая квалификация и т.д.", HR-LAL-.YYYY.-,HR-LAL-.YYYY.-, -Allocation,распределение, +Allocation,Распределение, New Leaves Allocated,Новые листья Выделенные, Add unused leaves from previous allocations,Добавить неиспользованные отпуска с прошлых периодов, Unused leaves,Неиспользованные листья, @@ -6785,15 +6785,15 @@ Leave Block List Allow,Оставьте Черный список Разреши Allow User,Разрешить пользователю, Leave Block List Date,Оставьте Блок-лист Дата, Block Date,Блок Дата, -Leave Control Panel,Оставьте панели управления, -Select Employees,Выберите Сотрудников, +Leave Control Panel,Выйти из панели управления, +Select Employees,Выберите сотрудников, Employment Type (optional),Тип занятости (необязательно), Branch (optional),Филиал (необязательно), Department (optional),Отдел (необязательно), Designation (optional),Обозначение (необязательно), Employee Grade (optional),Оценка сотрудника (необязательно), Employee (optional),Сотрудник (необязательно), -Allocate Leaves,Выделить листья, +Allocate Leaves,Распределить записи, Carry Forward,Переносить, Please select Carry Forward if you also want to include previous fiscal year's balance leaves to this fiscal year,"Пожалуйста, выберите переносить, если вы также хотите включить баланс предыдущего финансового года оставляет в этом финансовом году", New Leaves Allocated (In Days),Новые листья Выделенные (в днях), @@ -6811,7 +6811,7 @@ Leave Allocations,Оставить выделение, Leave Policy Details,Оставьте сведения о политике, Leave Policy Detail,Оставить информацию о политике, Annual Allocation,Ежегодное распределение, -Leave Type Name,Оставьте Тип Название, +Leave Type Name,Оставить имя типа, Max Leaves Allowed,Максимальные листья разрешены, Applicable After (Working Days),Применимые после (рабочие дни), Maximum Continuous Days Applicable,Максимальные непрерывные дни, @@ -6828,9 +6828,9 @@ Encashment Threshold Days,Дни порога инкассации, Earned Leave,Заработано, Is Earned Leave,Заработано, Earned Leave Frequency,Заработок, -Rounding,округление, -Payroll Employee Detail,Сведения о сотрудниках по расчетам, -Payroll Frequency,Расчет заработной платы Частота, +Rounding,Округление, +Payroll Employee Detail,Сведения о сотруднике по заработной плате, +Payroll Frequency,Частота расчета заработной платы, Fortnightly,раз в две недели, Bimonthly,Раз в два месяца, Employees,Сотрудники, @@ -6842,8 +6842,8 @@ Select Payroll Period,Выберите Период начисления зар Deduct Tax For Unclaimed Employee Benefits,Вычет налога для невостребованных сотрудников, Deduct Tax For Unsubmitted Tax Exemption Proof,Доход от вычета налога за отказ в освобождении от налогов, Select Payment Account to make Bank Entry,Выберите Учетная запись Оплата сделать Банк Стажер, -Salary Slips Created,Созданы зарплатные слайды, -Salary Slips Submitted,Заявки на зарплату, +Salary Slips Created,Зарплатные ведомости созданы, +Salary Slips Submitted,Утвержденные зарплатные ведомости, Payroll Periods,Периоды начисления заработной платы, Payroll Period Date,Дата периода расчета заработной платы, Purpose of Travel,Цель поездки, @@ -6917,7 +6917,7 @@ Time after the end of shift during which check-out is considered for attendance. Working Hours Threshold for Half Day,Порог рабочего времени на полдня, Working hours below which Half Day is marked. (Zero to disable),"Рабочее время, ниже которого отмечается полдня. (Ноль отключить)", Working Hours Threshold for Absent,Порог рабочего времени для отсутствующих, -Working hours below which Absent is marked. (Zero to disable),"Рабочее время, ниже которого отмечается отсутствие. (Ноль отключить)", +Working hours below which Absent is marked. (Zero to disable),Порог рабочего времени, ниже которого устанавливается отметка «Отсутствует». (Ноль для отключения),", Process Attendance After,Посещаемость процесса после, Attendance will be marked automatically only after this date.,Посещаемость будет отмечена автоматически только после этой даты., Last Sync of Checkin,Последняя синхронизация регистрации, @@ -6960,7 +6960,7 @@ Attendees,Присутствующие, Employee Emails,Электронные почты сотрудников, Training Event Employee,Обучение сотрудников Событие, Invited,приглашенный, -Feedback Submitted,Обратная связь Представлено, +Feedback Submitted,Отзыв отправлен, Optional,Необязательный, Training Result Employee,Результат обучения сотрудника, Travel Itinerary,Маршрут путешествия, @@ -6972,7 +6972,7 @@ Train,Поезд, Taxi,Такси, Rented Car,Прокат автомобилей, Meal Preference,Предпочитаемая еда, -Vegetarian,вегетарианец, +Vegetarian,Вегетарианец, Non-Vegetarian,Не вегетарианский, Gluten Free,Не содержит глютен, Non Diary,Не дневник, @@ -6992,7 +6992,7 @@ Require Full Funding,Требовать полного финансирован Fully Sponsored,Полностью спонсируемый, "Partially Sponsored, Require Partial Funding","Частично спонсируется, требует частичного финансирования", Copy of Invitation/Announcement,Копия приглашения / объявление, -"Details of Sponsor (Name, Location)","Подробная информация о спонсоре (название, местоположение)", +"Details of Sponsor (Name, Location)","Подробная информация о спонсоре (Название, Местоположение)", Identification Document Number,Идентификационный номер документа, Any other details,Любые другие детали, Costing Details,Сведения о стоимости, @@ -7014,29 +7014,29 @@ Vehicle,Средство передвижения, License Plate,Номерной знак, Odometer Value (Last),Значение одометра (последнее), Acquisition Date,Дата приобретения, -Chassis No,Шасси Нет, +Chassis No,VIN код, Vehicle Value,Значение автомобиля, Insurance Details,Страхование Подробнее, Insurance Company,Страховая компания, -Policy No,Политика Нет, +Policy No,Полис №, Additional Details,дополнительные детали, Fuel Type,Тип топлива, Petrol,Бензин, -Diesel,дизель, +Diesel,Дизель, Natural Gas,Природный газ, -Electric,электрический, +Electric,Электрический, Fuel UOM,Топливо UOM, -Last Carbon Check,Последний Carbon Проверить, +Last Carbon Check,Последняя проверка на углерод, Wheels,Колеса, -Doors,двери, +Doors,Двери, HR-VLOG-.YYYY.-,HR-Видеоблог-.YYYY.-, Odometer Reading,Показания одометра, Current Odometer value ,Текущее значение одометра, -last Odometer Value ,Значение последнего одометра, +last Odometer Value ,последнее значение одометра, Refuelling Details,Заправочные Подробнее, Invoice Ref,Счет-фактура Ссылка, Service Details,Сведения о службе, -Service Detail,Деталь обслуживания, +Service Detail,Сведения об услуге, Vehicle Service,Обслуживание автомобиля, Service Item,Продукт-услуга, Brake Oil,Тормозные масла, @@ -7044,8 +7044,8 @@ Brake Pad,Комплект тормозных колодок, Clutch Plate,Диск сцепления, Engine Oil,Машинное масло, Oil Change,Замена масла, -Inspection,осмотр, -Mileage,пробег, +Inspection,Осмотр, +Mileage,Пробег, Hub Tracked Item,Отслеживаемый элемент концентратора, Hub Node,Узел хаба, Image List,Список изображений, @@ -7098,7 +7098,7 @@ Repayment Info,Погашение информация, Total Payable Interest,Общая задолженность по процентам, Against Loan ,Против ссуды, Loan Interest Accrual,Начисление процентов по кредитам, -Amounts,суммы, +Amounts,Суммы, Pending Principal Amount,Ожидающая основная сумма, Payable Principal Amount,Основная сумма к оплате, Paid Principal Amount,Выплаченная основная сумма, @@ -7164,7 +7164,7 @@ Random,Случайный, No of Visits,Кол-во посещений, MAT-MVS-.YYYY.-,MAT-MVS-.YYYY.-, Maintenance Date,Дата технического обслуживания, -Maintenance Time,Техническое обслуживание Время, +Maintenance Time,Время технического обслуживания, Completion Status,Статус завершения, Partially Completed,Частично завершено, Fully Completed,Полностью завершен, @@ -7172,10 +7172,10 @@ Unscheduled,Незапланированный, Breakdown,Разбивка, Purposes,Цели, Customer Feedback,Обратная связь с клиентами, -Maintenance Visit Purpose,Техническое обслуживание Посетить Цель, -Work Done,Сделано, +Maintenance Visit Purpose,Цель технического обслуживания, +Work Done,Работа выполнена, Against Document No,Против Документ №, -Against Document Detail No,Против деталях документа Нет, +Against Document Detail No,Против деталях документа №, MFG-BLR-.YYYY.-,MFG-BLR-.YYYY.-, Order Type,Тип заказа, Blanket Order Item,Элемент заказа одеяла, @@ -7193,8 +7193,8 @@ Transfer Material Against,Передача материала против, Routing,Маршрутизация, Materials,Материалы, Quality Inspection Required,Требуется проверка качества, -Quality Inspection Template,Качественный контрольный шаблон, -Scrap,лом, +Quality Inspection Template,Шаблон контроля качества, +Scrap,Брак, Scrap Items,Утилизированные продукты, Operating Cost,Эксплуатационные затраты, Raw Material Cost,Стоимость сырья, @@ -7243,7 +7243,7 @@ BOM Website Operation,BOM Операция Сайт, Operation Time,Время работы, PO-JOB.#####,ПО-РАБОТА. #####, Timing Detail,Сроки, -Time Logs,Журналы Время, +Time Logs,Журналы времени, Total Time in Mins,Общее время в минутах, Operation ID,ID операции, Transferred Qty,Передано кол-во, @@ -7302,13 +7302,13 @@ Make Work Order for Sub Assembly Items,Создание заказа на раб Planned Start Date,Планируемая дата начала, Quantity and Description,Количество и описание, material_request_item,material_request_item, -Product Bundle Item,Продукт Связка товара, +Product Bundle Item,Связка продуктов, Production Plan Material Request,Производство План Материал Запрос, Production Plan Sales Order,Производственный План по Сделкам, Sales Order Date,Дата Сделки, Routing Name,Название маршрутизации, MFG-WO-.YYYY.-,MFG-WO-.YYYY.-, -Item To Manufacture,Продукт в производство, +Item To Manufacture,Продукт для производство, Material Transferred for Manufacturing,Материал переведен на Производство, Manufactured Qty,Изготовлено Кол-во, Use Multi-Level BOM,Использование Multi-Level BOM, @@ -7381,7 +7381,7 @@ Chapter Head,Глава главы, Meetup Embed HTML,Вставить HTML-код, chapters/chapter_name\nleave blank automatically set after saving chapter.,главы / chapter_name оставить пустым автоматически после сохранения главы., Chapter Members,Члены группы, -Members,члены, +Members,Участники, Chapter Member,Участник, Website URL,URL веб-сайта, Leave Reason,Оставить разум, @@ -7391,16 +7391,16 @@ Withdrawn,Изъятое, Grant Application Details ,Сведения о предоставлении гранта, Grant Description,Описание гранта, Requested Amount,Запрошенная сумма, -Has any past Grant Record,Имеет ли какая-либо прошлая грантовая запись, +Has any past Grant Record,Имеет предидущую связь в диаграмме, Show on Website,Показать на сайте, -Assessment Mark (Out of 10),Оценка Оценка (из 10), +Assessment Mark (Out of 10),Оценка (из 10), Assessment Manager,Менеджер по оценке, -Email Notification Sent,Уведомление отправлено по электронной почте, +Email Notification Sent,Отправлено уведомление по электронной почте, NPO-MEM-.YYYY.-,НПО-MEM-.YYYY.-, Membership Expiry Date,Дата истечения срока членства, Razorpay Details,Детали Razorpay, Subscription ID,ID подписки, -Customer ID,Пользовательский ИД, +Customer ID,Пользовательский ID, Subscription Activated,Подписка активирована, Subscription Start ,Подписка Старт, Subscription End,Конец подписки, @@ -7420,8 +7420,8 @@ Volunteer Type,Тип волонтера, Availability and Skills,Доступность и навыки, Availability,Доступность, Weekends,Выходные дни, -Availability Timeslot,Доступность Timeslot, -Morning,утро, +Availability Timeslot,Временной интервал доступности, +Morning,Утро, Afternoon,После полудня, Evening,Вечер, Anytime,В любой момент, @@ -7430,7 +7430,7 @@ Volunteer Skill,Волонтерский навык, Homepage,Главная страница, Hero Section Based On,Раздел героя на основе, Homepage Section,Раздел домашней страницы, -Hero Section,Раздел героя, +Hero Section,Раздел героев, Tag Line,Tag Line, Company Tagline for website homepage,Компания Слоган на главную страницу сайта, Company Description for website homepage,Описание компании на главную страницу сайта, @@ -7448,8 +7448,8 @@ Use this field to render any custom HTML in the section.,Используйте Section Order,Раздел Заказ, "Order in which sections should appear. 0 is first, 1 is second and so on.","Порядок, в котором должны появляться разделы. 0 - первое, 1 - второе и т. Д.", Homepage Section Card,Домашняя страница Раздел Карта, -Subtitle,подзаголовок, -Products Settings,Настройки Продукты, +Subtitle,Подзаголовок, +Products Settings,Настройки продуктов, Home Page is Products,Главная — Продукты, "If checked, the Home page will be the default Item Group for the website","Если этот флажок установлен, главная страница будет по умолчанию Item Group для веб-сайте", Show Availability Status,Показать статус доступности, @@ -7464,31 +7464,31 @@ Website Attribute,Атрибут сайта, Attribute,Атрибут, Website Filter Field,Поле фильтра веб-сайта, Activity Cost,Стоимость деятельности, -Billing Rate,Платежная Оценить, -Costing Rate,Калькуляция Оценить, -title,заглавие, +Billing Rate,Платежная оценка, +Costing Rate,Стоимость, +title,название, Projects User,Исполнитель проектов, -Default Costing Rate,По умолчанию Калькуляция Оценить, -Default Billing Rate,По умолчанию Платежная Оценить, +Default Costing Rate,Ставка стоимости по умолчанию, +Default Billing Rate,Платежная ставка по умолчанию, Dependent Task,Зависимая задача, Project Type,Тип проекта, -% Complete Method,% Полный метод, -Task Completion,Завершение задачи, -Task Progress,Готовность задачи, +% Complete Method,% выполнения, +Task Completion,Завершеные задачи, +Task Progress,Ход выполнения задачи, % Completed,% Завершено, From Template,Из шаблона, Project will be accessible on the website to these users,Проект будет доступен на веб-сайте для этих пользователей, Copied From,Скопировано из, Start and End Dates,Даты начала и окончания, Actual Time (in Hours),Фактическое время (в часах), -Costing and Billing,Калькуляция и биллинг, -Total Costing Amount (via Timesheets),Общая сумма калькуляции (через расписания), -Total Expense Claim (via Expense Claims),Итоговая сумма аванса (через Авансовые Отчеты), -Total Purchase Cost (via Purchase Invoice),Общая стоимость покупки (через счет покупки), -Total Sales Amount (via Sales Order),Общая Сумма Продаж (по Сделке), -Total Billable Amount (via Timesheets),Общая сумма платежа (через расписания), +Costing and Billing,Стоимость и платежи, +Total Costing Amount (via Timesheets),Общая стоимость (по табелю учета рабочего времени), +Total Expense Claim (via Expense Claims),Итоговая сумма аванса (через возмещение расходов), +Total Purchase Cost (via Purchase Invoice),Общая стоимость покупки (по счетам закупок), +Total Sales Amount (via Sales Order),Общая сумма продажи (по сделке), +Total Billable Amount (via Timesheets),Общая сумма платежа (по табелю учета рабочего времени), Total Billed Amount (via Sales Invoices),Общая сумма выставленных счетов (через счета-фактуры), -Total Consumed Material Cost (via Stock Entry),Общая потребляемая материальная стоимость (через вход запаса), +Total Consumed Material Cost (via Stock Entry),Общая потребляемая материальная стоимость (через записи на складе), Gross Margin,Валовая прибыль, Gross Margin %,Валовая маржа %, Monitor Progress,Мониторинг готовности, @@ -7509,26 +7509,26 @@ Project Update,Обновление проекта, Project User,Пользователь проекта, View attachments,Просмотр вложений, Projects Settings,Настройки проектов, -Ignore Workstation Time Overlap,Игнорировать перекрытие рабочей станции, +Ignore Workstation Time Overlap,Игнорировать перекрытие времени рабочей станции, Ignore User Time Overlap,Игнорировать перекрытие пользовательского времени, -Ignore Employee Time Overlap,Игнорировать перекрытие сотрудников, +Ignore Employee Time Overlap,Игнорировать перекрытие времени сотрудников, Weight,Вес, Parent Task,Родительская задача, Timeline,График, Expected Time (in hours),Ожидаемое время (в часах), % Progress,% Прогресс, -Is Milestone,Это этап, +Is Milestone,Является этапом, Task Description,Описание задания, -Dependencies,зависимости, +Dependencies,Зависимости, Dependent Tasks,Зависимые задачи, Depends on Tasks,Зависит от задач, -Actual Start Date (via Time Sheet),Фактическая дата начала (с помощью Time Sheet), +Actual Start Date (via Time Sheet),Фактическая дата начала (по табелю учета рабочего времени), Actual Time (in hours),Фактическое время (в часах), -Actual End Date (via Time Sheet),Фактическая дата окончания (с помощью табеля рабочего времени), -Total Costing Amount (via Time Sheet),Общая калькуляция Сумма (с помощью Time Sheet), +Actual End Date (via Time Sheet),Фактическая дата окончания (по табелю учета рабочего времени), +Total Costing Amount (via Time Sheet),Общая калькуляция Сумма (по табелю учета рабочего времени), Total Expense Claim (via Expense Claim),Итоговая сумма аванса (через Авансовый Отчет), -Total Billing Amount (via Time Sheet),Общая сумма Billing (через табель), -Review Date,Дата пересмотра, +Total Billing Amount (via Time Sheet),Общая сумма Billing (по табелю учета рабочего времени), +Review Date,Дата проверки, Closing Date,Дата закрытия, Task Depends On,Задача зависит от, Task Type,Тип задачи, @@ -7536,16 +7536,16 @@ TS-.YYYY.-,ТС-.ГГГГ.-, Employee Detail,Сотрудник Деталь, Billing Details,Платежные реквизиты, Total Billable Hours,Всего человеко-часов, -Total Billed Hours,Всего Оплачиваемые Часы, +Total Billed Hours,Всего оплачиваемые часы, Total Costing Amount,Общая сумма Стоимостью, Total Billable Amount,Общая сумма Выплачиваемый, Total Billed Amount,Общая сумма Объявленный, % Amount Billed,% Сумма счета, -Hrs,часов, +Hrs,Часов, Costing Amount,Калькуляция Сумма, -Corrective/Preventive,Корректирующее / Профилактическая, -Corrective,корректив, -Preventive,превентивный, +Corrective/Preventive,Корректирующее / Превентивная, +Corrective,Корректив, +Preventive,Превентивный, Resolution,Разрешение, Resolutions,Решения, Quality Action Resolution,Решение по качеству действий, @@ -7553,24 +7553,24 @@ Quality Feedback Parameter,Параметр обратной связи по к Quality Feedback Template Parameter,Параметр шаблона обратной связи по качеству, Quality Goal,Цель качества, Monitoring Frequency,Частота мониторинга, -Weekday,будний день, +Weekday,Будний день, Objectives,Цели, Quality Goal Objective,Цель качества, Objective,Задача, Agenda,Повестка дня, -Minutes,минут, -Quality Meeting Agenda,Повестка дня встречи качества, -Quality Meeting Minutes,Протокол встречи качества, +Minutes,Минут, +Quality Meeting Agenda,Встреча для оценки работ, +Quality Meeting Minutes,Протокол встречи для оценки работ, Minute,Минута, Parent Procedure,Родительская процедура, Processes,Процессы, -Quality Procedure Process,Процесс качественного процесса, +Quality Procedure Process,Процедура контроля качества, Process Description,Описание процесса, Link existing Quality Procedure.,Ссылка существующей процедуры качества., Additional Information,Дополнительная информация, Quality Review Objective,Цель проверки качества, DATEV Settings,Настройки DATEV, -Regional,региональный, +Regional,Региональный, Consultant ID,Идентификатор консультанта, GST HSN Code,Код GST HSN, HSN Code,Код HSN, @@ -7581,17 +7581,17 @@ GST Accounts,Учетные записи GST, B2C Limit,Ограничение B2C, Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.,"Установите значение счета для B2C. B2CL и B2CS, рассчитанные на основе этой стоимости счета.", GSTR 3B Report,Отчет GSTR 3B, -January,январь, -February,февраль, -March,марш, -April,апрель, +January,Январь, +February,Февраль, +March,Март, +April,Апрель, May,Мая, -June,июнь, -July,июль, -August,августейший, -September,сентябрь, -October,октября, -November,ноябрь, +June,Июнь, +July,Июль, +August,Август, +September,Сентябрь, +October,Октября, +November,Ноябрь, December,Декабрь, JSON Output,Выход JSON, Invoices with no Place Of Supply,Счета без места поставки, @@ -7612,9 +7612,9 @@ Certificate Details,Детали сертификата, 194LA,194LA, 194LBB,194LBB, 194LBC,194LBC, -Certificate No,Сертификат номер, +Certificate No,Сертификат №, Deductee Details,Детали франшизы, -PAN No,PAN Нет, +PAN No,PAN №, Validity Details,Сведения о сроке действия, Rate Of TDS As Per Certificate,Ставка TDS согласно сертификату, Certificate Limit,Лимит сертификата, @@ -7639,13 +7639,13 @@ Reservation Time,Время резервирования, Reservation End Time,Время окончания бронирования, No of Seats,Количество мест, Minimum Seating,Минимальное размещение, -"Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ","Следите за Кампаниями по продажам. Отслеживайте Обращения, Предложения, Сделки и т.п. от Кампаний и оцените отдачу от инвестиций.", +"Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ","Следите за Кампаниями по продажам. Отслеживайте Лиды, Предложения, Сделки и т.п. от Кампаний и оцените отдачу от инвестиций.", SAL-CAM-.YYYY.-,SAL-CAM-.YYYY.-, Campaign Schedules,Расписание кампаний, Buyer of Goods and Services.,Покупатель товаров и услуг., CUST-.YYYY.-,КЛИЕНТ-.YYYY.-, Default Company Bank Account,Стандартный банковский счет компании, -From Lead,Из Обращения, +From Lead,Из Лида, Account Manager,Менеджер по работе с клиентами, Allow Sales Invoice Creation Without Sales Order,Разрешить создание счета-фактуры без заказа на продажу, Allow Sales Invoice Creation Without Delivery Note,Разрешить создание счета-фактуры без накладной, @@ -7672,11 +7672,11 @@ Installation Date,Дата установки, Installation Time,Время установки, Installation Note Item,Установка Примечание Пункт, Installed Qty,Установленное количество, -Lead Source,Источник Обращения, +Lead Source,Источник лида, Period Start Date,Дата начала периода, Period End Date,Дата окончания периода, Cashier,Касса, -Difference,разница, +Difference,Разница, Modes of Payment,Способы оплаты, Linked Invoices,Связанные счета-фактуры, POS Closing Voucher Details,Информация о закрытии ваучера POS, @@ -7726,7 +7726,7 @@ Sales Partner Type,Тип торгового партнера, Contact No.,Контактный номер, Contribution (%),Вклад (%), Contribution to Net Total,Вклад в Сумму, -Selling Settings,Продажа Настройки, +Selling Settings,Настройки продаж, Settings for Selling Module,Настройки по продаже модуля, Customer Naming By,Именование клиентов По, Campaign Naming By,Кампания Именование По, @@ -7742,7 +7742,7 @@ All Contact,Всем Контактам, All Customer Contact,Контакты всех клиентов, All Supplier Contact,Всем Контактам Поставщиков, All Sales Partner Contact,Всем Контактам Торговых Партнеров, -All Lead (Open),Всем Обращениям (Созданным), +All Lead (Open),Всем лидам (Созданным), All Employee (Active),Всем Сотрудникам (Активным), All Sales Person,Всем Продавцам, Create Receiver List,Создать список получателей, @@ -7859,7 +7859,7 @@ Open Projects,Открытые проекты, Purchase Orders Items Overdue,Элементы заказа на поставку просрочены, Upcoming Calendar Events,Предстоящие события календаря, Open To Do,Открыто делать, -Add Quote,Добавить Цитата, +Add Quote,Добавить предложение, Global Defaults,Глобальные вводные по умолчанию, Default Company,Компания по умолчанию, Current Fiscal Year,Текущий финансовый год, @@ -7908,8 +7908,8 @@ Partner website,Сайт партнера, All Sales Transactions can be tagged against multiple **Sales Persons** so that you can set and monitor targets.,Все сделок купли-продажи могут быть помечены против нескольких ** продавцы ** так что вы можете устанавливать и контролировать цели., Name and Employee ID,Имя и ID сотрудника, Sales Person Name,Имя продавца, -Parent Sales Person,Головная группа продаж, -Select company name first.,Выберите название компании в первую очередь., +Parent Sales Person,Выше стоящий продавец, +Select company name first.,В первую очередь выберите название компании., Sales Person Targets,Цели продавца, Set targets Item Group-wise for this Sales Person.,Задайте цели Продуктовых Групп для Продавца, Supplier Group Name,Название группы поставщиков, @@ -7922,9 +7922,9 @@ Target Distribution,Распределение цели, Applicable Modules,Применимые модули, Terms and Conditions Help,Правила и условия Помощь, Classification of Customers by region,Классификация клиентов по регионам, -Territory Name,Территория Имя, -Parent Territory,Родитель Территория, -Territory Manager,Territory Manager, +Territory Name,Название региона, +Parent Territory,Родительский регион, +Territory Manager,Региональный менеджер, For reference,Для справки, Territory Targets,Территория Цели, Set Item Group-wise budgets on this Territory. You can also include seasonality by setting the Distribution.,"Установите группу товаров стрелке бюджеты на этой территории. Вы можете также включить сезонность, установив распределение.", @@ -7937,7 +7937,7 @@ Enable Shopping Cart,Включить Корзина, Display Settings,Настройки отображения, Show Public Attachments,Показать общедоступные приложения, Show Price,Показать цену, -Show Stock Availability,Доступность, +Show Stock Availability,Показать наличие на складе, Show Contact Us Button,Кнопка «Связаться с нами», Show Stock Quantity,Показывать количество запаса, Show Apply Coupon Code,Показать Применить код купона, @@ -7946,7 +7946,7 @@ Prices will not be shown if Price List is not set,"Цены не будут по Quotation Series,Идентификаторы Предложений, Checkout Settings,Checkout Настройки, Enable Checkout,Включить Checkout, -Payment Success Url,Успех Оплата URL, +Payment Success Url,URL успешного платежа, After payment completion redirect user to selected page.,После завершения оплаты перенаправить пользователя на выбранную страницу., Batch Details,Детали партии, Batch ID,ID партии, @@ -7988,7 +7988,7 @@ Print Without Amount,Распечатать без суммы, Installation Status,Состояние установки, Excise Page Number,Количество Акцизный Страница, Instructions,Инструкции, -From Warehouse,От Склад, +From Warehouse,Со склада, Against Sales Order,По Сделке, Against Sales Order Item,По Продукту Сделки, Against Sales Invoice,Повторная накладная, @@ -8003,7 +8003,7 @@ Leave blank to use the standard Delivery Note format,Оставьте пусты Send with Attachment,Отправить с прикрепленным файлом, Delay between Delivery Stops,Задержка между поставками, Delivery Stop,Остановить доставку, -Lock,Замок, +Lock,Заблокировано, Visited,Посещен, Order Information,запросить информацию, Contact Information,Контакты, @@ -8014,7 +8014,7 @@ MAT-DT-.YYYY.-,MAT-DT-.YYYY.-, Initial Email Notification Sent,Исходящее уведомление по электронной почте отправлено, Delivery Details,Подробности доставки, Driver Email,Электронная почта водителя, -Driver Address,Адрес драйвера, +Driver Address,Адрес водителя, Total Estimated Distance,Общее расчетное расстояние, Distance UOM,Расстояние UOM, Departure Time,Время отправления, @@ -8049,7 +8049,7 @@ Reorder level based on Warehouse,Уровень переупорядочиван Will also apply for variants unless overrridden,"Будет также применяться для модификаций, если не отменено", Units of Measure,Единицы измерения, Will also apply for variants,Также применять к модификациям, -Serial Nos and Batches,Серийные номера и партии, +Serial Nos and Batches,Серийные номера и партии, Has Batch No,Имеет номер партии, Automatically Create New Batch,Автоматически создавать новую группу, Batch Number Series,Партия Идентификаторов по номеру, @@ -8184,7 +8184,7 @@ Transferred,Переданы, % Ordered,% заказано, Terms and Conditions Content,Условия Содержимое, Quantity and Warehouse,Количество и Склад, -Lead Time Date,Время и Дата Обращения, +Lead Time Date,Время и Дата Лида, Min Order Qty,Минимальный заказ Кол-во, Packed Item,Упаковано, To Warehouse (Optional),На склад (Необязательно), @@ -8216,7 +8216,7 @@ Item Locations,Расположение предметов, Pick List Item,Элемент списка выбора, Picked Qty,Выбрал кол-во, Price List Master,Прайс-лист Мастер, -Price List Name,Цена Имя, +Price List Name,Название прайс-листа, Price Not UOM Dependent,Цена не зависит от UOM, Applicable for Countries,Применимо для стран, Price List Country,Цены Страна, @@ -8278,7 +8278,7 @@ Invoice Details,Сведения о счете, Warranty / AMC Details,Гарантия / подробная информация, Warranty Expiry Date,Срок действия гарантии, AMC Expiry Date,Срок действия AMC, -Under Warranty,Под гарантии, +Under Warranty,На гарантии, Out of Warranty,По истечении гарантийного срока, Under AMC,Под КУА, Out of AMC,Из КУА, @@ -8290,9 +8290,9 @@ Stock Entry (Outward GIT),Вход в акции (внешний GIT), Material Consumption for Manufacture,Потребление материала для производства, Repack,Перепаковать, Send to Subcontractor,Отправить субподрядчику, -Delivery Note No,Документ Отгрузки №, -Sales Invoice No,№ Счета на продажу, -Purchase Receipt No,Покупка Получение Нет, +Delivery Note No,Накладная о доставке №, +Sales Invoice No,Номер счета на продажу, +Purchase Receipt No,Квитанция о покупке №, Inspection Required,Инспекция Обязательные, From BOM,Из спецификации, For Quantity,Для Количество, @@ -8440,7 +8440,7 @@ File to Rename,Файл Переименовать, Rename Log,Переименовать Входить, SMS Log,СМС-журнал, Sender Name,Имя отправителя, -Sent On,Направлено на, +Sent On,Отправлено на, No of Requested SMS,Кол-во запрошенных СМС, Requested Numbers,Запрошенные номера, No of Sent SMS,Кол-во отправленных СМС, @@ -8517,8 +8517,8 @@ Item-wise Sales Register,Пункт мудрый Продажи Зарегист Items To Be Requested,Запрашиваемые продукты, Reserved,Зарезервировано, Itemwise Recommended Reorder Level,Рекомендация пополнения уровня продукта, -Lead Details,Подробнее об Обращении, -Lead Owner Efficiency,Эффективность Ответственного за Обращения, +Lead Details,Подробнее об лиде, +Lead Owner Efficiency,Эффективность ответственного за лид, Loan Repayment and Closure,Погашение и закрытие кредита, Loan Security Status,Состояние безопасности ссуды, Lost Opportunity,Потерянная возможность, @@ -8679,7 +8679,7 @@ Book Deferred Entries Based On,Забронируйте отложенные з Days,Дней, Months,Месяцы, Book Deferred Entries Via Journal Entry,Книга отложенных записей через запись в журнале, -Submit Journal Entries,Отправить записи журнала, +Submit Journal Entries,Утвердить журнальные записи, If this is unchecked Journal Entries will be saved in a Draft state and will have to be submitted manually,"Если этот флажок не установлен, записи журнала будут сохранены в состоянии черновик, и их придется отправлять вручную.", Enable Distributed Cost Center,Включить центр распределенных затрат, Distributed Cost Center,Центр распределенных затрат, @@ -8797,7 +8797,7 @@ Billing Address GSTIN,Платежный адрес GSTIN, Customer GSTIN,Клиент GSTIN, GST Transporter ID,GST Transporter ID, Distance (in km),Расстояние (в км), -Road,дорога, +Road,Дорога, Air,Воздух, Rail,Железнодорожный, Ship,Корабль, @@ -9070,7 +9070,7 @@ Calculate Payroll Working Days Based On,Расчет рабочих дней д Consider Unmarked Attendance As,Считайте неотмеченную посещаемость как, Fraction of Daily Salary for Half Day,Доля дневной заработной платы за полдня, Component Type,Тип компонента, -Provident Fund,резервный фонд, +Provident Fund,Резервный фонд, Additional Provident Fund,Дополнительный резервный фонд, Provident Fund Loan,Ссуда из фонда обеспечения персонала, Professional Tax,Профессиональный налог, @@ -9146,8 +9146,8 @@ Average Score,Средний балл, Select Assessment Template,Выберите шаблон экзамена, out of ,снаружи, Select Assessment Parameter,Выберите параметр оценки, -Gender: ,Пол:, -Contact: ,Контакты:, +Gender: ,Пол: , +Contact: ,Контакты: , Total Therapy Sessions: ,Всего сеансов терапии:, Monthly Therapy Sessions: ,Ежемесячные сеансы терапии:, Patient Profile,Профиль пациента, @@ -9177,7 +9177,7 @@ You must add atleast one item to save it as draft.,"Вы должны добав There was an error saving the document.,При сохранении документа произошла ошибка., You must select a customer before adding an item.,Перед добавлением товара необходимо выбрать клиента., Please Select a Company,"Пожалуйста, выберите компанию", -Active Leads,Активные лиды, +Active Leads,Активные обращения, Please Select a Company.,"Пожалуйста, выберите компанию.", BOM Operations Time,Время операций по спецификации, BOM ID,Идентификатор спецификации, @@ -9210,8 +9210,8 @@ Id,Я бы, Time Required (In Mins),Требуемое время (в минутах), From Posting Date,С даты публикации, To Posting Date,До даты публикации, -No records found,записей не найдено, -Customer/Lead Name,Имя клиента / лида, +No records found,Записей не найдено, +Customer/Lead Name,Имя клиента / обратившегося, Unmarked Days,Дни без отметок, Jan,Янв, Feb,Фев, @@ -9219,9 +9219,9 @@ Mar,Мар, Apr,Апр, Aug,Авг, Sep,Сен, -Oct,Октябрь, +Oct,Окт, Nov,Ноя, -Dec,Декабрь, +Dec,Дек, Summarized View,Обобщенный вид, Production Planning Report,Отчет о производственном планировании, Order Qty,Кол-во заказа, @@ -9416,7 +9416,7 @@ Import Italian Supplier Invoice.,Импортировать счет-факту "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.","Курс оценки для Предмета {0}, необходим для ведения бухгалтерских записей для {1} {2}.", Here are the options to proceed:,"Вот варианты, чтобы продолжить:", "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.","Если в этой записи предмет используется как предмет с нулевой оценкой, включите параметр «Разрешить нулевую ставку оценки» в таблице предметов {0}.", -"If not, you can Cancel / Submit this entry ","Если нет, вы можете отменить / отправить эту запись", +"If not, you can Cancel / Submit this entry ","Если нет, вы можете отменить / утвердить эту запись", performing either one below:,выполнение одного из следующих:, Create an incoming stock transaction for the Item.,Создайте проводку входящего запаса для Товара., Mention Valuation Rate in the Item master.,Упомяните коэффициент оценки в мастере предметов., @@ -9475,7 +9475,7 @@ Row {0}: Loan Security {1} added multiple times,Строка {0}: Обеспеч Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save,Строка № {0}: дочерний элемент не должен быть набором продукта. Удалите элемент {1} и сохраните, Credit limit reached for customer {0},Достигнут кредитный лимит для клиента {0}, Could not auto create Customer due to the following missing mandatory field(s):,Не удалось автоматически создать клиента из-за отсутствия следующих обязательных полей:, -Please create Customer from Lead {0}.,Создайте клиента из лида {0}., +Please create Customer from Lead {0}.,Создайте клиента из обращения {0}., Mandatory Missing,Обязательно отсутствует, Please set Payroll based on in Payroll settings,"Пожалуйста, установите Расчет заработной платы на основе в настройках Заработной платы", Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3},Дополнительная зарплата: {0} уже существует для компонента зарплаты: {1} за период {2} и {3}, @@ -9623,7 +9623,7 @@ Inpatient Medication Order Entry,Ввод заказа на лечение в с Is Order Completed,Заказ выполнен, Employee Records to Be Created By,"Записи о сотрудниках, которые будут создавать", Employee records are created using the selected field,Записи о сотрудниках создаются с использованием выбранного поля, -Don't send employee birthday reminders,Не отправляйте сотрудникам напоминания о днях рождения, +Don't send employee birthday reminders,Не отправлять сотрудникам напоминания о днях рождения, Restrict Backdated Leave Applications,Ограничение подачи заявлений на отпуск задним числом, Sequence ID,Идентификатор последовательности, Sequence Id,Идентификатор последовательности, @@ -9677,7 +9677,7 @@ Please check your configuration and try again,"Пожалуйста, прове Mpesa Account Balance Processing Error,Ошибка обработки остатка на счете Mpesa, Balance Details,Детали баланса, Current Balance,Текущий баланс, -Available Balance,доступные средства, +Available Balance,Доступные средства, Reserved Balance,Зарезервированный баланс, Uncleared Balance,Неочищенный баланс, Payment related to {0} is not completed,"Платеж, связанный с {0}, не завершен", @@ -9744,7 +9744,7 @@ Print Receipt,Распечатать квитанцию, Edit Receipt,Редактировать квитанцию, Focus on search input,Сосредоточьтесь на вводе поиска, Focus on Item Group filter,Фильтр по группам товаров, -Checkout Order / Submit Order / New Order,Заказ оформления заказа / Отправить заказ / Новый заказ, +Checkout Order / Submit Order / New Order,Оформление заказа / Утвердить заказ / Новый заказ, Add Order Discount,Добавить скидку на заказ, Item Code: {0} is not available under warehouse {1}.,Код товара: {0} недоступен на складе {1}., Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.,Отсутствуют серийные номера для позиции {0} на складе {1}. Попробуйте сменить склад., @@ -9813,7 +9813,7 @@ Row #{}: {},Строка #{}: {}, Invalid POS Invoices,Недействительные счета-фактуры POS, Please add the account to root level Company - {},"Пожалуйста, добавьте аккаунт в компанию корневого уровня - {}", "While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA","При создании аккаунта для дочерней компании {0} родительский аккаунт {1} не найден. Пожалуйста, создайте родительский аккаунт в соответствующем сертификате подлинности", -Account Not Found,Аккаунт не найден, +Account Not Found,Счет не найден, "While creating account for Child Company {0}, parent account {1} found as a ledger account.",При создании аккаунта для дочерней компании {0} родительский аккаунт {1} обнаружен как счет главной книги., Please convert the parent account in corresponding child company to a group account.,Преобразуйте родительскую учетную запись в соответствующей дочерней компании в групповую., Invalid Parent Account,Неверный родительский аккаунт, From 712443a91c56d45f6ef8e7ef5bbf048ca6078295 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 22 Apr 2022 11:48:07 +0530 Subject: [PATCH 899/951] chore: Resolve conflicts --- erpnext/regional/india/e_invoice/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 16f63c621b1..fae88b63003 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -12,11 +12,8 @@ import traceback import frappe import jwt -<<<<<<< HEAD import six -======= import requests ->>>>>>> ee8047aba3 (fix(india): 401 & 403 client error while generating IRN) from frappe import _, bold from frappe.core.page.background_jobs.background_jobs import get_info from frappe.integrations.utils import make_get_request, make_post_request From 6dddbb9f27354e57d5acc8cf692e6c2493c8ad7c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 22 Apr 2022 14:15:21 +0530 Subject: [PATCH 900/951] fix: linter --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index fae88b63003..ed5ad2f28ff 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -12,8 +12,8 @@ import traceback import frappe import jwt -import six import requests +import six from frappe import _, bold from frappe.core.page.background_jobs.background_jobs import get_info from frappe.integrations.utils import make_get_request, make_post_request From ea0fe5e10c5136f9419629cb7b82b265ac177517 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Fri, 22 Apr 2022 07:49:29 -0400 Subject: [PATCH 901/951] fix(Selling,E-Commerce): Shopping cart quotation without website item. (#29085) * test: assert error if quotation contains non website item * fix(test): validate exception without website item * fix: shopping cart quotation without website item * fix: sider issues * fix: linter trilaing whitespace * fix: linter format string after translation * fix: Skip unpublished Variants with published templates in shopping cart quote validation * style: Re-run pre-commit Co-authored-by: Marica --- .../selling/doctype/quotation/quotation.py | 21 +++++++++++++++++++ .../doctype/quotation/test_quotation.py | 9 ++++++++ 2 files changed, 30 insertions(+) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7cd7456ee01..5759b504cee 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -26,6 +26,7 @@ class Quotation(SellingController): self.set_status() self.validate_uom_is_integer("stock_uom", "qty") self.validate_valid_till() + self.validate_shopping_cart_items() self.set_customer_name() if self.items: self.with_items = 1 @@ -38,6 +39,26 @@ class Quotation(SellingController): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): frappe.throw(_("Valid till date cannot be before transaction date")) + def validate_shopping_cart_items(self): + if self.order_type != "Shopping Cart": + return + + for item in self.items: + has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code}) + + # If variant is unpublished but template is published: valid + template = frappe.get_cached_value("Item", item.item_code, "variant_of") + if template and not has_web_item: + has_web_item = frappe.db.exists("Website Item", {"item_code": template}) + + if not has_web_item: + frappe.throw( + _("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Unpublished Item"), + ) + def has_sales_order(self): return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1}) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b44fa5e5516..6f0b381fc16 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -130,6 +130,15 @@ class TestQuotation(FrappeTestCase): quotation.submit() self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) + def test_shopping_cart_without_website_item(self): + if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}): + frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete() + + quotation = frappe.copy_doc(test_records[0]) + quotation.order_type = "Shopping Cart" + quotation.valid_till = getdate() + self.assertRaises(frappe.ValidationError, quotation.validate) + def test_create_quotation_with_margin(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.selling.doctype.sales_order.sales_order import ( From 8335ca63313df7c3d5e02735b2277eb159f55d48 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Apr 2022 17:39:15 +0530 Subject: [PATCH 902/951] fix: translation (#30781) (#30783) fixed the short word for March in german language (cherry picked from commit 43c1d63ab2818cb0d3d00d459c5559759c12c629) Co-authored-by: Wolfram Schmidt --- erpnext/translations/de.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 4f30eb250c2..7e6187e8375 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -9215,7 +9215,7 @@ Customer/Lead Name,Name des Kunden / Lead, Unmarked Days,Nicht markierte Tage, Jan,Jan., Feb,Feb., -Mar,Beschädigen, +Mar,Mrz., Apr,Apr., Aug,Aug., Sep,Sep., From e69c71576ddcf9d363ba37bafb0c598ba05a281f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 20 Apr 2022 19:07:53 +0530 Subject: [PATCH 903/951] fix: Loan doctypes in bank reconciliation (cherry picked from commit c3e27b5556bc2ffe1c4bd6ab0b3731b8017af04c) --- .../doctype/bank_clearance/bank_clearance.py | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 96779d75be3..ed5a6994630 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -5,7 +5,10 @@ import frappe from frappe import _, msgprint from frappe.model.document import Document -from frappe.utils import flt, fmt_money, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.utils import flt, fmt_money, getdate + +import erpnext form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"} @@ -76,6 +79,52 @@ class BankClearance(Document): as_dict=1, ) + loan_disbursement = frappe.qb.DocType("Loan Disbursement") + + loan_disbursements = ( + frappe.qb.from_(loan_disbursement) + .select( + ConstantColumn("Loan Disbursement").as_("payment_document"), + loan_disbursement.name.as_("payment_entry"), + loan_disbursement.disbursed_amount.as_("credit"), + ConstantColumn(0).as_("debit"), + loan_disbursement.reference_number.as_("cheque_number"), + loan_disbursement.reference_date.as_("cheque_date"), + loan_disbursement.disbursement_date.as_("posting_date"), + loan_disbursement.applicant.as_("against_account"), + ) + .where(loan_disbursement.docstatus == 1) + .where(loan_disbursement.disbursement_date >= self.from_date) + .where(loan_disbursement.disbursement_date <= self.to_date) + .where(loan_disbursement.clearance_date.isnull()) + .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) + .orderby(loan_disbursement.disbursement_date) + .orderby(loan_disbursement.name, frappe.qb.desc) + ).run(as_dict=1) + + loan_repayment = frappe.qb.DocType("Loan Repayment") + + loan_repayments = ( + frappe.qb.from_(loan_repayment) + .select( + ConstantColumn("Loan Repayment").as_("doctype"), + loan_repayment.name.as_("payment_entry"), + loan_repayment.amount_paid.as_("debit"), + ConstantColumn(0).as_("credit"), + loan_repayment.reference_number.as_("cheque_number"), + loan_repayment.reference_date.as_("cheque_date"), + loan_repayment.applicant.as_("against_account"), + loan_repayment.posting_date, + ) + .where(loan_repayment.docstatus == 1) + .where(loan_repayment.clearance_date.isnull()) + .where(loan_repayment.posting_date >= self.from_date) + .where(loan_repayment.posting_date <= self.to_date) + .where(loan_repayment.payment_account.isin([self.bank_account, self.account])) + .orderby(loan_repayment.posting_date) + .orderby(loan_repayment.name, frappe.qb.desc) + ).run(as_dict=1) + pos_sales_invoices, pos_purchase_invoices = [], [] if self.include_pos_transactions: pos_sales_invoices = frappe.db.sql( @@ -114,18 +163,26 @@ class BankClearance(Document): entries = sorted( list(payment_entries) - + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), - key=lambda k: k["posting_date"] or getdate(nowdate()), + + list(journal_entries) + + list(pos_sales_invoices) + + list(pos_purchase_invoices) + + list(loan_disbursements) + + list(loan_repayments), + key=lambda k: getdate(k["posting_date"]), ) self.set("payment_entries", []) self.total_amount = 0.0 + default_currency = erpnext.get_default_currency() for d in entries: row = self.append("payment_entries", {}) amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0)) + if not d.get("account_currency"): + d.account_currency = default_currency + formatted_amount = fmt_money(abs(amount), 2, d.account_currency) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) From 44f0b691521c6e7cfe84beb0d52d4fb03268e9a7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 20 Apr 2022 19:49:53 +0530 Subject: [PATCH 904/951] fix: select doctype as payment_document (cherry picked from commit 3d0e68acaa82aa0e1a6ab50e835f192297bd7bd2) --- erpnext/accounts/doctype/bank_clearance/bank_clearance.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index ed5a6994630..0f617b5dda7 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -107,7 +107,7 @@ class BankClearance(Document): loan_repayments = ( frappe.qb.from_(loan_repayment) .select( - ConstantColumn("Loan Repayment").as_("doctype"), + ConstantColumn("Loan Repayment").as_("payment_document"), loan_repayment.name.as_("payment_entry"), loan_repayment.amount_paid.as_("debit"), ConstantColumn(0).as_("credit"), @@ -185,6 +185,7 @@ class BankClearance(Document): formatted_amount = fmt_money(abs(amount), 2, d.account_currency) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) + d.posting_date = getdate(d.posting_date) d.pop("credit") d.pop("debit") From 728ac1f54e46e0ec3a6be48da3e4207105dcd4ba Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 21 Apr 2022 18:53:18 +0530 Subject: [PATCH 905/951] test: Add test coverage for bank clearance (cherry picked from commit 8a8476bb5c9e487a80d0631b685a4a596caa297e) --- .../bank_clearance/test_bank_clearance.py | 121 +++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index 706fbbe245c..fbc44ee312e 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -1,9 +1,126 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_months, getdate + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.loan_management.doctype.loan.test_loan import ( + create_loan, + create_loan_accounts, + create_loan_type, + create_repayment_entry, + make_loan_disbursement_entry, +) + class TestBankClearance(unittest.TestCase): - pass + @classmethod + def setUpClass(cls): + make_bank_account() + create_loan_accounts() + create_loan_masters() + add_transactions() + + @classmethod + def tearDownClass(cls): + payment_entry = frappe.get_doc( + "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} + ) + payment_entry.cancel() + payment_entry.delete() + + loan = frappe.get_doc("Loan", {"applicant": "_Test Customer"}) + + ld_doc = frappe.get_doc("Loan Disbursement", {"against_loan": loan.name}) + ld_doc.cancel() + ld_doc.delete() + + lr_doc = frappe.get_doc("Loan Repayment", {"against_loan": loan.name}) + lr_doc.cancel() + lr_doc.delete() + + lia = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) + lia.delete() + + plia = frappe.get_doc("Process Loan Interest Accrual", {"loan": loan.name}) + plia.cancel() + plia.delete() + + loan.load_from_db() + loan.cancel() + loan.flags.ignore_links = True + loan.delete() + + # Basic test case to test if bank clearance tool doesn't break + # Detailed test can be added later + def test_bank_clearance(self): + bank_clearance = frappe.get_doc("Bank Clearance") + bank_clearance.account = "_Test Bank Clearance - _TC" + bank_clearance.from_date = add_months(getdate(), -1) + bank_clearance.to_date = getdate() + bank_clearance.get_payment_entries() + self.assertEqual(len(bank_clearance.payment_entries), 3) + + +def make_bank_account(): + if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"): + frappe.get_doc( + { + "doctype": "Account", + "account_type": "Bank", + "account_name": "_Test Bank Clearance", + "company": "_Test Company", + "parent_account": "Bank Accounts - _TC", + } + ).insert() + + +def create_loan_masters(): + create_loan_type( + "Clearance Loan", + 2000000, + 13.5, + 25, + 0, + 5, + "Cash", + "_Test Bank Clearance - _TC", + "_Test Bank Clearance - _TC", + "Loan Account - _TC", + "Interest Income Account - _TC", + "Penalty Income Account - _TC", + ) + + +def add_transactions(): + make_payment_entry() + make_loan() + + +def make_loan(): + loan = create_loan( + "_Test Customer", + "Clearance Loan", + 280000, + "Repay Over Number of Periods", + 20, + applicant_type="Customer", + ) + loan.submit() + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate()) + repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount) + repayment_entry.save() + repayment_entry.submit() + + +def make_payment_entry(): + pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690) + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC") + pe.reference_no = "Conrad Oct 18" + pe.reference_date = "2018-10-24" + pe.insert() + pe.submit() From 1a74c6ee56252cba62df70d4cd1feec1ff94b314 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 23 Apr 2022 12:08:30 +0530 Subject: [PATCH 906/951] test: Fixes in test case (cherry picked from commit 0eacc99ab705af446a0464f09ac92af760f5d9cf) --- .../bank_clearance/test_bank_clearance.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index fbc44ee312e..c09aa1c0139 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -30,25 +30,33 @@ class TestBankClearance(unittest.TestCase): payment_entry = frappe.get_doc( "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} ) - payment_entry.cancel() - payment_entry.delete() + + if payment_entry.docstatus == 1: + payment_entry.cancel() + payment_entry.delete() loan = frappe.get_doc("Loan", {"applicant": "_Test Customer"}) ld_doc = frappe.get_doc("Loan Disbursement", {"against_loan": loan.name}) - ld_doc.cancel() - ld_doc.delete() + + if ld_doc.docstatus == 1: + ld_doc.cancel() + ld_doc.delete() lr_doc = frappe.get_doc("Loan Repayment", {"against_loan": loan.name}) - lr_doc.cancel() - lr_doc.delete() + + if lr_doc.docstatus == 1: + lr_doc.cancel() + lr_doc.delete() lia = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) lia.delete() plia = frappe.get_doc("Process Loan Interest Accrual", {"loan": loan.name}) - plia.cancel() - plia.delete() + + if plia.docstatus == 1: + plia.cancel() + plia.delete() loan.load_from_db() loan.cancel() From 57485b30b83698de7b0c1b4a082cf1c34d62f347 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 23 Apr 2022 12:33:35 +0530 Subject: [PATCH 907/951] test: Fixes in test case (cherry picked from commit d4d83f4bb6f34353347f4b613bfe7c9554343425) --- .../accounts/doctype/bank_clearance/test_bank_clearance.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index c09aa1c0139..18645c7517c 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -27,6 +27,8 @@ class TestBankClearance(unittest.TestCase): @classmethod def tearDownClass(cls): + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1) + payment_entry = frappe.get_doc( "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} ) @@ -63,6 +65,8 @@ class TestBankClearance(unittest.TestCase): loan.flags.ignore_links = True loan.delete() + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0) + # Basic test case to test if bank clearance tool doesn't break # Detailed test can be added later def test_bank_clearance(self): From 12f5d652716c09eba906262d067b6a8b278f33c5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 24 Apr 2022 19:20:29 +0530 Subject: [PATCH 908/951] test: Remove teardown method (cherry picked from commit c515abc392a8ae6b230ff39e32aae6a8ca4a6dfe) --- .../bank_clearance/test_bank_clearance.py | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index 18645c7517c..c1e55f6f723 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -25,48 +25,6 @@ class TestBankClearance(unittest.TestCase): create_loan_masters() add_transactions() - @classmethod - def tearDownClass(cls): - frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1) - - payment_entry = frappe.get_doc( - "Payment Entry", {"party_name": "_Test Supplier", "paid_from": "_Test Bank Clearance - _TC"} - ) - - if payment_entry.docstatus == 1: - payment_entry.cancel() - payment_entry.delete() - - loan = frappe.get_doc("Loan", {"applicant": "_Test Customer"}) - - ld_doc = frappe.get_doc("Loan Disbursement", {"against_loan": loan.name}) - - if ld_doc.docstatus == 1: - ld_doc.cancel() - ld_doc.delete() - - lr_doc = frappe.get_doc("Loan Repayment", {"against_loan": loan.name}) - - if lr_doc.docstatus == 1: - lr_doc.cancel() - lr_doc.delete() - - lia = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name}) - lia.delete() - - plia = frappe.get_doc("Process Loan Interest Accrual", {"loan": loan.name}) - - if plia.docstatus == 1: - plia.cancel() - plia.delete() - - loan.load_from_db() - loan.cancel() - loan.flags.ignore_links = True - loan.delete() - - frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0) - # Basic test case to test if bank clearance tool doesn't break # Detailed test can be added later def test_bank_clearance(self): From 153b41a26902082a2cada995d9a9e5afac30d2aa Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 22 Apr 2022 13:39:43 +0530 Subject: [PATCH 909/951] fix: Do not validate while creating accounting dimension (cherry picked from commit 9bb132fdd3d98a149427be3341e268d818e3bfa1) --- .../doctype/accounting_dimension/accounting_dimension.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 445378300bb..3f1998a3b39 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -99,7 +99,7 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None): if doctype == "Budget": add_dimension_to_budget_doctype(df.copy(), doc) else: - create_custom_field(doctype, df) + create_custom_field(doctype, df, ignore_validate=True) count += 1 @@ -115,7 +115,7 @@ def add_dimension_to_budget_doctype(df, doc): } ) - create_custom_field("Budget", df) + create_custom_field("Budget", df, ignore_validate=True) property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options") From f70fca1c9eee27a82af76cf1661145e77604ab43 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 25 Apr 2022 15:08:09 +0530 Subject: [PATCH 910/951] fix(india): cess value not considered while validating e-invoice totals (#30800) (cherry picked from commit 8e6c7a6bf7b1b934ae239a77e8767c8d8e9c375b) Co-authored-by: Saqib Ansari --- erpnext/regional/india/e_invoice/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index ed5ad2f28ff..060c54e110b 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -553,6 +553,7 @@ def validate_totals(einvoice): + flt(value_details["CgstVal"]) + flt(value_details["SgstVal"]) + flt(value_details["IgstVal"]) + + flt(value_details["CesVal"]) + flt(value_details["OthChrg"]) - flt(value_details["Discount"]) ) From a2d95fc62b97914d39e70a6819bd30e45d22d875 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 20 Apr 2022 12:18:11 +0530 Subject: [PATCH 911/951] fix: First preference to parent cost center rather than round off cost center (cherry picked from commit 0ac11a5b305ecd6002bef94cefe04eba65c13f87) --- erpnext/accounts/general_ledger.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index b6b2fd2088a..7973387067b 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -273,7 +273,7 @@ def round_off_debit_credit(gl_map): def make_round_off_gle(gl_map, debit_credit_diff, precision): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - gl_map[0].company + gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no ) round_off_account_exists = False round_off_gle = frappe._dict() @@ -314,10 +314,17 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): gl_map.append(round_off_gle) -def get_round_off_account_and_cost_center(company): +def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): round_off_account, round_off_cost_center = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] + + # Give first preference to parent cost center for round off GLE + if frappe.db.has_column(voucher_type, "cost_center"): + parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") + if parent_cost_center: + round_off_cost_center = parent_cost_center + if not round_off_account: frappe.throw(_("Please mention Round Off Account in Company")) From fe9f32946ce9648caef926e3d0227b2d8f8652a4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 21 Apr 2022 13:26:44 +0530 Subject: [PATCH 912/951] fix: Use parent cost center for Sales and Purchase Invoice (cherry picked from commit c42547d40f5b23dd0c0a045005c49677863215fd) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 4 +++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a14ae251e01..e4719d6b40c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1317,7 +1317,9 @@ class PurchaseInvoice(BuyingController): if ( not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name + ) gl_entries.append( self.get_gl_dict( diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 269eb37f5b4..e39822e4036 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1473,7 +1473,9 @@ class SalesInvoice(SellingController): and self.base_rounding_adjustment and not self.is_internal_transfer() ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(self.company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Sales Invoice", self.name + ) gl_entries.append( self.get_gl_dict( From dedb90ea72e5d2eb5f213f3aa7887672d4f68078 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 23 Apr 2022 21:40:08 +0530 Subject: [PATCH 913/951] fix: Add accounting dimensions for round off GL Entry (cherry picked from commit 015812b0b8216d0d66e005700691dfa1ace74bf7) --- erpnext/accounts/general_ledger.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 7973387067b..6f7535fb964 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -310,10 +310,22 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): } ) + update_accounting_dimensions(round_off_gle) + if not round_off_account_exists: gl_map.append(round_off_gle) +def update_accounting_dimensions(round_off_gle): + dimensions = get_accounting_dimensions() + dimension_values = frappe.db.get_value( + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions + ) + + for dimension in dimensions: + round_off_gle[dimension] = dimension_values.get(dimension) + + def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): round_off_account, round_off_cost_center = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center"] From 1834671d59eccaaa4af910523466fc5066becf1f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 24 Apr 2022 18:11:32 +0530 Subject: [PATCH 914/951] fix: Check if accounting dimension exists (cherry picked from commit c312cd3725680cff91aa9e41da5a40fa91af7784) --- erpnext/accounts/general_ledger.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 6f7535fb964..f2468864d9f 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -318,12 +318,20 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): def update_accounting_dimensions(round_off_gle): dimensions = get_accounting_dimensions() - dimension_values = frappe.db.get_value( - round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions - ) + meta = frappe.get_meta(round_off_gle["voucher_type"]) + has_all_dimensions = True for dimension in dimensions: - round_off_gle[dimension] = dimension_values.get(dimension) + if not meta.has_field(dimension): + has_all_dimensions = False + + if dimensions and has_all_dimensions: + dimension_values = frappe.db.get_value( + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions + ) + + for dimension in dimensions: + round_off_gle[dimension] = dimension_values.get(dimension) def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): @@ -331,8 +339,10 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] + meta = frappe.get_meta(voucher_type) + # Give first preference to parent cost center for round off GLE - if frappe.db.has_column(voucher_type, "cost_center"): + if meta.has_field("cost_center"): parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") if parent_cost_center: round_off_cost_center = parent_cost_center From 45228263352ee2e084271e9da13f27895e96a71b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 25 Apr 2022 16:29:26 +0530 Subject: [PATCH 915/951] test: Unit test for round off entry dimensions (cherry picked from commit 3fa1c634790095bf7eabc135ed717e124efa4ff0) --- .../sales_invoice/test_sales_invoice.py | 23 +++++++++++++++++++ erpnext/accounts/general_ledger.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 68a83f4be0c..7ebb2a8b7bf 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1982,6 +1982,13 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][2], gle.credit) def test_rounding_adjustment_3(self): + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + disable_dimension, + ) + + create_dimension() + si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: @@ -2009,6 +2016,10 @@ class TestSalesInvoice(unittest.TestCase): "included_in_print_rate": 1, }, ) + + si.cost_center = "_Test Cost Center 2 - _TC" + si.location = "Block 1" + si.save() si.submit() self.assertEqual(si.net_total, 4007.16) @@ -2044,6 +2055,18 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(debit_credit_diff, 0) + round_off_gle = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"}, + ["cost_center", "location"], + as_dict=1, + ) + + self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") + self.assertEqual(round_off_gle.location, "Block 1") + + disable_dimension() + def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f2468864d9f..47e2e0761b6 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -327,7 +327,7 @@ def update_accounting_dimensions(round_off_gle): if dimensions and has_all_dimensions: dimension_values = frappe.db.get_value( - round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1 ) for dimension in dimensions: From edbf5513da121e483b5035719a1956af60271bd6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 26 Apr 2022 14:28:33 +0530 Subject: [PATCH 916/951] fix(pos): search field doesn't reset on checkout (cherry picked from commit 03a6103fe58b35ab18ea669d6cc4e82ff1534200) --- .../page/point_of_sale/pos_controller.js | 4 +-- .../page/point_of_sale/pos_item_selector.js | 36 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 65e0cbb7a5c..7a6838680f0 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -343,9 +343,9 @@ erpnext.PointOfSale.Controller = class { toggle_other_sections: (show) => { if (show) { this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : ''; - this.item_selector.$component.css('display', 'none'); + this.item_selector.toggle_component(false); } else { - this.item_selector.$component.css('display', 'flex'); + this.item_selector.toggle_component(true); } }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index b62b27bc4b3..7a90fb044f3 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -179,6 +179,25 @@ erpnext.PointOfSale.ItemSelector = class { }); this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); + + this.attach_clear_btn(); + } + + attach_clear_btn() { + this.search_field.$wrapper.find('.control-input').append( + ` + + ${frappe.utils.icon('close', 'sm')} + + ` + ); + + this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn'); + + this.$clear_search_btn.on('click', 'a', () => { + this.set_search_value(''); + this.search_field.set_focus(); + }); } set_search_value(value) { @@ -252,6 +271,16 @@ erpnext.PointOfSale.ItemSelector = class { const search_term = e.target.value; this.filter_items({ search_term }); }, 300); + + this.$clear_search_btn.toggle( + Boolean(this.search_field.$input.val()) + ); + }); + + this.search_field.$input.on('focus', () => { + this.$clear_search_btn.toggle( + Boolean(this.search_field.$input.val()) + ); }); } @@ -284,7 +313,7 @@ erpnext.PointOfSale.ItemSelector = class { if (this.items.length == 1) { this.$items_container.find(".item-wrapper").click(); frappe.utils.play_sound("submit"); - $(this.search_field.$input[0]).val("").trigger("input"); + this.set_search_value(''); } else if (this.items.length == 0 && this.barcode_scanned) { // only show alert of barcode is scanned and enter is pressed frappe.show_alert({ @@ -293,7 +322,7 @@ erpnext.PointOfSale.ItemSelector = class { }); frappe.utils.play_sound("error"); this.barcode_scanned = false; - $(this.search_field.$input[0]).val("").trigger("input"); + this.set_search_value(''); } }); } @@ -350,6 +379,7 @@ erpnext.PointOfSale.ItemSelector = class { } toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); + this.set_search_value(''); + this.$component.css('display', show ? 'flex': 'none'); } }; From b01f8555e5d2e0b53d858d61c5a4829b2667084f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 26 Apr 2022 14:41:36 +0530 Subject: [PATCH 917/951] fix(pos): number pad translations (cherry picked from commit b1ac5ff9d27eb54f7c5d1ae2e2ccaaa44bffdcc5) --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 8 ++++---- erpnext/selling/page/point_of_sale/pos_number_pad.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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 4a99f068cd5..eacf480ef8f 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -130,10 +130,10 @@ erpnext.PointOfSale.ItemCart = class { }, cols: 5, keys: [ - [ 1, 2, 3, __('Quantity') ], - [ 4, 5, 6, __('Discount') ], - [ 7, 8, 9, __('Rate') ], - [ '.', 0, __('Delete'), __('Remove') ] + [ 1, 2, 3, 'Quantity' ], + [ 4, 5, 6, 'Discount' ], + [ 7, 8, 9, 'Rate' ], + [ '.', 0, 'Delete', 'Remove' ] ], css_classes: [ [ '', '', '', 'col-span-2' ], diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js index 95293d1dd5c..f27b0d55ef6 100644 --- a/erpnext/selling/page/point_of_sale/pos_number_pad.js +++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js @@ -25,7 +25,7 @@ erpnext.PointOfSale.NumberPad = class { const fieldname = fieldnames && fieldnames[number] ? fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; - return a2 + `
    ${number}
    `; + return a2 + `
    ${__(number)}
    `; }, ''); }, ''); } From 38fbd94ac9fbd68b5fbb9e88bd96be0d701a4277 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 26 Apr 2022 17:23:56 +0530 Subject: [PATCH 918/951] chore: Warn users about multiple warehouse accounts - Get distinct accounts that warehouse value has been booked against - If same account as the one being set, ignore - If not same account or multiple accounts: warn user that it makes it harder to track mismatches (cherry picked from commit 44331f4f1ffb722f1ca2450156c86e16d127697e) --- erpnext/stock/doctype/warehouse/warehouse.py | 50 ++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 3b18a9ac26f..3c2d6b8d4cb 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -36,6 +36,9 @@ class Warehouse(NestedSet): self.set_onload("account", account) load_address_and_contact(self) + def validate(self): + self.warn_about_multiple_warehouse_account() + def on_update(self): self.update_nsm_model() @@ -70,6 +73,53 @@ class Warehouse(NestedSet): self.update_nsm_model() self.unlink_from_items() + def warn_about_multiple_warehouse_account(self): + "If Warehouse value is split across multiple accounts, warn." + + def get_accounts_where_value_is_booked(name): + sle = frappe.qb.DocType("Stock Ledger Entry") + gle = frappe.qb.DocType("GL Entry") + ac = frappe.qb.DocType("Account") + + return ( + frappe.qb.from_(sle) + .join(gle) + .on(sle.voucher_no == gle.voucher_no) + .join(ac) + .on(ac.name == gle.account) + .select(gle.account) + .distinct() + .where((sle.warehouse == name) & (ac.root_type == "Asset")) + .orderby(sle.creation) + .run(as_dict=True) + ) + + if self.is_new(): + return + + old_wh_account = frappe.db.get_value("Warehouse", self.name, "account") + + # WH account is being changed or set get all accounts against which wh value is booked + if self.account != old_wh_account: + accounts = get_accounts_where_value_is_booked(self.name) + accounts = [d.account for d in accounts] + + if not accounts or (len(accounts) == 1 and self.account in accounts): + # if same singular account has stock value booked ignore + return + + warning = _("Warehouse's Stock Value has already been booked in the following accounts:") + account_str = "
    " + ", ".join(frappe.bold(ac) for ac in accounts) + reason = "

    " + _( + "Booking stock value across multiple accounts will make it harder to track stock and account value." + ) + + frappe.msgprint( + warning + account_str + reason, + title=_("Multiple Warehouse Accounts"), + indicator="orange", + ) + def check_if_sle_exists(self): return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name}) From 93482f3302a53c83508dafe3a812819e40079443 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 26 Apr 2022 17:50:51 +0530 Subject: [PATCH 919/951] fix: Use `account_type == 'Stock'` to filter stock accounts (cherry picked from commit e2a163d4e9ca0e26c0bef8de83b4265db083518c) --- erpnext/stock/doctype/warehouse/warehouse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index 3c2d6b8d4cb..df16643d460 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -89,7 +89,7 @@ class Warehouse(NestedSet): .on(ac.name == gle.account) .select(gle.account) .distinct() - .where((sle.warehouse == name) & (ac.root_type == "Asset")) + .where((sle.warehouse == name) & (ac.account_type == "Stock")) .orderby(sle.creation) .run(as_dict=True) ) From b33317e98781e018df18497501ccff0d796f3902 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 26 Apr 2022 18:44:03 +0530 Subject: [PATCH 920/951] chore: format --- erpnext/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 08f4684a9eb..735d0a83c6b 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -4,7 +4,8 @@ import frappe from erpnext.hooks import regional_overrides -__version__ = '13.24.0' +__version__ = "13.24.0" + def get_default_company(user=None): """Get default company for user""" From 3cdbb65b5a886550411659e3cac05287cb090293 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 27 Apr 2022 14:44:25 +0530 Subject: [PATCH 921/951] fix: filters not working in Shift Assignment Calendar view (#30822) --- .../shift_assignment/shift_assignment.py | 20 +++++---- .../shift_assignment/test_shift_assignment.py | 45 ++++++++++++++++++- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 5a1248698c2..868be6ef719 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -84,7 +84,7 @@ class ShiftAssignment(Document): @frappe.whitelist() def get_events(start, end, filters=None): - events = [] + from frappe.desk.calendar import get_event_conditions employee = frappe.db.get_value( "Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True @@ -95,20 +95,22 @@ def get_events(start, end, filters=None): employee = "" company = frappe.db.get_value("Global Defaults", None, "default_company") - from frappe.desk.reportview import get_filters_cond - - conditions = get_filters_cond("Shift Assignment", filters, []) - add_assignments(events, start, end, conditions=conditions) + conditions = get_event_conditions("Shift Assignment", filters) + events = add_assignments(start, end, conditions=conditions) return events -def add_assignments(events, start, end, conditions=None): +def add_assignments(start, end, conditions=None): + events = [] + query = """select name, start_date, end_date, employee_name, employee, docstatus, shift_type from `tabShift Assignment` where - start_date >= %(start_date)s - or end_date <= %(end_date)s - or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date) + ( + start_date >= %(start_date)s + or end_date <= %(end_date)s + or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date) + ) and docstatus = 1""" if conditions: query += conditions diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py index 4a1ec293bde..ea37424762b 100644 --- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py @@ -4,14 +4,22 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, nowdate +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.shift_assignment.shift_assignment import get_events + test_dependencies = ["Shift Type"] -class TestShiftAssignment(unittest.TestCase): +class TestShiftAssignment(FrappeTestCase): def setUp(self): - frappe.db.sql("delete from `tabShift Assignment`") + frappe.db.delete("Shift Assignment") + if not frappe.db.exists("Shift Type", "Day Shift"): + frappe.get_doc( + {"doctype": "Shift Type", "name": "Day Shift", "start_time": "9:00:00", "end_time": "18:00:00"} + ).insert() def test_make_shift_assignment(self): shift_assignment = frappe.get_doc( @@ -86,3 +94,36 @@ class TestShiftAssignment(unittest.TestCase): ) self.assertRaises(frappe.ValidationError, shift_assignment_3.save) + + def test_shift_assignment_calendar(self): + employee1 = make_employee("test_shift_assignment1@example.com", company="_Test Company") + employee2 = make_employee("test_shift_assignment2@example.com", company="_Test Company") + date = nowdate() + + shift_1 = frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": employee1, + "start_date": date, + "status": "Active", + } + ).submit() + + frappe.get_doc( + { + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": employee2, + "start_date": date, + "status": "Active", + } + ).submit() + + events = get_events( + start=date, end=date, filters=[["Shift Assignment", "employee", "=", employee1, False]] + ) + self.assertEqual(len(events), 1) + self.assertEqual(events[0]["name"], shift_1.name) From b45db347cce0db31afb99bc11595237272dd3bc5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 14:45:21 +0530 Subject: [PATCH 922/951] ci: failfast when merge conflict exists (#30823) (#30824) [skip ci] (cherry picked from commit 3ae9fa98c4de9a1c016f6bcddd0b8ce5f1cbb5c5) Co-authored-by: Ankush Menat --- .github/helper/install.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index e7f46410e6c..f9a7a024aea 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -2,6 +2,13 @@ set -e +# Check for merge conflicts before proceeding +python -m compileall -f "${GITHUB_WORKSPACE}" +if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 +fi + cd ~ || exit sudo apt-get install redis-server libcups2-dev From 645ee2d8221f728d41fe52ffddf9b96307fbc8f4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Apr 2022 17:08:37 +0530 Subject: [PATCH 923/951] feat: support product bundles in picklist (backport #30762) (#30826) * refactor: misc pick list refactors - make tracking fields read only and no-copy :facepalm: - collapse print settings section, most users configure it once and forget about it, not need to show this. - call pick list grouping function directly - use get_descendants_of instead of obscure db function (cherry picked from commit 5c3f9019cc4dda9e0c58a7bf9a824e64db995a21) * test: bundles in picklist (cherry picked from commit 7d5682020ad59853ebec83431219671a3648d4b9) # Conflicts: # erpnext/stock/doctype/pick_list/test_pick_list.py * feat: Pick list from SO with Product Bundle (cherry picked from commit 36c5e8a14fd9cebac5c5f22cf062ca3a47a416f0) * refactor: sales order status update - rename badly named variables - support updated packed items (cherry picked from commit e64cc66df74dd1015b3b015adfacc4f665806f7c) # Conflicts: # erpnext/stock/doctype/pick_list/pick_list.py * perf: single update per Sales Order. For each SO item the sales order picking status was being updated, this isn't required and wasteful. (cherry picked from commit c3fc0a4f55789ee273c7acbc0fb711def4af3190) * feat: back-update min picked qty for a bundle (cherry picked from commit 60bc26fdbe9540cbb40a01732d469f3579314be2) * refactor: simplify needlessly complicated code (cherry picked from commit 3ddad6891ae6184e7bdcd12abb1cfa3f6993344d) * refactor: groupby using keys instead of int index (cherry picked from commit 277b51b40482447639d2c0b9d5440e758ea0b76f) * refactor: simpler check for non-SO items (cherry picked from commit f574121741800425e9eb8b34a78e26d8ed1d877a) * feat: create DN from pick list with bundle items (cherry picked from commit 23cb0d684d5e10fd12688b55caddc0911ff4e927) * refactor: remove unnecssary vars also remove misleading docstring (cherry picked from commit 25485edfd966301966a8ccc366da1fa0fd38db1f) * fix: round off bundle qty This is to accomodate bundles that might allow floating point qty. (cherry picked from commit 41aa4b352407190c4f2ed6bc223cb4510555206c) * feat: transfer picklist stock info to packing list (cherry picked from commit 1ac275ce61d0e113aac43a8b5ad81ba7c53b8f65) * test: product bundle fixture (cherry picked from commit ee54ece8fd7dd70d154e3483d372f43681fbc514) * test: test bundle - picklist behaviour (cherry picked from commit 9e60acdf56d15334f55a71c80f3c2e580dc46024) # Conflicts: # erpnext/stock/doctype/pick_list/test_pick_list.py * fix: compare against stock qty while validating Other changes: - only allow whole number of bundles to get picked (cherry picked from commit 8207697e43a8baa3353c0c984e8e99b14fcb5b55) * fix: dont map picked qty and consider pick qty for new PL Co-Authored-By: marination (cherry picked from commit 47e1a0104c83824a315a666c9a21956265c2b7d2) * fix(UX): only show pick list when picking is pending [skip ci] (cherry picked from commit 9a8e3ef2356ac080d8922d5072a4e8a58c5eb676) * chore: make picked qty read only (cherry picked from commit ebd5f0b1bb4a2792bb768b720b800ca5d8724085) * chore: conflicts and py3.7 compatibilty Co-authored-by: Ankush Menat --- .../doctype/sales_order/sales_order.js | 4 +- .../doctype/sales_order/sales_order.json | 3 +- .../doctype/sales_order/sales_order.py | 50 +++- .../sales_order_item/sales_order_item.json | 6 +- .../doctype/packed_item/packed_item.json | 10 +- .../stock/doctype/packed_item/packed_item.py | 6 +- .../doctype/packed_item/test_packed_item.py | 52 ++-- .../stock/doctype/pick_list/pick_list.json | 4 +- erpnext/stock/doctype/pick_list/pick_list.py | 237 +++++++++++++----- .../stock/doctype/pick_list/test_pick_list.py | 92 ++++++- .../pick_list_item/pick_list_item.json | 13 +- 11 files changed, 367 insertions(+), 110 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index c15c917f828..12fc3c74d9c 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -152,7 +152,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } - this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) { + this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + } const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1; const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1; diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index fe2f14e19a6..1d0432bddbe 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1520,6 +1520,7 @@ "fieldname": "per_picked", "fieldtype": "Percent", "label": "% Picked", + "no_copy": 1, "read_only": 1 } ], @@ -1527,7 +1528,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-03-15 21:38:31.437586", + "modified": "2022-04-21 08:16:48.316074", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 3cb422c587d..edd7d26d0bc 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -455,6 +455,16 @@ class SalesOrder(SellingController): if tot_qty != 0: self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False) + def update_picking_status(self): + total_picked_qty = 0.0 + total_qty = 0.0 + for so_item in self.items: + total_picked_qty += flt(so_item.picked_qty) + total_qty += flt(so_item.stock_qty) + per_picked = total_picked_qty / total_qty * 100 + + self.db_set("per_picked", flt(per_picked), update_modified=False) + def set_indicator(self): """Set indicator for portal""" if self.per_billed < 100 and self.per_delivered < 100: @@ -1302,9 +1312,30 @@ def make_inter_company_purchase_order(source_name, target_doc=None): @frappe.whitelist() def create_pick_list(source_name, target_doc=None): - def update_item_quantity(source, target, source_parent): - target.qty = flt(source.qty) - flt(source.delivered_qty) - target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor) + from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle + + def update_item_quantity(source, target, source_parent) -> None: + picked_qty = flt(source.picked_qty) / (flt(source.conversion_factor) or 1) + qty_to_be_picked = flt(source.qty) - max(picked_qty, flt(source.delivered_qty)) + + target.qty = qty_to_be_picked + target.stock_qty = qty_to_be_picked * flt(source.conversion_factor) + + def update_packed_item_qty(source, target, source_parent) -> None: + qty = flt(source.qty) + for item in source_parent.items: + if source.parent_detail_docname == item.name: + picked_qty = flt(item.picked_qty) / (flt(item.conversion_factor) or 1) + pending_percent = (item.qty - max(picked_qty, item.delivered_qty)) / item.qty + target.qty = target.stock_qty = qty * pending_percent + return + + def should_pick_order_item(item) -> bool: + return ( + abs(item.delivered_qty) < abs(item.qty) + and item.delivered_by_supplier != 1 + and not is_product_bundle(item.item_code) + ) doc = get_mapped_doc( "Sales Order", @@ -1315,8 +1346,17 @@ def create_pick_list(source_name, target_doc=None): "doctype": "Pick List Item", "field_map": {"parent": "sales_order", "name": "sales_order_item"}, "postprocess": update_item_quantity, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) - and doc.delivered_by_supplier != 1, + "condition": should_pick_order_item, + }, + "Packed Item": { + "doctype": "Pick List Item", + "field_map": { + "parent": "sales_order", + "name": "sales_order_item", + "parent_detail_docname": "product_bundle_item", + }, + "field_no_map": ["picked_qty"], + "postprocess": update_packed_item_qty, }, }, target_doc, 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 f13cc7e5c2a..21abb94557c 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -801,13 +801,15 @@ { "fieldname": "picked_qty", "fieldtype": "Float", - "label": "Picked Qty" + "label": "Picked Qty (in Stock UOM)", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-03-15 20:17:33.984799", + "modified": "2022-04-27 03:15:34.366563", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index e94c34d7adc..cb8eb30cb30 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -29,6 +29,7 @@ "ordered_qty", "column_break_16", "incoming_rate", + "picked_qty", "page_break", "prevdoc_doctype", "parent_detail_docname" @@ -234,13 +235,20 @@ "label": "Ordered Qty", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "picked_qty", + "fieldtype": "Float", + "label": "Picked Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-03-10 15:42:00.265915", + "modified": "2022-04-27 05:23:08.683245", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 026dd4e122a..4d05d7a345c 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -32,7 +32,7 @@ def make_packing_list(doc): reset = reset_packing_list(doc) for item_row in doc.get("items"): - if frappe.db.exists("Product Bundle", {"new_item_code": item_row.item_code}): + if is_product_bundle(item_row.item_code): for bundle_item in get_product_bundle_items(item_row.item_code): pi_row = add_packed_item_row( doc=doc, @@ -54,6 +54,10 @@ def make_packing_list(doc): set_product_bundle_rate_amount(doc, parent_items_price) # set price in bundle item +def is_product_bundle(item_code: str) -> bool: + return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code})) + + def get_indexed_packed_items_table(doc): """ Create dict from stale packed items table like: diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index fe1b0d9f792..ad7fd9a6976 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,10 +1,12 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from typing import List, Optional, Tuple + +import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_to_date, nowdate -from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.stock.doctype.item.test_item import make_item @@ -12,6 +14,33 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +def create_product_bundle( + quantities: Optional[List[int]] = None, warehouse: Optional[str] = None +) -> Tuple[str, List[str]]: + """Get a new product_bundle for use in tests. + + Create 10x required stock if warehouse is specified. + """ + if not quantities: + quantities = [2, 2] + + bundle = make_item(properties={"is_stock_item": 0}).name + + bundle_doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": bundle}) + + components = [] + for qty in quantities: + compoenent = make_item().name + components.append(compoenent) + bundle_doc.append("items", {"item_code": compoenent, "qty": qty}) + if warehouse: + make_stock_entry(item=compoenent, to_warehouse=warehouse, qty=10 * qty, rate=100) + + bundle_doc.insert() + + return bundle, components + + class TestPackedItem(FrappeTestCase): "Test impact on Packed Items table in various scenarios." @@ -19,24 +48,11 @@ class TestPackedItem(FrappeTestCase): def setUpClass(cls) -> None: super().setUpClass() cls.warehouse = "_Test Warehouse - _TC" - cls.bundle = "_Test Product Bundle X" - cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"] - cls.bundle2 = "_Test Product Bundle Y" - cls.bundle2_items = ["_Test Bundle Item 3", "_Test Bundle Item 4"] + cls.bundle, cls.bundle_items = create_product_bundle(warehouse=cls.warehouse) + cls.bundle2, cls.bundle2_items = create_product_bundle(warehouse=cls.warehouse) - make_item(cls.bundle, {"is_stock_item": 0}) - make_item(cls.bundle2, {"is_stock_item": 0}) - for item in cls.bundle_items + cls.bundle2_items: - make_item(item, {"is_stock_item": 1}) - - make_item("_Test Normal Stock Item", {"is_stock_item": 1}) - - make_product_bundle(cls.bundle, cls.bundle_items, qty=2) - make_product_bundle(cls.bundle2, cls.bundle2_items, qty=2) - - for item in cls.bundle_items + cls.bundle2_items: - make_stock_entry(item_code=item, to_warehouse=cls.warehouse, qty=100, rate=100) + cls.normal_item = make_item().name def test_adding_bundle_item(self): "Test impact on packed items if bundle item row is added." @@ -58,7 +74,7 @@ class TestPackedItem(FrappeTestCase): self.assertEqual(so.packed_items[1].qty, 4) # change item code to non bundle item - so.items[0].item_code = "_Test Normal Stock Item" + so.items[0].item_code = self.normal_item so.save() self.assertEqual(len(so.packed_items), 0) diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index c604c711ef5..e984c082d48 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -114,6 +114,7 @@ "set_only_once": 1 }, { + "collapsible": 1, "fieldname": "print_settings_section", "fieldtype": "Section Break", "label": "Print Settings" @@ -129,7 +130,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-10-05 15:08:40.369957", + "modified": "2022-04-21 07:56:40.646473", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -199,5 +200,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index d3476a88f05..858481aa7b9 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -4,13 +4,14 @@ import json from collections import OrderedDict, defaultdict from itertools import groupby -from operator import itemgetter +from typing import Dict, List, Set import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.utils import cint, floor, flt, today +from frappe.utils.nestedset import get_descendants_of from erpnext.selling.doctype.sales_order.sales_order import ( make_delivery_note as create_delivery_note_from_sales_order, @@ -36,6 +37,7 @@ class PickList(Document): frappe.throw("Row " + str(location.idx) + " has been picked already!") def before_submit(self): + update_sales_orders = set() for item in self.locations: # if the user has not entered any picked qty, set it to stock_qty, before submit if item.picked_qty == 0: @@ -43,7 +45,8 @@ class PickList(Document): if item.sales_order_item: # update the picked_qty in SO Item - self.update_so(item.sales_order_item, item.picked_qty, item.item_code) + self.update_sales_order_item(item, item.picked_qty, item.item_code) + update_sales_orders.add(item.sales_order) if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"): continue @@ -63,18 +66,29 @@ class PickList(Document): title=_("Quantity Mismatch"), ) + self.update_bundle_picked_qty() + self.update_sales_order_picking_status(update_sales_orders) + def before_cancel(self): - # update picked_qty in SO Item on cancel of PL + """Deduct picked qty on cancelling pick list""" + updated_sales_orders = set() + for item in self.get("locations"): if item.sales_order_item: - self.update_so(item.sales_order_item, -1 * item.picked_qty, item.item_code) + self.update_sales_order_item(item, -1 * item.picked_qty, item.item_code) + updated_sales_orders.add(item.sales_order) + + self.update_bundle_picked_qty() + self.update_sales_order_picking_status(updated_sales_orders) + + def update_sales_order_item(self, item, picked_qty, item_code): + item_table = "Sales Order Item" if not item.product_bundle_item else "Packed Item" + stock_qty_field = "stock_qty" if not item.product_bundle_item else "qty" - def update_so(self, so_item, picked_qty, item_code): - so_doc = frappe.get_doc( - "Sales Order", frappe.db.get_value("Sales Order Item", so_item, "parent") - ) already_picked, actual_qty = frappe.db.get_value( - "Sales Order Item", so_item, ["picked_qty", "qty"] + item_table, + item.sales_order_item, + ["picked_qty", stock_qty_field], ) if self.docstatus == 1: @@ -82,23 +96,18 @@ class PickList(Document): 100 + flt(frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance")) ): frappe.throw( - "You are picking more than required quantity for " - + item_code - + ". Check if there is any other pick list created for " - + so_doc.name + _( + "You are picking more than required quantity for {}. Check if there is any other pick list created for {}" + ).format(item_code, item.sales_order) ) - frappe.db.set_value("Sales Order Item", so_item, "picked_qty", already_picked + picked_qty) + frappe.db.set_value(item_table, item.sales_order_item, "picked_qty", already_picked + picked_qty) - total_picked_qty = 0 - total_so_qty = 0 - for item in so_doc.get("items"): - total_picked_qty += flt(item.picked_qty) - total_so_qty += flt(item.stock_qty) - total_picked_qty = total_picked_qty + picked_qty - per_picked = total_picked_qty / total_so_qty * 100 - - so_doc.db_set("per_picked", flt(per_picked), update_modified=False) + @staticmethod + def update_sales_order_picking_status(sales_orders: Set[str]) -> None: + for sales_order in sales_orders: + if sales_order: + frappe.get_doc("Sales Order", sales_order).update_picking_status() @frappe.whitelist() def set_item_locations(self, save=False): @@ -108,7 +117,7 @@ class PickList(Document): from_warehouses = None if self.parent_warehouse: - from_warehouses = frappe.db.get_descendants("Warehouse", self.parent_warehouse) + from_warehouses = get_descendants_of("Warehouse", self.parent_warehouse) # Create replica before resetting, to handle empty table on update after submit. locations_replica = self.get("locations") @@ -189,8 +198,7 @@ class PickList(Document): frappe.throw(_("Qty of Finished Goods Item should be greater than 0.")) def before_print(self, settings=None): - if self.get("group_same_items"): - self.group_similar_items() + self.group_similar_items() def group_similar_items(self): group_item_qty = defaultdict(float) @@ -216,6 +224,58 @@ class PickList(Document): for idx, item in enumerate(self.locations, start=1): item.idx = idx + def update_bundle_picked_qty(self): + product_bundles = self._get_product_bundles() + product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) + + for so_row, item_code in product_bundles.items(): + picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) + item_table = "Sales Order Item" + already_picked = frappe.db.get_value(item_table, so_row, "picked_qty") + frappe.db.set_value( + item_table, + so_row, + "picked_qty", + already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), + ) + + def _get_product_bundles(self) -> Dict[str, str]: + # Dict[so_item_row: item_code] + product_bundles = {} + for item in self.locations: + if not item.product_bundle_item: + continue + product_bundles[item.product_bundle_item] = frappe.db.get_value( + "Sales Order Item", + item.product_bundle_item, + "item_code", + ) + return product_bundles + + def _get_product_bundle_qty_map(self, bundles: List[str]) -> Dict[str, Dict[str, float]]: + # bundle_item_code: Dict[component, qty] + product_bundle_qty_map = {} + for bundle_item_code in bundles: + bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code}) + product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} + return product_bundle_qty_map + + def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: + """Compute how many full bundles can be created from picked items.""" + precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction") + + possible_bundles = [] + for item in self.locations: + if item.product_bundle_item != bundle_row: + continue + + qty_in_bundle = bundle_items.get(item.item_code) + if qty_in_bundle: + possible_bundles.append(item.picked_qty / qty_in_bundle) + else: + possible_bundles.append(0) + return int(flt(min(possible_bundles), precision or 6)) + def validate_item_locations(pick_list): if not pick_list.locations: @@ -449,22 +509,18 @@ def create_delivery_note(source_name, target_doc=None): for location in pick_list.locations: if location.sales_order: sales_orders.append( - [frappe.db.get_value("Sales Order", location.sales_order, "customer"), location.sales_order] + frappe.db.get_value( + "Sales Order", location.sales_order, ["customer", "name as sales_order"], as_dict=True + ) ) - # Group sales orders by customer - for key, keydata in groupby(sales_orders, key=itemgetter(0)): - sales_dict[key] = set([d[1] for d in keydata]) + + for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]): + sales_dict[customer] = {row.sales_order for row in rows} if sales_dict: delivery_note = create_dn_with_so(sales_dict, pick_list) - is_item_wo_so = 0 - for location in pick_list.locations: - if not location.sales_order: - is_item_wo_so = 1 - break - if is_item_wo_so == 1: - # Create a DN for items without sales orders as well + if not all(item.sales_order for item in pick_list.locations): delivery_note = create_dn_wo_so(pick_list) frappe.msgprint(_("Delivery Note(s) created for the Pick List")) @@ -491,27 +547,30 @@ def create_dn_wo_so(pick_list): def create_dn_with_so(sales_dict, pick_list): delivery_note = None + item_table_mapper = { + "doctype": "Delivery Note Item", + "field_map": { + "rate": "rate", + "name": "so_detail", + "parent": "against_sales_order", + }, + "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) + and doc.delivered_by_supplier != 1, + } + for customer in sales_dict: for so in sales_dict[customer]: delivery_note = None delivery_note = create_delivery_note_from_sales_order(so, delivery_note, skip_item_mapping=True) - - item_table_mapper = { - "doctype": "Delivery Note Item", - "field_map": { - "rate": "rate", - "name": "so_detail", - "parent": "against_sales_order", - }, - "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) - and doc.delivered_by_supplier != 1, - } break if delivery_note: # map all items of all sales orders of that customer for so in sales_dict[customer]: map_pl_locations(pick_list, item_table_mapper, delivery_note, so) - delivery_note.insert(ignore_mandatory=True) + delivery_note.flags.ignore_mandatory = True + delivery_note.insert() + update_packed_item_details(pick_list, delivery_note) + delivery_note.save() return delivery_note @@ -519,28 +578,28 @@ def create_dn_with_so(sales_dict, pick_list): def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): for location in pick_list.locations: - if location.sales_order == sales_order: - if location.sales_order_item: - sales_order_item = frappe.get_cached_doc( - "Sales Order Item", {"name": location.sales_order_item} - ) - else: - sales_order_item = None + if location.sales_order != sales_order or location.product_bundle_item: + continue - source_doc, table_mapper = ( - [sales_order_item, item_mapper] if sales_order_item else [location, item_mapper] - ) + if location.sales_order_item: + sales_order_item = frappe.get_doc("Sales Order Item", location.sales_order_item) + else: + sales_order_item = None - dn_item = map_child_doc(source_doc, delivery_note, table_mapper) + source_doc = sales_order_item or location - if dn_item: - dn_item.pick_list_item = location.name - dn_item.warehouse = location.warehouse - dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1) - dn_item.batch_no = location.batch_no - dn_item.serial_no = location.serial_no + dn_item = map_child_doc(source_doc, delivery_note, item_mapper) - update_delivery_note_item(source_doc, dn_item, delivery_note) + if dn_item: + dn_item.pick_list_item = location.name + dn_item.warehouse = location.warehouse + dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1) + dn_item.batch_no = location.batch_no + dn_item.serial_no = location.serial_no + + update_delivery_note_item(source_doc, dn_item, delivery_note) + + add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper) set_delivery_note_missing_values(delivery_note) delivery_note.pick_list = pick_list.name @@ -548,6 +607,50 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None): delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") +def add_product_bundles_to_delivery_note( + pick_list: "PickList", delivery_note, item_mapper +) -> None: + """Add product bundles found in pick list to delivery note. + + When mapping pick list items, the bundle item itself isn't part of the + locations. Dynamically fetch and add parent bundle item into DN.""" + product_bundles = pick_list._get_product_bundles() + product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) + + for so_row, item_code in product_bundles.items(): + sales_order_item = frappe.get_doc("Sales Order Item", so_row) + dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) + dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( + so_row, product_bundle_qty_map[item_code] + ) + update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) + + +def update_packed_item_details(pick_list: "PickList", delivery_note) -> None: + """Update stock details on packed items table of delivery note.""" + + def _find_so_row(packed_item): + for item in delivery_note.items: + if packed_item.parent_detail_docname == item.name: + return item.so_detail + + def _find_pick_list_location(bundle_row, packed_item): + if not bundle_row: + return + for loc in pick_list.locations: + if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code: + return loc + + for packed_item in delivery_note.packed_items: + so_row = _find_so_row(packed_item) + location = _find_pick_list_location(so_row, packed_item) + if not location: + continue + packed_item.warehouse = location.warehouse + packed_item.batch_no = location.batch_no + packed_item.serial_no = location.serial_no + + @frappe.whitelist() def create_stock_entry(pick_list): pick_list = frappe.get_doc(json.loads(pick_list)) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index ec5011b93d6..e8cebc8e622 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -3,18 +3,21 @@ import frappe from frappe import _dict - -test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"] - from frappe.tests.utils import FrappeTestCase -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.selling.doctype.sales_order.sales_order import create_pick_list +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( EmptyStockReconciliationItemsError, ) +test_dependencies = ["Item", "Sales Invoice", "Stock Entry", "Batch"] + class TestPickList(FrappeTestCase): def test_pick_list_picks_warehouse_for_each_item(self): @@ -566,14 +569,79 @@ class TestPickList(FrappeTestCase): if dn_item.item_code == "_Test Item 2": self.assertEqual(dn_item.qty, 2) - # def test_pick_list_skips_items_in_expired_batch(self): - # pass + def test_picklist_with_multi_uom(self): + warehouse = "_Test Warehouse - _TC" + item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name + make_stock_entry(item=item, to_warehouse=warehouse, qty=1000) - # def test_pick_list_from_sales_order(self): - # pass + so = make_sales_order(item_code=item, qty=10, rate=42, uom="Box") + pl = create_pick_list(so.name) + # pick half the qty + for loc in pl.locations: + loc.picked_qty = loc.stock_qty / 2 + pl.save() + pl.submit() - # def test_pick_list_from_work_order(self): - # pass + so.reload() + self.assertEqual(so.per_picked, 50) - # def test_pick_list_from_material_request(self): - # pass + def test_picklist_with_bundles(self): + warehouse = "_Test Warehouse - _TC" + + quantities = [5, 2] + bundle, components = create_product_bundle(quantities, warehouse=warehouse) + bundle_items = dict(zip(components, quantities)) + + so = make_sales_order(item_code=bundle, qty=3, rate=42) + + pl = create_pick_list(so.name) + pl.save() + self.assertEqual(len(pl.locations), 2) + for item in pl.locations: + self.assertEqual(item.stock_qty, bundle_items[item.item_code] * 3) + + # check picking status on sales order + pl.submit() + so.reload() + self.assertEqual(so.per_picked, 100) + + # deliver + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + self.assertEqual(dn.packed_items[0].warehouse, warehouse) + so.reload() + self.assertEqual(so.per_delivered, 100) + + def test_picklist_with_partial_bundles(self): + # from test_records.json + warehouse = "_Test Warehouse - _TC" + + quantities = [5, 2] + bundle, components = create_product_bundle(quantities, warehouse=warehouse) + + so = make_sales_order(item_code=bundle, qty=4, rate=42) + + pl = create_pick_list(so.name) + for loc in pl.locations: + loc.picked_qty = loc.qty / 2 + + pl.save().submit() + so.reload() + self.assertEqual(so.per_picked, 50) + + # deliver half qty + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + so.reload() + self.assertEqual(so.per_delivered, 50) + + pl = create_pick_list(so.name) + pl.save().submit() + so.reload() + self.assertEqual(so.per_picked, 100) + + # deliver remaining + dn = create_delivery_note(pl.name).submit() + self.assertEqual(dn.items[0].rate, 42) + so.reload() + self.assertEqual(so.per_delivered, 100) diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index 805286ddcc0..a96ebfcdee6 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -27,6 +27,7 @@ "column_break_15", "sales_order", "sales_order_item", + "product_bundle_item", "material_request", "material_request_item" ], @@ -146,6 +147,7 @@ { "fieldname": "sales_order_item", "fieldtype": "Data", + "hidden": 1, "label": "Sales Order Item", "read_only": 1 }, @@ -177,11 +179,19 @@ "fieldtype": "Data", "label": "Item Group", "read_only": 1 + }, + { + "description": "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle", + "fieldname": "product_bundle_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Product Bundle Item", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-09-28 12:02:16.923056", + "modified": "2022-04-22 05:27:38.497997", "modified_by": "Administrator", "module": "Stock", "name": "Pick List Item", @@ -190,5 +200,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 731107d0a3fbd9ad7548c86727cdc0775d60826c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:58:24 +0000 Subject: [PATCH 924/951] test: create new item instead of using with _Test Item (backport #30827) (#30829) This is an automatic backport of pull request #30827 done by [Mergify](https://mergify.com). ---
    Mergify commands and options
    More conditions and actions can be found in the [documentation](https://docs.mergify.com/). You can also trigger Mergify actions by commenting on this pull request: - `@Mergifyio refresh` will re-evaluate the rules - `@Mergifyio rebase` will rebase this PR on its base branch - `@Mergifyio update` will merge the base branch into this PR - `@Mergifyio backport ` will backport this PR on `` branch Additionally, on Mergify [dashboard](https://dashboard.mergify.com/) you can: - look at your merge queues - generate the Mergify configuration with the config editor. Finally, you can contact us on https://mergify.com
    --- .../delivery_note/test_delivery_note.py | 54 ------------------- .../stock/doctype/stock_entry/stock_entry.py | 2 +- .../test_stock_reconciliation.py | 22 ++++---- 3 files changed, 13 insertions(+), 65 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index b5a45578c6d..f97e7ca9c68 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -78,56 +78,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertFalse(get_gl_entries("Delivery Note", dn.name)) - # def test_delivery_note_gl_entry(self): - # company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') - - # set_valuation_method("_Test Item", "FIFO") - - # make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) - - # stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory') - # prev_bal = get_balance_on(stock_in_hand_account) - - # dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") - - # gl_entries = get_gl_entries("Delivery Note", dn.name) - # self.assertTrue(gl_entries) - - # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - - # expected_values = { - # stock_in_hand_account: [0.0, stock_value_difference], - # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - # } - # for i, gle in enumerate(gl_entries): - # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - - # # check stock in hand balance - # bal = get_balance_on(stock_in_hand_account) - # self.assertEqual(bal, prev_bal - stock_value_difference) - - # # back dated incoming entry - # make_stock_entry(posting_date=add_days(nowdate(), -2), target="Stores - TCP1", - # qty=5, basic_rate=100) - - # gl_entries = get_gl_entries("Delivery Note", dn.name) - # self.assertTrue(gl_entries) - - # stock_value_difference = abs(frappe.db.get_value("Stock Ledger Entry", - # {"voucher_type": "Delivery Note", "voucher_no": dn.name}, "stock_value_difference")) - - # expected_values = { - # stock_in_hand_account: [0.0, stock_value_difference], - # "Cost of Goods Sold - TCP1": [stock_value_difference, 0.0] - # } - # for i, gle in enumerate(gl_entries): - # self.assertEqual([gle.debit, gle.credit], expected_values.get(gle.account)) - - # dn.cancel() - # self.assertTrue(get_gl_entries("Delivery Note", dn.name)) - # set_perpetual_inventory(0, company) - def test_delivery_note_gl_entry_packing_item(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") @@ -854,8 +804,6 @@ class TestDeliveryNote(FrappeTestCase): company="_Test Company with perpetual inventory", ) - company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) @@ -881,8 +829,6 @@ class TestDeliveryNote(FrappeTestCase): def test_delivery_note_cost_center_with_balance_sheet_account(self): cost_center = "Main - TCP1" - company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") - set_valuation_method("_Test Item", "FIFO") make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e3af675178a..f0566b08897 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -557,7 +557,7 @@ class StockEntry(StockController): ) def set_actual_qty(self): - allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) + allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) for d in self.get("items"): previous_sle = get_previous_sle( diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 45e840322b1..cb9b5abaa23 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -30,7 +30,6 @@ class TestStockReconciliation(FrappeTestCase): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) def tearDown(self): - frappe.flags.dont_execute_stock_reposts = None frappe.local.future_sle = {} def test_reco_for_fifo(self): @@ -40,7 +39,9 @@ class TestStockReconciliation(FrappeTestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1") + item_code = make_item(properties={"valuation_method": valuation_method}).name + + se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code) company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") # [[qty, valuation_rate, posting_date, # posting_time, expected_stock_value, bin_qty, bin_valuation]] @@ -54,11 +55,9 @@ class TestStockReconciliation(FrappeTestCase): ] for d in input_data: - set_valuation_method("_Test Item", valuation_method) - last_sle = get_previous_sle( { - "item_code": "_Test Item", + "item_code": item_code, "warehouse": "Stores - TCP1", "posting_date": d[2], "posting_time": d[3], @@ -67,6 +66,7 @@ class TestStockReconciliation(FrappeTestCase): # submit stock reconciliation stock_reco = create_stock_reconciliation( + item_code=item_code, qty=d[0], rate=d[1], posting_date=d[2], @@ -480,9 +480,11 @@ class TestStockReconciliation(FrappeTestCase): """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + frappe.db.rollback() + # repost will make this test useless, qty should update in realtime without reposts frappe.flags.dont_execute_stock_reposts = True - frappe.db.rollback() + self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") item_code = make_item().name warehouse = "_Test Warehouse - _TC" @@ -593,26 +595,26 @@ def create_batch_item_with_batch(item_name, batch_id): b.save() -def insert_existing_sle(warehouse): +def insert_existing_sle(warehouse, item_code="_Test Item"): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry se1 = make_stock_entry( posting_date="2012-12-15", posting_time="02:00", - item_code="_Test Item", + item_code=item_code, target=warehouse, qty=10, basic_rate=700, ) se2 = make_stock_entry( - posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item", source=warehouse, qty=15 + posting_date="2012-12-25", posting_time="03:00", item_code=item_code, source=warehouse, qty=15 ) se3 = make_stock_entry( posting_date="2013-01-05", posting_time="07:00", - item_code="_Test Item", + item_code=item_code, target=warehouse, qty=15, basic_rate=1200, From 2e62d518e8f9cd253b1002da871922a73f896e6f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 29 Apr 2022 14:27:03 +0530 Subject: [PATCH 925/951] fix: Multi currency opening invoices (cherry picked from commit a8452c2ba241a943f99ca3856985f728e11d47b1) --- .../opening_invoice_creation_tool.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 735ab301aba..0f0ab68dcb8 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -114,10 +114,13 @@ class OpeningInvoiceCreationTool(Document): ) or {} ) + + default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency") + if company_details: invoice.update( { - "currency": company_details.get("default_currency"), + "currency": default_currency or company_details.get("default_currency"), "letter_head": company_details.get("default_letter_head"), } ) From f7bf4a3e621597d254adf477d28e8297700f679a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 29 Apr 2022 16:37:26 +0530 Subject: [PATCH 926/951] fix(UX): record reason for skipping attendance or marking absent for auto attendance (#30846) --- .../employee_checkin/employee_checkin.py | 58 ++++++++++++++----- erpnext/hr/doctype/shift_type/shift_type.py | 12 +++- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 87f48b7e257..f3cd864c908 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, get_datetime +from frappe.utils import cint, get_datetime, get_link_to_form from erpnext.hr.doctype.shift_assignment.shift_assignment import ( get_actual_start_end_datetime_of_shift, @@ -127,19 +127,17 @@ def mark_attendance_and_link_log( log_names = [x.name for x in logs] employee = logs[0].employee if attendance_status == "Skip": - frappe.db.sql( - """update `tabEmployee Checkin` - set skip_auto_attendance = %s - where name in %s""", - ("1", log_names), - ) + skip_attendance_in_checkins(log_names) return None + elif attendance_status in ("Present", "Absent", "Half Day"): employee_doc = frappe.get_doc("Employee", employee) - if not frappe.db.exists( + duplicate = frappe.db.exists( "Attendance", {"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")}, - ): + ) + + if not duplicate: doc_dict = { "doctype": "Attendance", "employee": employee, @@ -155,6 +153,12 @@ def mark_attendance_and_link_log( } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() + + if attendance_status == "Absent": + attendance.add_comment( + text=_("Employee was marked Absent for not meeting the working hours threshold.") + ) + frappe.db.sql( """update `tabEmployee Checkin` set attendance = %s @@ -163,12 +167,10 @@ def mark_attendance_and_link_log( ) return attendance else: - frappe.db.sql( - """update `tabEmployee Checkin` - set skip_auto_attendance = %s - where name in %s""", - ("1", log_names), - ) + skip_attendance_in_checkins(log_names) + if duplicate: + add_comment_in_checkins(log_names, duplicate) + return None else: frappe.throw(_("{} is an invalid Attendance Status.").format(attendance_status)) @@ -237,3 +239,29 @@ def time_diff_in_hours(start, end): def find_index_in_dict(dict_list, key, value): return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) + + +def add_comment_in_checkins(log_names, duplicate): + text = _("Auto Attendance skipped due to duplicate attendance record: {}").format( + get_link_to_form("Attendance", duplicate) + ) + + for name in log_names: + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": "Employee Checkin", + "reference_name": name, + "content": text, + } + ).insert(ignore_permissions=True) + + +def skip_attendance_in_checkins(log_names): + EmployeeCheckin = frappe.qb.DocType("Employee Checkin") + ( + frappe.qb.update(EmployeeCheckin) + .set("skip_auto_attendance", 1) + .where(EmployeeCheckin.name.isin(log_names)) + ).run() diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 3f5cb222bfa..2000eeb5443 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -139,7 +139,17 @@ class ShiftType(Document): for date in dates: shift_details = get_employee_shift(employee, date, True) if shift_details and shift_details.shift_type.name == self.name: - mark_attendance(employee, date, "Absent", self.name) + attendance = mark_attendance(employee, date, "Absent", self.name) + if attendance: + frappe.get_doc( + { + "doctype": "Comment", + "comment_type": "Comment", + "reference_doctype": "Attendance", + "reference_name": attendance, + "content": frappe._("Employee was marked Absent due to missing Employee Checkins."), + } + ).insert(ignore_permissions=True) def get_assigned_employee(self, from_date=None, consider_default_shift=False): filters = {"start_date": (">", from_date), "shift_type": self.name, "docstatus": "1"} From d5319a48269a4277818486e7ed74468e9107b8f4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 30 Apr 2022 22:01:05 +0530 Subject: [PATCH 927/951] fix: Vat Audit report fixes --- .../vat_audit_report/vat_audit_report.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/erpnext/regional/report/vat_audit_report/vat_audit_report.py b/erpnext/regional/report/vat_audit_report/vat_audit_report.py index 6e5982465cf..70f2c0a3339 100644 --- a/erpnext/regional/report/vat_audit_report/vat_audit_report.py +++ b/erpnext/regional/report/vat_audit_report/vat_audit_report.py @@ -95,10 +95,9 @@ class VATAuditReport(object): as_dict=1, ) for d in items: - if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0}) - self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0) - self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0}) + self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0) + self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated def get_items_based_on_tax_rate(self, doctype): self.items_based_on_tax_rate = frappe._dict() @@ -110,7 +109,7 @@ class VATAuditReport(object): self.tax_details = frappe.db.sql( """ SELECT - parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount + parent, account_head, item_wise_tax_detail FROM `tab%s` WHERE @@ -123,7 +122,7 @@ class VATAuditReport(object): tuple([doctype] + list(self.invoices.keys())), ) - for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: + for parent, account, item_wise_tax_detail in self.tax_details: if item_wise_tax_detail: try: if account in self.sa_vat_accounts: @@ -135,7 +134,7 @@ class VATAuditReport(object): # to skip items with non-zero tax rate in multiple rows if taxes[0] == 0 and not is_zero_rated: continue - tax_rate, item_amount_map = self.get_item_amount_map(parent, item_code, taxes) + tax_rate = self.get_item_amount_map(parent, item_code, taxes) if tax_rate is not None: rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault( @@ -151,16 +150,22 @@ class VATAuditReport(object): tax_rate = taxes[0] tax_amount = taxes[1] gross_amount = net_amount + tax_amount - item_amount_map = self.item_tax_rate.setdefault(parent, {}).setdefault(item_code, []) - amount_dict = { - "tax_rate": tax_rate, - "gross_amount": gross_amount, - "tax_amount": tax_amount, - "net_amount": net_amount, - } - item_amount_map.append(amount_dict) - return tax_rate, item_amount_map + self.item_tax_rate.setdefault(parent, {}).setdefault( + item_code, + { + "tax_rate": tax_rate, + "gross_amount": 0.0, + "tax_amount": 0.0, + "net_amount": 0.0, + }, + ) + + self.item_tax_rate[parent][item_code]["net_amount"] += net_amount + self.item_tax_rate[parent][item_code]["tax_amount"] += tax_amount + self.item_tax_rate[parent][item_code]["gross_amount"] += gross_amount + + return tax_rate def get_conditions(self): conditions = "" @@ -205,9 +210,10 @@ class VATAuditReport(object): for inv, inv_data in self.invoices.items(): if self.items_based_on_tax_rate.get(inv): for rate, items in self.items_based_on_tax_rate.get(inv).items(): + row = {"tax_amount": 0.0, "gross_amount": 0.0, "net_amount": 0.0} + consolidated_data_map.setdefault(rate, {"data": []}) for item in items: - row = {} item_details = self.item_tax_rate.get(inv).get(item) row["account"] = inv_data.get("account") row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy") @@ -216,10 +222,11 @@ class VATAuditReport(object): row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier" row["party"] = inv_data.get("party") row["remarks"] = inv_data.get("remarks") - row["gross_amount"] = item_details[0].get("gross_amount") - row["tax_amount"] = item_details[0].get("tax_amount") - row["net_amount"] = item_details[0].get("net_amount") - consolidated_data_map[rate]["data"].append(row) + row["gross_amount"] += item_details.get("gross_amount") + row["tax_amount"] += item_details.get("tax_amount") + row["net_amount"] += item_details.get("net_amount") + + consolidated_data_map[rate]["data"].append(row) return consolidated_data_map From ee54bf7fe25ccb3543d70c7ecb55783a71bb689c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 26 Apr 2022 20:15:15 +0530 Subject: [PATCH 928/951] fix: Ignore custom field validation while setup (cherry picked from commit 67bb29026f6a897bfda0dcccb4a0a2c0d3adeb7f) --- erpnext/regional/united_arab_emirates/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 2acab029149..4b9623f014e 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -245,7 +245,7 @@ def make_custom_fields(): "Supplier Quotation Item": invoice_item_fields, } - create_custom_fields(custom_fields) + create_custom_fields(custom_fields, ignore_validate=True) def add_print_formats(): From 5df50588cf6cff6716b920b5d24464358368bd51 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 26 Apr 2022 14:01:13 +0530 Subject: [PATCH 929/951] fix: Consistent accounting dimensions across Sales and Purchase docs (cherry picked from commit 82a0635c6620837c1871ce6f6597ac35dd45c5c3) # Conflicts: # erpnext/patches.txt # erpnext/selling/doctype/sales_order/sales_order.json --- .../purchase_order/purchase_order.json | 30 +++++++++++++- erpnext/hooks.py | 3 ++ erpnext/patches.txt | 8 +++- .../create_accounting_dimensions_in_orders.py | 39 +++++++++++++++++++ .../doctype/sales_order/sales_order.json | 25 +++++++++++- .../doctype/delivery_note/delivery_note.json | 24 +++++++++++- .../purchase_receipt/purchase_receipt.json | 23 ++++++++++- 7 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 896208f25e1..239f4988303 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -23,6 +23,10 @@ "order_confirmation_no", "order_confirmation_date", "amended_from", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "drop_ship", "customer", "customer_name", @@ -1138,16 +1142,39 @@ "fieldtype": "Link", "label": "Tax Withholding Category", "options": "Tax Withholding Category" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions " + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:10:47.955401", + "modified": "2022-04-26 12:16:38.694276", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -1194,6 +1221,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "supplier_name", "track_changes": 1 diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a21a0313544..ff120a52926 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -592,6 +592,9 @@ accounting_dimension_doctypes = [ "Subscription Plan", "POS Invoice", "POS Invoice Item", + "Purchase Order", + "Purchase Receipt", + "Sales Order", ] regional_overrides = { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 841c59b78a2..f64a51ff7f3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -357,6 +357,12 @@ erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation +<<<<<<< HEAD erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.set_return_against_in_pos_invoice_references -erpnext.patches.v13_0.copy_custom_field_filters_to_website_item \ No newline at end of file +erpnext.patches.v13_0.copy_custom_field_filters_to_website_item +======= +erpnext.patches.v13_0.copy_custom_field_filters_to_website_item +erpnext.patches.v14_0.delete_employee_transfer_property_doctype +erpnext.patches.v13_0.create_accounting_dimensions_in_orders +>>>>>>> 82a0635c66 (fix: Consistent accounting dimensions across Sales and Purchase docs) diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py new file mode 100644 index 00000000000..8a3f1d0a58f --- /dev/null +++ b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py @@ -0,0 +1,39 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + count = 1 + for d in accounting_dimensions: + + if count % 2 == 0: + insert_after_field = "dimension_col_break" + else: + insert_after_field = "accounting_dimensions_section" + + for doctype in ["Purchase Order", "Purchase Receipt", "Sales Order"]: + + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": insert_after_field, + } + + create_custom_field(doctype, df, ignore_validate=False) + frappe.clear_cache(doctype=doctype) + + count += 1 diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 1d0432bddbe..13ef29b7d4c 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -25,6 +25,10 @@ "po_no", "po_date", "tax_id", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "contact_info", "customer_address", "address_display", @@ -113,7 +117,6 @@ "is_internal_customer", "represents_company", "inter_company_order_reference", - "project", "party_account_currency", "column_break_77", "source", @@ -1522,13 +1525,33 @@ "label": "% Picked", "no_copy": 1, "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2022-04-21 08:16:48.316074", +======= + "modified": "2022-04-26 14:38:18.350207", +>>>>>>> 82a0635c66 (fix: Consistent accounting dimensions across Sales and Purchase docs) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 7ebc4eed751..e3222bc8850 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -23,6 +23,10 @@ "is_return", "issue_credit_note", "return_against", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "customer_po_details", "po_no", "column_break_17", @@ -115,7 +119,6 @@ "driver_name", "lr_date", "more_info", - "project", "campaign", "source", "column_break5", @@ -1309,13 +1312,29 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-03-10 14:29:13.428984", + "modified": "2022-04-26 14:48:08.781837", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", @@ -1380,6 +1399,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 7c6189b1eb1..355f0e593ef 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -24,6 +24,10 @@ "apply_putaway_rule", "is_return", "return_against", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "section_addresses", "supplier_address", "contact_person", @@ -107,7 +111,6 @@ "bill_no", "bill_date", "more_info", - "project", "status", "amended_from", "range", @@ -1144,13 +1147,29 @@ "label": "Represents Company", "options": "Company", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-04-10 22:50:37.761362", + "modified": "2022-04-26 13:41:32.625197", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From 7ee18e86a2eeb752ab527cb9efe457466a1cc58d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 10:59:58 +0530 Subject: [PATCH 930/951] feat: Copy task color from project template (backport #30857) (#30859) Co-authored-by: Rucha Mahabal Co-authored-by: sersaber <93864988+sersaber@users.noreply.github.com> --- erpnext/projects/doctype/project/project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 2a3e31f2891..6b691786a1a 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -84,6 +84,7 @@ class Project(Document): type=task_details.type, issue=task_details.issue, is_group=task_details.is_group, + color=task_details.color, ) ).insert() From d9756d54ad0c41cfcb77efddd83c3fa0b4f54379 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 2 May 2022 11:15:58 +0530 Subject: [PATCH 931/951] chore: resolve conflicts --- erpnext/patches.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f64a51ff7f3..4a452c30424 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -357,12 +357,7 @@ erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation -<<<<<<< HEAD erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.copy_custom_field_filters_to_website_item -======= -erpnext.patches.v13_0.copy_custom_field_filters_to_website_item -erpnext.patches.v14_0.delete_employee_transfer_property_doctype erpnext.patches.v13_0.create_accounting_dimensions_in_orders ->>>>>>> 82a0635c66 (fix: Consistent accounting dimensions across Sales and Purchase docs) From b9c326e3f63d70851e28c4f8c4dc962ba0083c36 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 2 May 2022 11:16:48 +0530 Subject: [PATCH 932/951] chore: resolve conflicts --- erpnext/selling/doctype/sales_order/sales_order.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 13ef29b7d4c..6bcc8f05ac3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1547,11 +1547,7 @@ "idx": 105, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2022-04-21 08:16:48.316074", -======= "modified": "2022-04-26 14:38:18.350207", ->>>>>>> 82a0635c66 (fix: Consistent accounting dimensions across Sales and Purchase docs) "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1630,4 +1626,4 @@ "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} From 7c0069c0d32ed47442a455bcf04766c5fede47a5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 2 May 2022 13:29:25 +0530 Subject: [PATCH 933/951] chore: Education domain deprecation warning (#30861) * chore: Education domain deprecation warning * chore: Education domain deprecation warning * chore: Rename file and patch --- erpnext/patches.txt | 1 + erpnext/patches/v13_0/education_deprecation_warning.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 erpnext/patches/v13_0/education_deprecation_warning.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4a452c30424..2fad1efa832 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -360,4 +360,5 @@ erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.copy_custom_field_filters_to_website_item +erpnext.patches.v13_0.education_deprecation_warning erpnext.patches.v13_0.create_accounting_dimensions_in_orders diff --git a/erpnext/patches/v13_0/education_deprecation_warning.py b/erpnext/patches/v13_0/education_deprecation_warning.py new file mode 100644 index 00000000000..96602ebdf94 --- /dev/null +++ b/erpnext/patches/v13_0/education_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Education Domain is moved to a separate app and will be removed from ERPNext in version-14.\n" + "When upgrading to ERPNext version-14, please install the app to continue using the Education domain: https://github.com/frappe/education", + fg="yellow", + ) From 37fad7e04ce7f436a12836da2ef12f96200feb89 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 14:58:05 +0530 Subject: [PATCH 934/951] fix: convert default_item_manufacturer to link field (#30835) (#30866) (cherry picked from commit dcda55641b822f62b9808c36027b59b5eaf697b3) Co-authored-by: Ankush Menat --- erpnext/patches.txt | 1 + ...change_default_item_manufacturer_fieldtype.py | 16 ++++++++++++++++ erpnext/stock/doctype/item/item.json | 5 +++-- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2fad1efa832..d90a5e19488 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -358,6 +358,7 @@ erpnext.patches.v13_0.rename_non_profit_fields erpnext.patches.v13_0.enable_ksa_vat_docs #1 erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances +erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.copy_custom_field_filters_to_website_item erpnext.patches.v13_0.education_deprecation_warning diff --git a/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py b/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py new file mode 100644 index 00000000000..0b00188e6a8 --- /dev/null +++ b/erpnext/patches/v13_0/change_default_item_manufacturer_fieldtype.py @@ -0,0 +1,16 @@ +import frappe + + +def execute(): + + # Erase all default item manufacturers that dont exist. + item = frappe.qb.DocType("Item") + manufacturer = frappe.qb.DocType("Manufacturer") + + ( + frappe.qb.update(item) + .set(item.default_item_manufacturer, None) + .left_join(manufacturer) + .on(item.default_item_manufacturer == manufacturer.name) + .where(manufacturer.name.isnull() & item.default_item_manufacturer.isnotnull()) + ).run() diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index e6f1f0a2952..06baa0fe912 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -918,8 +918,9 @@ }, { "fieldname": "default_item_manufacturer", - "fieldtype": "Data", + "fieldtype": "Link", "label": "Default Item Manufacturer", + "options": "Manufacturer", "read_only": 1 }, { @@ -954,7 +955,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-12-14 04:13:16.857534", + "modified": "2022-04-28 04:52:10.272256", "modified_by": "Administrator", "module": "Stock", "name": "Item", From 5a5b49b61a2b79ddf34fc7b14c6aeec1a1513349 Mon Sep 17 00:00:00 2001 From: Sherin KR Date: Mon, 2 May 2022 15:03:30 +0530 Subject: [PATCH 935/951] fix: Period Closing Voucher is considering GL entries with is_cancelled=1 (#30865) * fix: Period Closing Voucher - Period Closing Voucher is considering GL entry with is_cancelled=1 as well * fix: condition Co-authored-by: Saqib Ansari --- .../doctype/period_closing_voucher/period_closing_voucher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index f66cf1c9f1a..53b1c64c460 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -166,7 +166,7 @@ class PeriodClosingVoucher(AccountsController): sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, sum(t1.debit) - sum(t1.credit) as bal_in_company_currency from `tabGL Entry` t1, `tabAccount` t2 - where t1.account = t2.name and t2.report_type = 'Profit and Loss' + where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss' and t2.docstatus < 2 and t2.company = %s and t1.posting_date between %s and %s group by t1.account, {dimension_fields} From 57b03f0bf259cd84e7cebfac43022fff58698b41 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 15:17:13 +0530 Subject: [PATCH 936/951] fix(UX): misleading stock entry lables (#30870) (#30871) * fix(UX): misleading stock entry lables * chore: field labels [skip ci] Co-authored-by: Marica Co-authored-by: Marica (cherry picked from commit 59a50908436040d6fcb6d9cce1336197a00fe0bb) Co-authored-by: Ankush Menat --- erpnext/stock/doctype/stock_entry/stock_entry.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index c38dfaa1c84..f56e059f81c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -46,9 +46,9 @@ "items", "get_stock_and_rate", "section_break_19", - "total_incoming_value", - "column_break_22", "total_outgoing_value", + "column_break_22", + "total_incoming_value", "value_difference", "additional_costs_section", "additional_costs", @@ -374,7 +374,7 @@ { "fieldname": "total_incoming_value", "fieldtype": "Currency", - "label": "Total Incoming Value", + "label": "Total Incoming Value (Receipt)", "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 @@ -386,7 +386,7 @@ { "fieldname": "total_outgoing_value", "fieldtype": "Currency", - "label": "Total Outgoing Value", + "label": "Total Outgoing Value (Consumption)", "options": "Company:company:default_currency", "print_hide": 1, "read_only": 1 @@ -394,7 +394,7 @@ { "fieldname": "value_difference", "fieldtype": "Currency", - "label": "Total Value Difference (Out - In)", + "label": "Total Value Difference (Incoming - Outgoing)", "options": "Company:company:default_currency", "print_hide_if_no_value": 1, "read_only": 1 @@ -619,7 +619,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-07 12:55:14.614077", + "modified": "2022-05-02 05:21:39.060501", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", From 22e7f03a0325fe449b39ee45cb092f8482af610f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 29 Apr 2022 18:06:13 +0530 Subject: [PATCH 937/951] fix: Cost center filter on payment reconciliation (cherry picked from commit ab94b73e9383cf7b7bca7cb8454d7c55cde0b63b) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js --- .../payment_reconciliation.js | 13 +++++++++++++ .../payment_reconciliation.json | 10 +++++++++- .../payment_reconciliation.py | 3 +++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 648d2da754e..17ac5d959f7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -38,7 +38,20 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext ] }; }); +<<<<<<< HEAD }, +======= + + this.frm.set_query("cost_center", () => { + return { + "filters": { + "company": this.frm.doc.company, + "is_group": 0 + } + } + }); + } +>>>>>>> ab94b73e93 (fix: Cost center filter on payment reconciliation) refresh: function() { this.frm.disable_save(); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index eb0c20f92d9..18d34850850 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -24,6 +24,7 @@ "invoice_limit", "payment_limit", "bank_cash_account", + "cost_center", "sec_break1", "invoices", "column_break_15", @@ -178,13 +179,19 @@ { "fieldname": "column_break_11", "fieldtype": "Column Break" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], - "modified": "2021-10-04 20:27:11.114194", + "modified": "2022-04-29 15:37:10.246831", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -209,5 +216,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b596df92247..e5b942fb6ef 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -332,6 +332,9 @@ class PaymentReconciliation(Document): def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False): condition = " and company = '{0}' ".format(self.company) + if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices): + condition = " and cost_center = '{0}' ".format(self.cost_center) + if get_invoices: condition += ( " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) From 0df96e1084b06e211173a03aa3ad579173587f1a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 2 May 2022 16:09:48 +0530 Subject: [PATCH 938/951] test: Add test for payment reconciliation (cherry picked from commit b440dabe12adfa29c7c61d83a7e145b0c3882cf9) --- .../test_payment_reconciliation.py | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 2271f48a2b9..d2374b77a63 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1,9 +1,96 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_days, getdate + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + class TestPaymentReconciliation(unittest.TestCase): - pass + @classmethod + def setUpClass(cls): + make_customer() + make_invoice_and_payment() + + def test_payment_reconciliation(self): + payment_reco = frappe.get_doc("Payment Reconciliation") + payment_reco.company = "_Test Company" + payment_reco.party_type = "Customer" + payment_reco.party = "_Test Payment Reco Customer" + payment_reco.receivable_payable_account = "Debtors - _TC" + payment_reco.from_invoice_date = add_days(getdate(), -1) + payment_reco.to_invoice_date = getdate() + payment_reco.from_payment_date = add_days(getdate(), -1) + payment_reco.to_payment_date = getdate() + payment_reco.maximum_invoice_amount = 1000 + payment_reco.maximum_payment_amount = 1000 + payment_reco.invoice_limit = 10 + payment_reco.payment_limit = 10 + payment_reco.bank_cash_account = "_Test Bank - _TC" + payment_reco.cost_center = "_Test Cost Center - _TC" + payment_reco.get_unreconciled_entries() + + self.assertEqual(len(payment_reco.get("invoices")), 1) + self.assertEqual(len(payment_reco.get("payments")), 1) + + payment_entry = payment_reco.get("payments")[0].reference_name + invoice = payment_reco.get("invoices")[0].invoice_number + + payment_reco.allocate_entries( + { + "payments": [payment_reco.get("payments")[0].as_dict()], + "invoices": [payment_reco.get("invoices")[0].as_dict()], + } + ) + payment_reco.reconcile() + + payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry) + self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice) + + +def make_customer(): + if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"): + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "_Test Payment Reco Customer", + "customer_type": "Individual", + "customer_group": "_Test Customer Group", + "territory": "_Test Territory", + } + ).insert() + + +def make_invoice_and_payment(): + si = create_sales_invoice( + customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True + ) + si.cost_center = "_Test Cost Center - _TC" + si.save() + si.submit() + + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "payment_type": "Receive", + "party_type": "Customer", + "party": "_Test Payment Reco Customer", + "company": "_Test Company", + "paid_from_account_currency": "INR", + "paid_to_account_currency": "INR", + "source_exchange_rate": 1, + "target_exchange_rate": 1, + "reference_no": "1", + "reference_date": getdate(), + "received_amount": 690, + "paid_amount": 690, + "paid_from": "Debtors - _TC", + "paid_to": "_Test Bank - _TC", + "cost_center": "_Test Cost Center - _TC", + } + ) + pe.insert() + pe.submit() From fdcc591a5e6f6cb2d501c0e1d12361632b187022 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 2 May 2022 19:23:43 +0530 Subject: [PATCH 939/951] fix: Supply type for overseas invoices with payment of tax (cherry picked from commit d7cb269e0c569eaf4e9793a44a313830983d4ee5) --- erpnext/regional/india/e_invoice/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 060c54e110b..70703fae372 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -130,7 +130,10 @@ def get_transaction_details(invoice): elif invoice.gst_category == "SEZ": supply_type = "SEZWOP" elif invoice.gst_category == "Overseas": - supply_type = "EXPWOP" + if invoice.export_type == "Without Payment of Tax": + supply_type = "EXPWOP" + else: + supply_type = "EXPWP" elif invoice.gst_category == "Deemed Export": supply_type = "DEXP" From cf087103cb824f7a5049ed8612a538a1429024f5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 2 May 2022 21:15:54 +0530 Subject: [PATCH 940/951] fix: supply type for sez invoices with payment of tax (cherry picked from commit c8aa77285e9498250d290152d501e5349715177f) --- erpnext/regional/india/e_invoice/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 70703fae372..cbd07607bd2 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -128,7 +128,10 @@ def get_transaction_details(invoice): if invoice.gst_category == "Registered Regular": supply_type = "B2B" elif invoice.gst_category == "SEZ": - supply_type = "SEZWOP" + if invoice.export_type == "Without Payment of Tax": + supply_type = "SEZWOP" + else: + supply_type = "SEZWP" elif invoice.gst_category == "Overseas": if invoice.export_type == "Without Payment of Tax": supply_type = "EXPWOP" From 246869dd28bc2fdea91dd18c756faeee7833b4ae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 2 May 2022 22:16:43 +0530 Subject: [PATCH 941/951] fix(india): e-invoice generation for registered composition gst category type (#30814) (#30877) --- erpnext/regional/india/e_invoice/utils.py | 10 +++++++--- erpnext/regional/india/setup.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 060c54e110b..0b95ba1ef50 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -58,6 +58,7 @@ def validate_eligibility(doc): invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")}) invalid_supply_type = doc.get("gst_category") not in [ "Registered Regular", + "Registered Composition", "SEZ", "Overseas", "Deemed Export", @@ -125,7 +126,9 @@ def read_json(name): def get_transaction_details(invoice): supply_type = "" - if invoice.gst_category == "Registered Regular": + if ( + invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition" + ): supply_type = "B2B" elif invoice.gst_category == "SEZ": supply_type = "SEZWOP" @@ -135,14 +138,15 @@ def get_transaction_details(invoice): supply_type = "DEXP" if not supply_type: - rr, sez, overseas, export = ( + rr, rc, sez, overseas, export = ( bold("Registered Regular"), + bold("Registered Composition"), bold("SEZ"), bold("Overseas"), bold("Deemed Export"), ) frappe.throw( - _("GST category should be one of {}, {}, {}, {}").format(rr, sez, overseas, export), + _("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export), title=_("Invalid Supply Type"), ) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 8e3d8e3c8a7..82d734d845c 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -714,7 +714,7 @@ def get_custom_fields(): insert_after="customer", no_copy=1, print_hide=1, - depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0', + depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0', ), dict( fieldname="irn_cancelled", From a1b0813966109eda2c4f426b57954bc407c4e35f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 May 2022 11:46:46 +0530 Subject: [PATCH 942/951] fix: payment days calculation for employees joining/leaving mid-month (#30863) (#30883) (cherry picked from commit 924cf7763e864b143da463f91f348d2ff5ac07e0) Co-authored-by: Rucha Mahabal --- .../doctype/salary_slip/salary_slip.py | 14 +++- .../doctype/salary_slip/test_salary_slip.py | 78 +++++++++++++++++-- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 1962117608b..b3e4ad54aa5 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -376,13 +376,19 @@ class SalarySlip(TransactionBase): if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)): start_date = joining_date unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( - unmarked_days, include_holidays_in_total_working_days, self.start_date, joining_date + unmarked_days, + include_holidays_in_total_working_days, + self.start_date, + add_days(joining_date, -1), ) if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)): end_date = relieving_date unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving( - unmarked_days, include_holidays_in_total_working_days, relieving_date, self.end_date + unmarked_days, + include_holidays_in_total_working_days, + add_days(relieving_date, 1), + self.end_date, ) # exclude days for which attendance has been marked @@ -408,10 +414,10 @@ class SalarySlip(TransactionBase): from erpnext.hr.doctype.employee.employee import is_holiday if include_holidays_in_total_working_days: - unmarked_days -= date_diff(end_date, start_date) + unmarked_days -= date_diff(end_date, start_date) + 1 else: # exclude only if not holidays - for days in range(date_diff(end_date, start_date)): + for days in range(date_diff(end_date, start_date) + 1): date = add_days(end_date, -days) if not is_holiday(self.employee, date): unmarked_days -= 1 diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 65dae625b6e..0abf58b062c 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -128,6 +128,72 @@ class TestSalarySlip(unittest.TestCase): }, ) def test_payment_days_for_mid_joinee_including_holidays(self): + no_of_days = self.get_no_of_days() + month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) + + new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") + joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) + + for days in range(date_diff(month_end_date, month_start_date) + 1): + date = add_days(month_start_date, days) + mark_attendance(new_emp_id, date, "Present", ignore_validate=True) + + # Case 1: relieving in mid month + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": month_start_date, "relieving_date": relieving_date, "status": "Active"}, + ) + + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + self.assertEqual(new_ss.payment_days, no_of_days[0] - 5) + + # Case 2: joining in mid month + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": month_end_date, "status": "Active"}, + ) + + frappe.delete_doc("Salary Slip", new_ss.name, force=True) + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + self.assertEqual(new_ss.payment_days, no_of_days[0] - 3) + + # Case 3: joining and relieving in mid-month + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, + ) + + frappe.delete_doc("Salary Slip", new_ss.name, force=True) + new_ss = make_employee_salary_slip( + "test_payment_days_based_on_joining_date@salary.com", + "Monthly", + "Test Payment Based On Attendence", + ) + + self.assertEqual(new_ss.total_working_days, no_of_days[0]) + self.assertEqual(new_ss.payment_days, no_of_days[0] - 8) + + @change_settings( + "Payroll Settings", + { + "payroll_based_on": "Attendance", + "consider_unmarked_attendance_as": "Absent", + "include_holidays_in_total_working_days": True, + }, + ) + def test_payment_days_for_mid_joinee_including_holidays_and_unmarked_days(self): + # tests mid month joining and relieving along with unmarked days from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday no_of_days = self.get_no_of_days() @@ -135,12 +201,6 @@ class TestSalarySlip(unittest.TestCase): new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5) - frappe.db.set_value( - "Employee", - new_emp_id, - {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, - ) - holidays = 0 for days in range(date_diff(relieving_date, joining_date) + 1): @@ -150,6 +210,12 @@ class TestSalarySlip(unittest.TestCase): else: holidays += 1 + frappe.db.set_value( + "Employee", + new_emp_id, + {"date_of_joining": joining_date, "relieving_date": relieving_date, "status": "Left"}, + ) + new_ss = make_employee_salary_slip( "test_payment_days_based_on_joining_date@salary.com", "Monthly", From 366cd6171c6537720edba083e9f4e0a31f15c8b0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 3 May 2022 12:21:06 +0530 Subject: [PATCH 943/951] chore: resolve conflicts --- .../payment_reconciliation/payment_reconciliation.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 17ac5d959f7..c0b8fa57011 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -38,10 +38,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext ] }; }); -<<<<<<< HEAD - }, -======= - + this.frm.set_query("cost_center", () => { return { "filters": { @@ -50,8 +47,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext } } }); - } ->>>>>>> ab94b73e93 (fix: Cost center filter on payment reconciliation) + }, refresh: function() { this.frm.disable_save(); From b7e1d40e43c136f0e967111e10af95ddc80e2fd5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 3 May 2022 12:55:04 +0530 Subject: [PATCH 944/951] fix: Ignore loan repayments made from salary slip --- erpnext/accounts/doctype/bank_clearance/bank_clearance.py | 1 + .../bank_reconciliation_tool/bank_reconciliation_tool.py | 1 + .../bank_reconciliation_statement.py | 7 +++++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 0f617b5dda7..98ba399a35d 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -118,6 +118,7 @@ class BankClearance(Document): ) .where(loan_repayment.docstatus == 1) .where(loan_repayment.clearance_date.isnull()) + .where(loan_repayment.repay_from_salary == 0) .where(loan_repayment.posting_date >= self.from_date) .where(loan_repayment.posting_date <= self.to_date) .where(loan_repayment.payment_account.isin([self.bank_account, self.account])) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 4c25d7ccbe8..0efe086d94e 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -467,6 +467,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters): loan_repayment.posting_date, ) .where(loan_repayment.docstatus == 1) + .where(loan_repayment.repay_from_salary == 0) .where(loan_repayment.clearance_date.isnull()) .where(loan_repayment.payment_account == bank_account) ) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 2ac1fea5afc..f3ccc868c4c 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -203,7 +203,7 @@ def get_loan_entries(filters): posting_date = (loan_doc.posting_date).as_("posting_date") account = loan_doc.payment_account - entries = ( + query = ( frappe.qb.from_(loan_doc) .select( ConstantColumn(doctype).as_("payment_document"), @@ -217,9 +217,12 @@ def get_loan_entries(filters): .where(account == filters.get("account")) .where(posting_date <= getdate(filters.get("report_date"))) .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) - .run(as_dict=1) ) + if doctype == "Loan Repayment": + query.where(loan_doc.repay_from_salary == 0) + + entries = query.run(as_dict=1) loan_docs.extend(entries) return loan_docs From 4f4af523e0d5c9e947d94ce13dc416d16941cfe6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 4 May 2022 17:30:52 +0530 Subject: [PATCH 945/951] fix: Show linked time sheets in sales invoice dashboard (cherry picked from commit 3e38dc7ea8a83faa2e714bcebc2d313c8bad7e7a) --- .../doctype/sales_invoice/sales_invoice_dashboard.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index b83d6a575e0..c0005f78cfd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -12,7 +12,10 @@ def get_data(): "Sales Invoice": "return_against", "Auto Repeat": "reference_document", }, - "internal_links": {"Sales Order": ["items", "sales_order"]}, + "internal_links": { + "Sales Order": ["items", "sales_order"], + "Timesheet": ["timesheets", "time_sheet"], + }, "transactions": [ { "label": _("Payment"), From c458e14e680abedf1b4b9ee4f68a28af802c60e2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 May 2022 11:07:11 +0530 Subject: [PATCH 946/951] fix: show group warehouse in Sales Order (#30891) (#30893) (cherry picked from commit 91cd5f5d4a4fee59d23161cd9742375e21bb5033) Co-authored-by: Ankush Menat --- .../doctype/sales_order/sales_order.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 12fc3c74d9c..213909b9b99 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -65,7 +65,11 @@ frappe.ui.form.on("Sales Order", { frm.set_value('transaction_date', frappe.datetime.get_today()) } erpnext.queries.setup_queries(frm, "Warehouse", function() { - return erpnext.queries.warehouse(frm.doc); + return { + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ] + }; }); frm.set_query('project', function(doc, cdt, cdn) { @@ -77,7 +81,19 @@ frappe.ui.form.on("Sales Order", { } }); - erpnext.queries.setup_warehouse_query(frm); + frm.set_query('warehouse', 'items', function(doc, cdt, cdn) { + let row = locals[cdt][cdn]; + let query = { + filters: [ + ["Warehouse", "company", "in", ["", cstr(frm.doc.company)]], + ] + }; + if (row.item_code) { + query.query = "erpnext.controllers.queries.warehouse_query"; + query.filters.push(["Bin", "item_code", "=", row.item_code]); + } + return query; + }); frm.ignore_doctypes_on_cancel_all = ['Purchase Order']; }, From d60a6cb2f83c7972875a2dd201a1ab2d9b5a5bbc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 5 May 2022 17:38:11 +0530 Subject: [PATCH 947/951] fix: disable form save on naming series tool (#30909) (#30910) (cherry picked from commit f31122cbc3bc86b28fda72b565a0a0c7782af769) Co-authored-by: Ankush Menat --- erpnext/setup/doctype/naming_series/naming_series.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/naming_series/naming_series.js b/erpnext/setup/doctype/naming_series/naming_series.js index 7c76d9ca4ba..861b2b39835 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.js +++ b/erpnext/setup/doctype/naming_series/naming_series.js @@ -4,10 +4,13 @@ frappe.ui.form.on("Naming Series", { onload: function(frm) { - frm.disable_save(); frm.events.get_doc_and_prefix(frm); }, + refresh: function(frm) { + frm.disable_save(); + }, + get_doc_and_prefix: function(frm) { frappe.call({ method: "get_transactions", From 98d799e7cc2c3571683164e56d587ba595c8966c Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Fri, 6 May 2022 11:23:16 +0530 Subject: [PATCH 948/951] fix(india): keyerror while generating e-way bill from an e-invoice (#30879) (cherry picked from commit ee7a7eb78232f5ad94e36ae73526a73ca9a9981f) --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 64964853be6..1a60ce2ec47 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -444,7 +444,7 @@ def get_eway_bill_details(invoice): dict( gstin=invoice.gst_transporter_id, name=invoice.transporter_name, - mode_of_transport=mode_of_transport[invoice.mode_of_transport], + mode_of_transport=mode_of_transport[invoice.mode_of_transport or ""] or None, distance=invoice.distance or 0, document_name=invoice.lr_no, document_date=format_date(invoice.lr_date, "dd/mm/yyyy"), From b7698aa2105a7475aa159d4354a444d0b634d708 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 6 May 2022 13:08:09 +0530 Subject: [PATCH 949/951] chore: Linting issues --- .../doctype/payment_reconciliation/payment_reconciliation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index c0b8fa57011..867fcc7f13e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -1,4 +1,4 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors // For license information, please see license.txt frappe.provide("erpnext.accounts"); @@ -38,7 +38,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext ] }; }); - + this.frm.set_query("cost_center", () => { return { "filters": { From 189fc89e2d5cc0f9108ea35b44eb8535bbf40475 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 4 May 2022 15:59:24 +0530 Subject: [PATCH 950/951] fix: Consider paryt and party type as well in group by consolidated view (cherry picked from commit c2d52a1ac0722160efbdc7cf88e0eac499ff8ca7) --- erpnext/accounts/report/general_ledger/general_ledger.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 91c176301af..85b4d153e9f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -478,7 +478,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): gle_map[group_by_value].entries.append(gle) elif group_by_voucher_consolidated: - keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] + keylist = [ + gle.get("voucher_type"), + gle.get("voucher_no"), + gle.get("account"), + gle.get("party_type"), + gle.get("party"), + ] if filters.get("include_dimensions"): for dim in accounting_dimensions: keylist.append(gle.get(dim)) From bf2eaecb1d8cec3676162868422ac96a7cc958cc Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Fri, 6 May 2022 18:06:21 +0530 Subject: [PATCH 951/951] fix: Set available-for-use date if missing (#30838) --- .../doctype/asset_repair/asset_repair.py | 58 ++++++++++--------- erpnext/patches.txt | 1 + .../set_available_for_use_date_if_missing.py | 22 +++++++ 3 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 erpnext/patches/v13_0/set_available_for_use_date_if_missing.py diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 5bf6011cf80..94b4c641333 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -41,40 +41,46 @@ class AssetRepair(AccountsController): if self.get("stock_consumption") or self.get("capitalize_repair_cost"): self.increase_asset_value() - if self.get("stock_consumption"): - self.check_for_stock_items_and_warehouse() - self.decrease_stock_quantity() - if self.get("capitalize_repair_cost"): - self.make_gl_entries() - if ( - frappe.db.get_value("Asset", self.asset, "calculate_depreciation") - and self.increase_in_asset_life - ): - self.modify_depreciation_schedule() - self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() - self.asset_doc.save() + if self.get("stock_consumption"): + self.check_for_stock_items_and_warehouse() + self.decrease_stock_quantity() + + if self.get("capitalize_repair_cost"): + self.make_gl_entries() + + if ( + frappe.db.get_value("Asset", self.asset, "calculate_depreciation") + and self.increase_in_asset_life + ): + self.modify_depreciation_schedule() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() def before_cancel(self): self.asset_doc = frappe.get_doc("Asset", self.asset) if self.get("stock_consumption") or self.get("capitalize_repair_cost"): self.decrease_asset_value() - if self.get("stock_consumption"): - self.increase_stock_quantity() - if self.get("capitalize_repair_cost"): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") - self.make_gl_entries(cancel=True) - if ( - frappe.db.get_value("Asset", self.asset, "calculate_depreciation") - and self.increase_in_asset_life - ): - self.revert_depreciation_schedule_on_cancellation() - self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() - self.asset_doc.save() + if self.get("stock_consumption"): + self.increase_stock_quantity() + + if self.get("capitalize_repair_cost"): + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.make_gl_entries(cancel=True) + + if ( + frappe.db.get_value("Asset", self.asset, "calculate_depreciation") + and self.increase_in_asset_life + ): + self.revert_depreciation_schedule_on_cancellation() + + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.asset_doc.prepare_depreciation_data() + self.asset_doc.save() def check_repair_status(self): if self.repair_status == "Pending": diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d90a5e19488..5d95f824dce 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -361,5 +361,6 @@ erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances erpnext.patches.v13_0.change_default_item_manufacturer_fieldtype erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.copy_custom_field_filters_to_website_item +erpnext.patches.v13_0.set_available_for_use_date_if_missing erpnext.patches.v13_0.education_deprecation_warning erpnext.patches.v13_0.create_accounting_dimensions_in_orders diff --git a/erpnext/patches/v13_0/set_available_for_use_date_if_missing.py b/erpnext/patches/v13_0/set_available_for_use_date_if_missing.py new file mode 100644 index 00000000000..3cfbd6e7fd0 --- /dev/null +++ b/erpnext/patches/v13_0/set_available_for_use_date_if_missing.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + """ + Sets available-for-use date for Assets created in older versions of ERPNext, + before the field was introduced. + """ + + assets = get_assets_without_available_for_use_date() + + for asset in assets: + frappe.db.set_value("Asset", asset.name, "available_for_use_date", asset.purchase_date) + +def get_assets_without_available_for_use_date(): + return frappe.get_all( + "Asset", + filters = { + "available_for_use_date": ["in", ["", None]] + }, + fields = ["name", "purchase_date"] + )