diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js index 1c19c1d2255..cf5fbe12afe 100644 --- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js +++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on("Journal Entry Template", { - setup: function(frm) { + refresh: function(frm) { frappe.model.set_default_values(frm.doc); frm.set_query("account" ,"accounts", function(){ diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index d5f7ee4f217..994b6776e3c 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -43,6 +43,7 @@ "currency", "write_off_account", "write_off_cost_center", + "write_off_limit", "account_for_change_amount", "disable_rounded_total", "column_break_23", @@ -360,6 +361,14 @@ "fieldtype": "Check", "label": "Validate Stock on Save" }, + { + "default": "1", + "description": "Auto write off precision loss while consolidation", + "fieldname": "write_off_limit", + "fieldtype": "Currency", + "label": "Write Off Limit", + "reqd": 1 + }, { "default": "0", "description": "If enabled, the consolidated invoices will have rounded total disabled", @@ -393,7 +402,7 @@ "link_fieldname": "pos_profile" } ], - "modified": "2022-07-21 11:16:46.911173", + "modified": "2022-08-10 12:57:06.241439", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", 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 8ec726b36cd..1f88849b26c 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.py @@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document): filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, ) - make_gl_entries(gl_entries=gl_entries, cancel=1) + make_gl_entries(gl_map=gl_entries, cancel=1) diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py index 164ba6aa348..5a0aeb7284a 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py +++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py @@ -57,3 +57,16 @@ class TestProcessDeferredAccounting(unittest.TestCase): ] check_gl_entries(self, si.name, expected_gle, "2019-01-10") + + def test_pda_submission_and_cancellation(self): + pda = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date="2019-01-01", + start_date="2019-01-01", + end_date="2019-01-31", + type="Income", + ) + ) + pda.submit() + pda.cancel() diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 28b8d257783..3f504b13f67 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -179,6 +179,11 @@ class ReceivablePayableReport(object): key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) row = self.voucher_balance.get(key) + + if not row: + # no invoice, this is an invoice / stand-alone payment / credit note + row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + return row def update_voucher_balance(self, ple): diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index edddbbce219..bac8beed2e2 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,19 +1,27 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -class TestAccountsReceivable(unittest.TestCase): - def test_accounts_receivable(self): +class TestAccountsReceivable(FrappeTestCase): + def setUp(self): frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") + frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") + frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") + def tearDown(self): + frappe.db.rollback() + + def test_accounts_receivable(self): filters = { "company": "_Test Company 2", "based_on_payment_terms": 1, @@ -66,6 +74,50 @@ class TestAccountsReceivable(unittest.TestCase): ], ) + def test_payment_againt_po_in_receivable_report(self): + """ + Payments made against Purchase Order will show up as outstanding amount + """ + + so = make_sales_order( + company="_Test Company 2", + customer="_Test Customer 2", + warehouse="Finished Goods - _TC2", + currency="EUR", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + ) + + pe = get_payment_entry(so.doctype, so.name) + pe = pe.save().submit() + + filters = { + "company": "_Test Company 2", + "based_on_payment_terms": 0, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + report = execute(filters) + + expected_data_after_payment = [0, 1000, 0, -1000] + + row = report[1][0] + self.assertEqual( + expected_data_after_payment, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + def make_sales_invoice(): frappe.set_user("Administrator") 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 98dbbf6c449..330e442a808 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -535,7 +535,11 @@ def get_accounts(root_type, companies): ): if account.account_name not in added_accounts: accounts.append(account) - added_accounts.append(account.account_name) + if account.account_number: + account_key = account.account_number + "-" + account.account_name + else: + account_key = account.account_name + added_accounts.append(account_key) return accounts diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 21205c31634..615804ef623 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -4,7 +4,7 @@ frappe.query_reports["Gross Profit"] = { "filters": [ { - "fieldname":"company", + "fieldname": "company", "label": __("Company"), "fieldtype": "Link", "options": "Company", @@ -12,32 +12,44 @@ frappe.query_reports["Gross Profit"] = { "reqd": 1 }, { - "fieldname":"from_date", + "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_start_date"), "reqd": 1 }, { - "fieldname":"to_date", + "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), "reqd": 1 }, { - "fieldname":"sales_invoice", + "fieldname": "sales_invoice", "label": __("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice" }, { - "fieldname":"group_by", + "fieldname": "group_by", "label": __("Group By"), "fieldtype": "Select", "options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term", "default": "Invoice" }, + { + "fieldname": "item_group", + "label": __("Item Group"), + "fieldtype": "Link", + "options": "Item Group" + }, + { + "fieldname": "sales_person", + "label": __("Sales Person"), + "fieldtype": "Link", + "options": "Sales Person" + }, ], "tree": true, "name_field": "parent", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 54af2259154..f0106bea4fc 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -7,6 +7,7 @@ from frappe import _, scrub from frappe.utils import cint, flt, formatdate from erpnext.controllers.queries import get_match_cond +from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.utils import get_incoming_rate @@ -676,6 +677,17 @@ class GrossProfitGenerator(object): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" + if self.filters.item_group: + conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) + + if self.filters.sales_person: + conditions += """ + and exists(select 1 + from `tabSales Team` st + where st.parent = `tabSales Invoice`.name + and st.sales_person = %(sales_person)s) + """ + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" @@ -723,6 +735,7 @@ class GrossProfitGenerator(object): from `tabSales Invoice` inner join `tabSales Invoice Item` on `tabSales Invoice Item`.parent = `tabSales Invoice`.name + join `tabItem` item on item.name = `tabSales Invoice Item`.item_code {sales_team_table} {payment_term_table} where diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 6c18a4650b7..aad26075f2d 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -76,7 +76,7 @@ "label": "Subcontracting Settings" }, { - "default": "Material Transferred for Subcontract", + "default": "BOM", "fieldname": "backflush_raw_materials_of_subcontract_based_on", "fieldtype": "Select", "label": "Backflush Raw Materials of Subcontract Based On", @@ -148,7 +148,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-05-31 19:40:26.103909", + "modified": "2022-09-01 18:01:34.994657", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index a3d41ab29af..5e9c069b1d5 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -311,6 +311,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": p.get("incoming_rate"), + "item_row": p, } ) ) @@ -334,6 +335,7 @@ class SellingController(StockController): "sales_invoice_item": d.get("sales_invoice_item"), "dn_detail": d.get("dn_detail"), "incoming_rate": d.get("incoming_rate"), + "item_row": d, } ) ) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 36bed36484e..d4f9aba41d2 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -390,6 +390,10 @@ class StockController(AccountsController): return sl_dict def update_inventory_dimensions(self, row, sl_dict) -> None: + # To handle delivery note and sales invoice + if row.get("item_row"): + row = row.get("item_row") + dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self) for dimension in dimensions: if not dimension: @@ -407,9 +411,17 @@ class StockController(AccountsController): "DocField", {"parent": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" ) + if not fieldname: + fieldname = frappe.get_cached_value( + "Custom Field", {"dt": self.doctype, "options": dimension.fetch_from_parent}, "fieldname" + ) + if fieldname and self.get(fieldname): sl_dict[dimension.target_fieldname] = self.get(fieldname) + if sl_dict[dimension.target_fieldname] and self.docstatus == 1: + row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname]) + def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.stock.stock_ledger import make_sl_entries diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 9dbcdb04c5b..cbcccce5f71 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -770,6 +770,18 @@ class calculate_taxes_and_totals(object): self.doc.precision("outstanding_amount"), ) + if ( + self.doc.doctype == "Sales Invoice" + and self.doc.get("is_pos") + and self.doc.get("pos_profile") + and self.doc.get("is_consolidated") + ): + write_off_limit = flt( + frappe.db.get_value("POS Profile", self.doc.pos_profile, "write_off_limit") + ) + if write_off_limit and abs(self.doc.outstanding_amount) <= write_off_limit: + self.doc.write_off_outstanding_amount_automatically = 1 + if ( self.doc.doctype == "Sales Invoice" and self.doc.get("is_pos") diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5f5923dc89b..6e7ba1fd5bc 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -7,7 +7,7 @@ from collections import Counter import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import get_url, getdate +from frappe.utils import get_url, getdate, now from frappe.utils.verified_command import get_signed_params @@ -104,16 +104,28 @@ class Appointment(Document): # Return if already linked if self.party: return + lead = frappe.get_doc( { "doctype": "Lead", "lead_name": self.customer_name, "email_id": self.customer_email, - "notes": self.customer_details, "phone": self.customer_phone_number, } ) + + if self.customer_details: + lead.append( + "notes", + { + "note": self.customer_details, + "added_by": frappe.session.user, + "added_on": now(), + }, + ) + lead.insert(ignore_permissions=True) + # Link lead self.party = lead.name diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 776e6043331..178b9d2de53 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -6,29 +6,20 @@ import unittest import frappe - -def create_test_lead(): - test_lead = frappe.db.get_value("Lead", {"email_id": "test@example.com"}) - if test_lead: - return frappe.get_doc("Lead", test_lead) - test_lead = frappe.get_doc( - {"doctype": "Lead", "lead_name": "Test Lead", "email_id": "test@example.com"} - ) - test_lead.insert(ignore_permissions=True) - return test_lead +LEAD_EMAIL = "test_appointment_lead@example.com" -def create_test_appointments(): +def create_test_appointment(): test_appointment = frappe.get_doc( { "doctype": "Appointment", - "email": "test@example.com", "status": "Open", "customer_name": "Test Lead", "customer_phone_number": "666", "customer_skype": "test", - "customer_email": "test@example.com", + "customer_email": LEAD_EMAIL, "scheduled_time": datetime.datetime.now(), + "customer_details": "Hello, Friend!", } ) test_appointment.insert() @@ -36,16 +27,16 @@ def create_test_appointments(): class TestAppointment(unittest.TestCase): - test_appointment = test_lead = None + def setUpClass(): + frappe.db.delete("Lead", {"email_id": LEAD_EMAIL}) def setUp(self): - self.test_lead = create_test_lead() - self.test_appointment = create_test_appointments() + self.test_appointment = create_test_appointment() + self.test_appointment.set_verified(self.test_appointment.customer_email) def test_calendar_event_created(self): cal_event = frappe.get_doc("Event", self.test_appointment.calendar_event) self.assertEqual(cal_event.starts_on, self.test_appointment.scheduled_time) def test_lead_linked(self): - lead = frappe.get_doc("Lead", self.test_lead.name) - self.assertIsNotNone(lead) + self.assertTrue(self.test_appointment.party) 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 4978f1fcb89..15e9e3f4c1a 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 @@ -233,7 +233,7 @@ def get_term_loans(date, term_loan=None, loan_type=None): AND l.is_term_loan =1 AND rs.payment_date <= %s AND rs.is_accrued=0 {0} - AND rs.interest_amount > 0 + AND rs.principal_amount > 0 AND l.status = 'Disbursed' ORDER BY rs.payment_date""".format( condition diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 07a1d0d8506..9b7603fc3b3 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -732,6 +732,7 @@ def get_amounts(amounts, against_loan, posting_date): ) amounts["pending_accrual_entries"] = pending_accrual_entries amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) + amounts["written_off_amount"] = flt(against_loan_doc.written_off_amount, precision) if final_due_date: amounts["due_date"] = final_due_date 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 81464a36c3d..25c72d91a7c 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 @@ -57,7 +57,7 @@ def process_loan_interest_accrual_for_demand_loans( 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()): + if not term_loan_accrual_pending(posting_date or nowdate(), loan=loan): return loan_process = frappe.new_doc("Process Loan Interest Accrual") @@ -71,9 +71,12 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No return loan_process.name -def term_loan_accrual_pending(date): - pending_accrual = frappe.db.get_value( - "Repayment Schedule", {"payment_date": ("<=", date), "is_accrued": 0} - ) +def term_loan_accrual_pending(date, loan=None): + filters = {"payment_date": ("<=", date), "is_accrued": 0} + + if loan: + filters.update({"parent": loan}) + + pending_accrual = frappe.db.get_value("Repayment Schedule", filters) return pending_accrual diff --git a/erpnext/patches/v13_0/delete_old_sales_reports.py b/erpnext/patches/v13_0/delete_old_sales_reports.py index b31c9d17d71..1b53da755cd 100644 --- a/erpnext/patches/v13_0/delete_old_sales_reports.py +++ b/erpnext/patches/v13_0/delete_old_sales_reports.py @@ -16,18 +16,18 @@ def execute(): delete_auto_email_reports(report) check_and_delete_linked_reports(report) - frappe.delete_doc("Report", report) + frappe.delete_doc("Report", report, force=True) def delete_auto_email_reports(report): """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]) + frappe.delete_doc("Auto Email Report", auto_email_report[0], force=True) def delete_links_from_desktop_icons(report): """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]) + frappe.delete_doc("Desktop Icon", desktop_icon[0], force=True) diff --git a/erpnext/patches/v14_0/update_opportunity_currency_fields.py b/erpnext/patches/v14_0/update_opportunity_currency_fields.py index 076de526195..b803e9fa2dd 100644 --- a/erpnext/patches/v14_0/update_opportunity_currency_fields.py +++ b/erpnext/patches/v14_0/update_opportunity_currency_fields.py @@ -1,3 +1,4 @@ +import click import frappe from frappe.utils import flt @@ -16,6 +17,19 @@ def execute(): for opportunity in opportunities: company_currency = erpnext.get_company_currency(opportunity.company) + if opportunity.currency is None or opportunity.currency == "": + opportunity.currency = company_currency + frappe.db.set_value( + "Opportunity", + opportunity.name, + {"currency": opportunity.currency}, + update_modified=False, + ) + click.secho( + f' Opportunity `{opportunity.name}` has no currency set. Setting it to company currency as default: `{opportunity.currency}`"\n', + fg="yellow", + ) + # base total and total will be 0 only since item table did not have amount field earlier if opportunity.currency != company_currency: conversion_rate = get_exchange_rate(opportunity.currency, company_currency) diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index 15996d2d1f8..3571f962667 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -177,16 +177,16 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): "parent": invoice.name, "item_tax_template": vat_setting.item_tax_template, }, - fields=["item_code", "net_amount"], + fields=["item_code", "base_net_amount"], ) for item in invoice_items: # Summing up total taxable amount if invoice.is_return == 0: - total_taxable_amount += item.net_amount + total_taxable_amount += item.base_net_amount if invoice.is_return == 1: - total_taxable_adjustment_amount += item.net_amount + total_taxable_adjustment_amount += item.base_net_amount # Summing up total tax total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 7a806d5906f..39e0acd02aa 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -10,79 +10,89 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "basic_details_tab", "basic_information", "employee", "naming_series", "first_name", "middle_name", "last_name", - "salutation", "employee_name", - "image", - "column_break1", - "company", - "status", + "column_break_9", "gender", "date_of_birth", + "salutation", + "column_break1", "date_of_joining", - "employee_number", - "emergency_contact_details", - "person_to_be_contacted", - "relation", - "column_break_19", - "emergency_phone_number", + "image", + "status", "erpnext_user", "user_id", "create_user", "create_user_permission", - "employment_details", - "scheduled_confirmation_date", - "final_confirmation_date", - "col_break_22", - "contract_end_date", - "notice_number_of_days", - "date_of_retirement", - "job_profile", + "company_details_section", + "company", "department", + "employee_number", + "column_break_25", "designation", "reports_to", - "column_break_31", + "column_break_18", "branch", + "employment_details", + "scheduled_confirmation_date", + "column_break_32", + "final_confirmation_date", + "contract_end_date", + "col_break_22", + "notice_number_of_days", + "date_of_retirement", + "contact_details", + "cell_number", + "column_break_40", + "personal_email", + "company_email", + "column_break4", + "prefered_contact_email", + "prefered_email", + "unsubscribed", + "address_section", + "current_address", + "current_accommodation_type", + "column_break_46", + "permanent_address", + "permanent_accommodation_type", + "emergency_contact_details", + "person_to_be_contacted", + "column_break_55", + "emergency_phone_number", + "column_break_19", + "relation", "attendance_and_leave_details", "attendance_device_id", "column_break_44", "holiday_list", "salary_information", - "salary_currency", "ctc", - "payroll_cost_center", - "column_break_52", + "salary_currency", + "salary_mode", + "bank_details_section", "bank_name", "bank_ac_no", - "contact_details", - "cell_number", - "prefered_email", - "personal_email", - "unsubscribed", - "permanent_accommodation_type", - "permanent_address", - "column_break4", - "prefered_contact_email", - "company_email", - "current_accommodation_type", - "current_address", - "sb53", - "bio", "personal_details", - "passport_number", - "date_of_issue", - "valid_upto", - "place_of_issue", "marital_status", - "blood_group", - "column_break6", "family_background", + "column_break6", + "blood_group", "health_details", + "passport_details_section", + "passport_number", + "valid_upto", + "column_break_73", + "date_of_issue", + "place_of_issue", + "profile_tab", + "bio", "educational_qualification", "education", "previous_work_experience", @@ -92,16 +102,20 @@ "exit", "resignation_letter_date", "relieving_date", - "reason_for_leaving", - "leave_encashed", - "encashment_date", "exit_interview_details", "held_on", "new_workplace", + "column_break_99", + "leave_encashed", + "encashment_date", + "feedback_section", + "reason_for_leaving", + "column_break_104", "feedback", "lft", "rgt", - "old_parent" + "old_parent", + "connections_tab" ], "fields": [ { @@ -261,7 +275,7 @@ "collapsible": 1, "fieldname": "erpnext_user", "fieldtype": "Section Break", - "label": "ERPNext User" + "label": "User Details" }, { "description": "System User (login) ID. If set, it will become default for all HR forms.", @@ -289,8 +303,8 @@ "allow_in_quick_entry": 1, "collapsible": 1, "fieldname": "employment_details", - "fieldtype": "Section Break", - "label": "Joining Details" + "fieldtype": "Tab Break", + "label": "Joining" }, { "fieldname": "scheduled_confirmation_date", @@ -331,12 +345,6 @@ "oldfieldname": "date_of_retirement", "oldfieldtype": "Date" }, - { - "collapsible": 1, - "fieldname": "job_profile", - "fieldtype": "Section Break", - "label": "Department" - }, { "fieldname": "department", "fieldtype": "Link", @@ -366,10 +374,6 @@ "oldfieldtype": "Link", "options": "Employee" }, - { - "fieldname": "column_break_31", - "fieldtype": "Column Break" - }, { "fieldname": "branch", "fieldtype": "Link", @@ -391,7 +395,7 @@ { "collapsible": 1, "fieldname": "salary_information", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Salary Details", "oldfieldtype": "Section Break", "width": "50%" @@ -423,8 +427,8 @@ { "collapsible": 1, "fieldname": "contact_details", - "fieldtype": "Section Break", - "label": "Contact Details" + "fieldtype": "Tab Break", + "label": "Contact" }, { "fieldname": "cell_number", @@ -493,12 +497,6 @@ "fieldtype": "Small Text", "label": "Current Address" }, - { - "collapsible": 1, - "fieldname": "sb53", - "fieldtype": "Section Break", - "label": "Personal Bio" - }, { "description": "Short biography for website and other publications.", "fieldname": "bio", @@ -508,7 +506,7 @@ { "collapsible": 1, "fieldname": "personal_details", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Personal Details" }, { @@ -601,7 +599,7 @@ { "collapsible": 1, "fieldname": "exit", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Exit", "oldfieldtype": "Section Break" }, @@ -702,7 +700,7 @@ { "collapsible": 1, "fieldname": "attendance_and_leave_details", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Attendance and Leave Details" }, { @@ -713,10 +711,6 @@ "fieldname": "column_break_19", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_52", - "fieldtype": "Column Break" - }, { "fieldname": "salary_currency", "fieldtype": "Link", @@ -728,13 +722,95 @@ "fieldtype": "Currency", "label": "Cost to Company (CTC)", "options": "salary_currency" + }, + { + "fieldname": "basic_details_tab", + "fieldtype": "Tab Break", + "label": "Basic Details" + }, + { + "fieldname": "company_details_section", + "fieldtype": "Section Break", + "label": "Company Details" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "address_section", + "fieldtype": "Section Break", + "label": "Address" + }, + { + "fieldname": "column_break_46", + "fieldtype": "Column Break" + }, + { + "fieldname": "profile_tab", + "fieldtype": "Tab Break", + "label": "Profile" + }, + { + "fieldname": "passport_details_section", + "fieldtype": "Section Break", + "label": "Passport Details" + }, + { + "fieldname": "column_break_73", + "fieldtype": "Column Break" + }, + { + "fieldname": "bank_details_section", + "fieldtype": "Section Break", + "label": "Bank Details" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fieldname": "column_break_32", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_40", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_55", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_99", + "fieldtype": "Column Break" + }, + { + "fieldname": "feedback_section", + "fieldtype": "Section Break", + "label": "Feedback" + }, + { + "fieldname": "column_break_104", + "fieldtype": "Column Break" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2022-06-27 01:29:32.952091", + "modified": "2022-08-23 13:47:46.944993", "modified_by": "Administrator", "module": "Setup", "name": "Employee", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 07cb73b1d56..79e7895f6d0 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -30,6 +30,7 @@ frappe.ui.form.on('Inventory Dimension', { onload(frm) { frm.trigger('render_traget_field'); + frm.trigger("set_parent_fields"); }, refresh(frm) { @@ -52,6 +53,30 @@ frappe.ui.form.on('Inventory Dimension', { } }, + document_type(frm) { + frm.trigger("set_parent_fields"); + }, + + set_parent_fields(frm) { + if (frm.doc.apply_to_all_doctypes) { + frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document); + } else if (frm.doc.document_type && frm.doc.istable) { + frappe.call({ + method: 'erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields', + args: { + child_doctype: frm.doc.document_type, + dimension_name: frm.doc.reference_document + }, + callback: (r) => { + if (r.message && r.message.length) { + frm.set_df_property("fetch_from_parent", "options", + [""].concat(r.message)); + } + } + }); + } + }, + delete_dimension(frm) { let msg = (` Custom fields related to this dimension will be deleted on deletion of dimension. diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 03e7fda8411..09f4f63031c 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -144,16 +144,15 @@ "fieldtype": "Column Break" }, { - "depends_on": "istable", "description": "Set fieldname or DocType name like Supplier, Customer etc.", "fieldname": "fetch_from_parent", - "fieldtype": "Data", + "fieldtype": "Select", "label": "Fetch Value From Parent Form" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-17 11:43:24.722441", + "modified": "2022-09-02 13:29:04.098469", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 4ff8f33b409..9e8c10b394d 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -236,3 +236,30 @@ def get_inventory_dimensions(): def delete_dimension(dimension): doc = frappe.get_doc("Inventory Dimension", dimension) doc.delete() + + +@frappe.whitelist() +def get_parent_fields(child_doctype, dimension_name): + parent_doctypes = frappe.get_all( + "DocField", fields=["parent"], filters={"options": child_doctype} + ) + + fields = [] + + fields.extend( + frappe.get_all( + "DocField", + fields=["fieldname as value", "label"], + filters={"options": dimension_name, "parent": ("in", [d.parent for d in parent_doctypes])}, + ) + ) + + fields.extend( + frappe.get_all( + "Custom Field", + fields=["fieldname as value", "label"], + filters={"options": dimension_name, "dt": ("in", [d.parent for d in parent_doctypes])}, + ) + ) + + return fields diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index cc90b74ee85..19ddc449f0e 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -2,14 +2,17 @@ # See license.txt import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.tests.utils import FrappeTestCase +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( CanNotBeChildDoc, CanNotBeDefaultDimension, DoNotChangeError, delete_dimension, ) +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.warehouse.test_warehouse import create_warehouse @@ -136,6 +139,58 @@ class TestInventoryDimension(FrappeTestCase): self.assertTrue(inv_dim1.has_stock_ledger()) self.assertRaises(DoNotChangeError, inv_dim1.save) + def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self): + create_inventory_dimension( + reference_document="Rack", + type_of_transaction="Both", + dimension_name="Rack", + apply_to_all_doctypes=1, + fetch_from_parent="Rack", + ) + + create_custom_field( + "Purchase Receipt", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack") + ) + + create_custom_field( + "Delivery Note", dict(fieldname="rack", label="Rack", fieldtype="Link", options="Rack") + ) + + frappe.reload_doc("stock", "doctype", "purchase_receipt_item") + frappe.reload_doc("stock", "doctype", "delivery_note_item") + + pr_doc = make_purchase_receipt(qty=2, do_not_submit=True) + pr_doc.rack = "Rack 1" + pr_doc.save() + pr_doc.submit() + + pr_doc.load_from_db() + + self.assertEqual(pr_doc.items[0].rack, "Rack 1") + sle_rack = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": pr_doc.items[0].name, "voucher_type": pr_doc.doctype}, + "rack", + ) + + self.assertEqual(sle_rack, "Rack 1") + + dn_doc = create_delivery_note(qty=2, do_not_submit=True) + dn_doc.rack = "Rack 1" + dn_doc.save() + dn_doc.submit() + + dn_doc.load_from_db() + + self.assertEqual(dn_doc.items[0].rack, "Rack 1") + sle_rack = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": dn_doc.items[0].name, "voucher_type": dn_doc.doctype}, + "rack", + ) + + self.assertEqual(sle_rack, "Rack 1") + def prepare_test_data(): if not frappe.db.exists("DocType", "Shelf"): @@ -160,6 +215,28 @@ def prepare_test_data(): create_warehouse("Shelf Warehouse") + if not frappe.db.exists("DocType", "Rack"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Rack", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:rack_name", + "fields": [{"label": "Rack Name", "fieldname": "rack_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for rack in ["Rack 1"]: + if not frappe.db.exists("Rack", rack): + frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True) + + create_warehouse("Rack Warehouse") + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 29b001fdcb3..7e1476d240a 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -562,7 +562,7 @@ $.extend(erpnext.item, { let selected_attributes = {}; me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { if(i===0) return; - let attribute_name = $(col).find('label').html().trim(); + let attribute_name = $(col).find('.control-label').html().trim(); selected_attributes[attribute_name] = []; let checked_opts = $(col).find('.checkbox input'); checked_opts.each((i, opt) => { diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 83177b372ad..8c6f6d85a46 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -48,41 +48,31 @@ "oldfieldtype": "Select", "options": "Item", "reqd": 1, - "search_index": 1, - "show_days": 1, - "show_seconds": 1 + "search_index": 1 }, { "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM", - "show_days": 1, - "show_seconds": 1 + "options": "UOM" }, { "default": "0", "description": "Quantity that must be bought or sold per UOM", "fieldname": "packing_unit", "fieldtype": "Int", - "label": "Packing Unit", - "show_days": 1, - "show_seconds": 1 + "label": "Packing Unit" }, { "fieldname": "column_break_17", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "item_name", "fieldtype": "Data", "in_list_view": 1, "label": "Item Name", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fetch_from": "item_code.brand", @@ -90,36 +80,29 @@ "fieldtype": "Read Only", "in_list_view": 1, "label": "Brand", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "item_description", "fieldtype": "Text", "label": "Item Description", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "price_list_details", "fieldtype": "Section Break", "label": "Price List", - "options": "fa fa-tags", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tags" }, { "fieldname": "price_list", "fieldtype": "Link", "in_global_search": 1, + "in_list_view": 1, "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "bold": 1, @@ -127,49 +110,37 @@ "fieldname": "customer", "fieldtype": "Link", "label": "Customer", - "options": "Customer", - "show_days": 1, - "show_seconds": 1 + "options": "Customer" }, { "depends_on": "eval:doc.buying == 1", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", - "options": "Supplier", - "show_days": 1, - "show_seconds": 1 + "options": "Supplier" }, { "fieldname": "column_break_3", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "default": "0", "fieldname": "buying", "fieldtype": "Check", "label": "Buying", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "default": "0", "fieldname": "selling", "fieldtype": "Check", "label": "Selling", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "item_details", "fieldtype": "Section Break", - "options": "fa fa-tag", - "show_days": 1, - "show_seconds": 1 + "options": "fa fa-tag" }, { "bold": 1, @@ -177,15 +148,11 @@ "fieldtype": "Link", "label": "Currency", "options": "Currency", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "col_br_1", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "price_list_rate", @@ -197,80 +164,61 @@ "oldfieldname": "ref_rate", "oldfieldtype": "Currency", "options": "currency", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "section_break_15", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "default": "Today", "fieldname": "valid_from", "fieldtype": "Date", - "label": "Valid From", - "show_days": 1, - "show_seconds": 1 + "label": "Valid From" }, { "default": "0", "fieldname": "lead_time_days", "fieldtype": "Int", - "label": "Lead Time in days", - "show_days": 1, - "show_seconds": 1 + "label": "Lead Time in days" }, { "fieldname": "column_break_18", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "valid_upto", "fieldtype": "Date", - "label": "Valid Upto", - "show_days": 1, - "show_seconds": 1 + "label": "Valid Upto" }, { "fieldname": "section_break_24", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "note", "fieldtype": "Text", - "label": "Note", - "show_days": 1, - "show_seconds": 1 + "label": "Note" }, { "fieldname": "reference", "fieldtype": "Data", "in_list_view": 1, - "label": "Reference", - "show_days": 1, - "show_seconds": 1 + "in_standard_filter": 1, + "label": "Reference" }, { "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", - "options": "Batch", - "show_days": 1, - "show_seconds": 1 + "options": "Batch" } ], "icon": "fa fa-flag", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-08 18:12:15.395772", + "modified": "2022-09-02 16:33:55.612992", "modified_by": "Administrator", "module": "Stock", "name": "Item Price", @@ -307,6 +255,7 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "title_field": "item_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item_price/item_price_list.js b/erpnext/stock/doctype/item_price/item_price_list.js new file mode 100644 index 00000000000..48158393f67 --- /dev/null +++ b/erpnext/stock/doctype/item_price/item_price_list.js @@ -0,0 +1,3 @@ +frappe.listview_settings['Item Price'] = { + hide_name_column: true, +}; diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 098242aed89..f4a943f88dc 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -187,22 +187,13 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(len(ste.items), len(rm_items)) def test_update_reserved_qty_for_subcontracting(self): - # Make stock available for raw materials - make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + # Create RM Material Receipt + make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=10, basic_rate=100) make_stock_entry( target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=30, - basic_rate=100, - ) - bin1 = frappe.db.get_value( + bin_before_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], @@ -222,102 +213,97 @@ class TestSubcontractingOrder(FrappeTestCase): ] sco = get_subcontracting_order(service_items=service_items) - bin2 = frappe.db.get_value( + bin_after_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1, ) - self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) - self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) - self.assertNotEqual(bin1.modified, bin2.modified) + # reserved_qty_for_sub_contract should be increased by 10 + self.assertEqual( + bin_after_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + 10 + ) - # Create stock transfer + # projected_qty should be decreased by 10 + self.assertEqual(bin_after_sco.projected_qty, bin_before_sco.projected_qty - 10) + + self.assertNotEqual(bin_before_sco.modified, bin_after_sco.modified) + + # Create Stock Entry(Send to Subcontractor) rm_items = [ { "item_code": "_Test FG Item", "rm_item_code": "_Test Item", "item_name": "_Test Item", - "qty": 6, + "qty": 10, "warehouse": "_Test Warehouse - _TC", "rate": 100, - "amount": 600, + "amount": 1000, "stock_uom": "Nos", - } + }, + { + "item_code": "_Test FG Item", + "rm_item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 20, + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 2000, + "stock_uom": "Nos", + }, ] ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) ste.to_warehouse = "_Test Warehouse 1 - _TC" ste.save() ste.submit() - bin3 = frappe.db.get_value( + bin_after_rm_transfer = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=40, - basic_rate=100, + # reserved_qty_for_sub_contract should be decreased by 10 + self.assertEqual( + bin_after_rm_transfer.reserved_qty_for_sub_contract, + bin_after_sco.reserved_qty_for_sub_contract - 10, ) - # Make SCR against the SCO - scr = make_subcontracting_receipt(sco.name) - scr.save() - scr.submit() - - bin4 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Cancel SCR - scr.reload() - scr.cancel() - bin5 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # Cancel Stock Entry + # Cancel Stock Entry(Send to Subcontractor) ste.cancel() - bin6 = frappe.db.get_value( + bin_after_cancel_ste = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + # reserved_qty_for_sub_contract should be increased by 10 + self.assertEqual( + bin_after_cancel_ste.reserved_qty_for_sub_contract, + bin_after_rm_transfer.reserved_qty_for_sub_contract + 10, + ) - # Cancel PO + # Cancel SCO sco.reload() sco.cancel() - bin7 = frappe.db.get_value( + bin_after_cancel_sco = frappe.db.get_value( "Bin", filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, fieldname="reserved_qty_for_sub_contract", as_dict=1, ) - self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + # reserved_qty_for_sub_contract should be decreased by 10 + self.assertEqual( + bin_after_cancel_sco.reserved_qty_for_sub_contract, + bin_after_cancel_ste.reserved_qty_for_sub_contract - 10, + ) + self.assertEqual( + bin_after_cancel_sco.reserved_qty_for_sub_contract, bin_before_sco.reserved_qty_for_sub_contract + ) def test_exploded_items(self): item_code = "_Test Subcontracted FG Item 11" diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 021d9aa8547..1da73405e8d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -75,6 +75,7 @@ class SubcontractingReceipt(SubcontractingController): self.get_current_stock() def on_submit(self): + self.validate_available_qty_for_consumption() self.update_status_updater_args() self.update_prevdoc_status() self.set_subcontracting_order_status() @@ -107,10 +108,42 @@ class SubcontractingReceipt(SubcontractingController): self.set_missing_values_in_supplied_items() self.set_missing_values_in_items() + def set_available_qty_for_consumption(self): + supplied_items_details = {} + + sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") + for item in self.get("items"): + supplied_items = ( + frappe.qb.from_(sco_supplied_item) + .select( + sco_supplied_item.rm_item_code, + sco_supplied_item.reference_name, + (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"), + ) + .where( + (sco_supplied_item.parent == item.subcontracting_order) + & (sco_supplied_item.main_item_code == item.item_code) + & (sco_supplied_item.reference_name == item.subcontracting_order_item) + ) + ).run(as_dict=True) + + if supplied_items: + supplied_items_details[item.name] = {} + + for supplied_item in supplied_items: + supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty + else: + for item in self.get("supplied_items"): + item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( + item.rm_item_code, 0 + ) + def set_missing_values_in_supplied_items(self): for item in self.get("supplied_items") or []: item.amount = item.rate * item.consumed_qty + self.set_available_qty_for_consumption() + def set_missing_values_in_items(self): rm_supp_cost = {} for item in self.get("supplied_items") or []: @@ -147,6 +180,17 @@ class SubcontractingReceipt(SubcontractingController): _("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code) ) + def validate_available_qty_for_consumption(self): + for item in self.get("supplied_items"): + if ( + item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty + ): + frappe.throw( + _( + "Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table." + ).format(item.idx) + ) + def set_items_cost_center(self): if self.company: cost_center = frappe.get_cached_value("Company", self.company, "cost_center") diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 763e76882e0..a47af52b337 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -70,6 +70,55 @@ class TestSubcontractingReceipt(FrappeTestCase): rm_supp_cost = sum(item.amount for item in scr.get("supplied_items")) self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) + def test_available_qty_for_consumption(self): + make_stock_entry( + 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, + ) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + { + "main_item_code": "_Test FG Item", + "item_code": "_Test Item", + "qty": 5.0, + "rate": 100.0, + "stock_uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "_Test FG Item", + "item_code": "_Test Item Home Desktop 100", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "_Test UOM", + "warehouse": "_Test Warehouse - _TC", + }, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + self.assertRaises(frappe.ValidationError, scr.submit) + def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json index 100a8060e8c..ddbb80661ad 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -19,6 +19,7 @@ "col_break2", "amount", "secbreak_2", + "available_qty_for_consumption", "required_qty", "col_break3", "consumed_qty", @@ -75,8 +76,7 @@ { "fieldname": "required_qty", "fieldtype": "Float", - "in_list_view": 1, - "label": "Available Qty For Consumption", + "label": "Required Qty", "print_hide": 1, "read_only": 1 }, @@ -85,7 +85,7 @@ "fieldname": "consumed_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Qty to be Consumed", + "label": "Consumed Qty", "reqd": 1 }, { @@ -179,12 +179,21 @@ "options": "Subcontracting Order", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "available_qty_for_consumption", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Available Qty For Consumption", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-04-18 10:45:16.538479", + "modified": "2022-09-02 22:28:53.392381", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Supplied Item", @@ -193,6 +202,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file