diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 6ce79ee3ea6..2b98baf2210 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -111,8 +111,6 @@ class PaymentReconciliation(Document): def get_payment_entries(self): order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" condition = self.get_conditions(get_payments=True) - if self.payment_name: - condition += "name like '%%{0}%%'".format(self.payment_name) payment_entries = get_advance_payment_entries_for_regional( self.party_type, @@ -122,6 +120,7 @@ class PaymentReconciliation(Document): against_all_orders=True, limit=self.payment_limit, condition=condition, + payment_name=self.payment_name, ) return payment_entries diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index e711ae0de2b..bdbb1419784 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -15,6 +15,7 @@ "group_by", "cost_center", "territory", + "ignore_exchange_rate_revaluation_journals", "column_break_14", "to_date", "finance_book", @@ -374,10 +375,16 @@ "fieldname": "pdf_name", "fieldtype": "Data", "label": "PDF Name" + }, + { + "default": "0", + "fieldname": "ignore_exchange_rate_revaluation_journals", + "fieldtype": "Check", + "label": "Ignore Exchange Rate Revaluation Journals" } ], "links": [], - "modified": "2023-08-28 12:59:53.071334", + "modified": "2023-12-18 12:20:08.965120", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", 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 b7d6827f64c..d4b4b37b4ee 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 @@ -64,6 +64,18 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" + err_journals = None + if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals: + err_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": doc.company, + "docstatus": 1, + "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), + }, + as_list=True, + ) + for entry in doc.customers: if doc.include_ageing: ageing = set_ageing(doc, entry) @@ -76,6 +88,8 @@ def get_statement_dict(doc, get_statement_dict=False): ) filters = get_common_filters(doc) + if err_journals: + filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) if doc.report == "General Ledger": filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 434662c298b..3d18a860361 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2195,9 +2195,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def get_received_items(reference_name, doctype, reference_fieldname): + reference_field = "inter_company_invoice_reference" + if doctype == "Purchase Order": + reference_field = "inter_company_order_reference" + + filters = { + reference_field: reference_name, + "docstatus": 1, + } + target_doctypes = frappe.get_all( doctype, - filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, + filters=filters, as_list=True, ) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f1abc1d4ddb..e4d5938c0b5 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -244,8 +244,12 @@ class ReceivablePayableReport(object): row.invoiced_in_account_currency += amount_in_account_currency else: if self.is_invoice(ple): - row.credit_note -= amount - row.credit_note_in_account_currency -= amount_in_account_currency + if row.voucher_no == ple.voucher_no == ple.against_voucher_no: + row.paid -= amount + row.paid_in_account_currency -= amount_in_account_currency + else: + row.credit_note -= amount + row.credit_note_in_account_currency -= amount_in_account_currency else: row.paid -= amount row.paid_in_account_currency -= amount_in_account_currency diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index f83285a1a72..77f8c6eaaa9 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): return credit_note + def test_pos_receivable(self): + filters = { + "company": self.company, + "party_type": "Customer", + "party": [self.customer], + "report_date": add_days(today(), 2), + "based_on_payment_terms": 0, + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_remarks": False, + } + + pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + pos_inv.posting_date = add_days(today(), 2) + pos_inv.is_pos = 1 + pos_inv.append( + "payments", + frappe._dict( + mode_of_payment="Cash", + amount=flt(pos_inv.grand_total / 2), + ), + ) + pos_inv.disable_rounded_total = 1 + pos_inv.save() + pos_inv.submit() + + report = execute(filters) + expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]] + + row = report[1][-1] + self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note]) + pos_inv.cancel() + def test_accounts_receivable(self): filters = { "company": self.company, diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 759bb71ab24..95397452b01 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -231,6 +231,9 @@ def get_conditions(filters): if filters.get("voucher_no"): conditions.append("voucher_no=%(voucher_no)s") + if filters.get("voucher_no_not_in"): + conditions.append("voucher_no not in %(voucher_no_not_in)s") + if filters.get("group_by") == "Group by Party" and not filters.get("party_type"): conditions.append("party_type in ('Customer', 'Supplier')") diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c98b8193538..52b0c34673a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -626,8 +626,10 @@ def update_reference_in_payment_entry( "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), - "exchange_gain_loss": d.exchange_gain_loss, + "exchange_rate": d.exchange_rate + if d.difference_amount is not None + else payment_entry.get_exchange_rate(), + "exchange_gain_loss": d.difference_amount, } if d.voucher_detail_no: diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 6b39982bb81..8226aa32c0e 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -80,6 +80,15 @@ class RequestforQuotation(BuyingController): supplier.quote_status = "Pending" self.send_to_supplier() + def before_print(self, settings=None): + """Use the first suppliers data to render the print preview.""" + if self.vendor or not self.suppliers: + # If a specific supplier is already set, via Tools > Download PDF, + # we don't want to override it. + return + + self.update_supplier_part_no(self.suppliers[0].supplier) + def on_cancel(self): self.db_set("status", "Cancelled") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 393ad171d52..47aba077b49 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -165,6 +165,7 @@ class AccountsController(TransactionBase): self.disable_pricing_rule_on_internal_transfer() self.disable_tax_included_prices_for_internal_transfer() self.set_incoming_rate() + self.init_internal_values() if self.meta.get_field("currency"): self.calculate_taxes_and_totals() @@ -224,6 +225,16 @@ class AccountsController(TransactionBase): self.set_total_in_words() + def init_internal_values(self): + # init all the internal values as 0 on sa + if self.docstatus.is_draft(): + # TODO: Add all such pending values here + fields = ["billed_amt", "delivered_qty"] + for item in self.get("items"): + for field in fields: + if hasattr(item, field): + item.set(field, 0) + def before_cancel(self): validate_einvoice_fields(self) @@ -629,6 +640,7 @@ class AccountsController(TransactionBase): args["doctype"] = self.doctype args["name"] = self.name + args["child_doctype"] = item.doctype args["child_docname"] = item.name args["ignore_pricing_rule"] = ( self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 @@ -2504,6 +2516,7 @@ def get_advance_payment_entries( against_all_orders=False, limit=None, condition=None, + payment_name=None, ): party_account_field = "paid_from" if party_type == "Customer" else "paid_to" currency_field = ( @@ -2526,6 +2539,10 @@ def get_advance_payment_entries( reference_condition = "" order_list = [] + payment_name_filter = "" + if payment_name: + payment_name_filter = " and t1.name like '%%{0}%%'".format(payment_name) + if not condition: condition = "" @@ -2540,7 +2557,7 @@ def get_advance_payment_entries( where t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 - and t2.reference_doctype = %s {2} {3} + and t2.reference_doctype = %s {2} {3} {6} order by t1.posting_date {4} """.format( currency_field, @@ -2549,12 +2566,17 @@ def get_advance_payment_entries( condition, limit_cond, exchange_rate_field, + payment_name_filter, ), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1, ) if include_unallocated: + payment_name_filter = "" + if payment_name: + payment_name_filter = " and name like '%%{0}%%'".format(payment_name) + unallocated_payment_entries = frappe.db.sql( """ select 'Payment Entry' as reference_type, name as reference_name, posting_date, @@ -2562,10 +2584,15 @@ def get_advance_payment_entries( from `tabPayment Entry` where {0} = %s and party_type = %s and party = %s and payment_type = %s - and docstatus = 1 and unallocated_amount > 0 {condition} + and docstatus = 1 and unallocated_amount > 0 {condition} {4} order by posting_date {1} """.format( - party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or "" + party_account_field, + limit_cond, + exchange_rate_field, + currency_field, + payment_name_filter, + condition=condition or "", ), (party_account, party_type, party, payment_type), as_dict=1, diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 81736915f55..8e9b2289079 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -354,7 +354,11 @@ class BuyingController(SubcontractingController): rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) else: - field = "incoming_rate" if self.get("is_internal_supplier") else "rate" + field = ( + "incoming_rate" + if self.get("is_internal_supplier") and not self.doctype == "Purchase Order" + else "rate" + ) rate = flt( frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) * (d.conversion_factor or 1), diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index e68ee909d9f..1ba10259a8e 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -52,10 +52,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part copy_attributes_to_variant(template, variant) - variant.manufacturer = manufacturer - variant.manufacturer_part_no = manufacturer_part_no - variant.item_code = append_number_if_name_exists("Item", template.name) + variant.flags.ignore_mandatory = True + variant.save() + + if not frappe.db.exists( + "Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer} + ): + manufacturer_doc = frappe.new_doc("Item Manufacturer") + manufacturer_doc.update( + { + "item_code": variant.name, + "manufacturer": manufacturer, + "manufacturer_part_no": manufacturer_part_no, + } + ) + + manufacturer_doc.flags.ignore_mandatory = True + manufacturer_doc.save(ignore_permissions=True) return variant diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 7cef623148e..bac94f98af4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor -from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.utils import get_incoming_rate, get_valuation_method class SellingController(StockController): @@ -422,11 +422,13 @@ class SellingController(StockController): items = self.get("items") + (self.get("packed_items") or []) for d in items: - if not self.get("return_against"): + if not self.get("return_against") or ( + get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + ): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty")) - if not (self.get("is_return") and d.incoming_rate): + if not d.incoming_rate: d.incoming_rate = get_incoming_rate( { "item_code": d.item_code, diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 8f8a086d99e..b7292a77f7d 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -514,7 +514,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2022-10-13 12:42:04.277879", + "modified": "2023-12-01 18:46:49.468526", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -576,10 +576,11 @@ ], "search_fields": "lead_name,lead_owner,status", "sender_field": "email_id", + "sender_name_field": "lead_name", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], "subject_field": "title", "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 2d5b3573ae4..cbef699ce61 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -14,6 +14,7 @@ from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_ema from erpnext.accounts.party import set_taxes from erpnext.controllers.selling_controller import SellingController from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events +from erpnext.selling.doctype.customer.customer import parse_full_name class Lead(SellingController, CRMNote): @@ -48,6 +49,10 @@ class Lead(SellingController, CRMNote): return self.contact_doc = self.create_contact() + # leads created by email inbox only have the full name set + if self.lead_name and not any([self.first_name, self.middle_name, self.last_name]): + self.first_name, self.middle_name, self.last_name = parse_full_name(self.lead_name) + def after_insert(self): self.link_to_contact() diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 7778f060146..b546594a4ed 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -508,6 +508,7 @@ class ProductionPlan(Document): if close: self.db_set("status", "Closed") + self.update_bin_qty() return if self.total_produced_qty > 0: @@ -522,6 +523,9 @@ class ProductionPlan(Document): if close is not None: self.db_set("status", self.status) + if self.docstatus == 1 and self.status != "Completed": + self.update_bin_qty() + def update_ordered_status(self): update_status = False for d in self.po_items: diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 6a50a10d07a..d546306f080 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1447,6 +1447,47 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(row.get("uom"), "Nos") self.assertEqual(row.get("conversion_factor"), 10.0) + def test_unreserve_qty_on_closing_of_pp(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.stock.utils import get_or_make_bin + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + + store_warehouse = create_warehouse("Store Warehouse", company="_Test Company") + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1 + ) + + pln.for_warehouse = rm_warehouse + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + pln.append("mr_items", d) + + pln.save() + pln.submit() + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln.reload() + pln.set_status(close=True) + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertAlmostEqual(after_qty, before_qty - 10) + + pln.reload() + pln.set_status(close=False) + + bin_name = get_or_make_bin(rm_item, rm_warehouse) + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + self.assertAlmostEqual(after_qty, before_qty) + def create_production_plan(**args): """ diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 279610e56c6..d96f45fd676 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -354,3 +354,4 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency" erpnext.patches.v14_0.clear_reconciliation_values_from_singles # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger +erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 050b9dcd3db..0f291e1eba7 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -482,6 +482,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe cost_center: item.cost_center, tax_category: me.frm.doc.tax_category, item_tax_template: item.item_tax_template, + child_doctype: item.doctype, child_docname: item.name, is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow, } diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index eafc1ed70e6..4c7b2534fcc 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -971,7 +971,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) { } function get_time_left(timestamp, agreement_status) { - const diff = moment(timestamp).diff(moment()); + const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true)); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed'; let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green'; return {'diff_display': diff_display, 'indicator': indicator}; diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index de3c21e6eb8..4bbce9b2f00 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -307,18 +307,19 @@ class Customer(TransactionBase): def create_contact(contact, party_type, party, email): """Create contact based on given contact name""" - contact = contact.split(" ") - - contact = frappe.get_doc( + first, middle, last = parse_full_name(contact) + doc = frappe.get_doc( { "doctype": "Contact", - "first_name": contact[0], - "last_name": len(contact) > 1 and contact[1] or "", + "first_name": first, + "middle_name": middle, + "last_name": last, + "is_primary_contact": 1, } ) - contact.append("email_ids", dict(email_id=email, is_primary=1)) - contact.append("links", dict(link_doctype=party_type, link_name=party)) - contact.insert() + doc.append("email_ids", dict(email_id=email, is_primary=1)) + doc.append("links", dict(link_doctype=party_type, link_name=party)) + return doc.insert() @frappe.whitelist() @@ -684,24 +685,42 @@ def get_credit_limit(customer, company): 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")}], - } - ) + values = { + "doctype": "Contact", + "is_primary_contact": is_primary_contact, + "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], + } + if args.customer_type == "Individual": + first, middle, last = parse_full_name(args.get("customer_name")) + values.update( + { + "first_name": first, + "middle_name": middle, + "last_name": last, + } + ) + else: + values.update( + { + "company_name": args.get("customer_name"), + } + ) + contact = frappe.get_doc(values) + 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() + + if flags := args.get("flags"): + contact.insert(ignore_permissions=flags.get("ignore_permissions")) + else: + contact.insert() return contact -def make_address(args, is_primary_address=1): +def make_address(args, is_primary_address=1, is_shipping_address=1): reqd_fields = [] for field in ["city", "country"]: if not args.get(field): @@ -717,16 +736,23 @@ def make_address(args, is_primary_address=1): address = frappe.get_doc( { "doctype": "Address", - "address_title": args.get("name"), + "address_title": args.get("customer_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"), + "is_primary_address": is_primary_address, + "is_shipping_address": is_shipping_address, "links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}], } - ).insert() + ) + + if flags := args.get("flags"): + address.insert(ignore_permissions=flags.get("ignore_permissions")) + else: + address.insert() return address @@ -747,3 +773,13 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) .run() ) + + +def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]: + """Parse full name into first name, middle name and last name""" + names = full_name.split() + first_name = names[0] + middle_name = " ".join(names[1:-1]) if len(names) > 2 else None + last_name = names[-1] if len(names) > 1 else None + + return first_name, middle_name, last_name diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index a621c737ed3..7a601a78876 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -10,7 +10,11 @@ from frappe.utils import flt from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen -from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding +from erpnext.selling.doctype.customer.customer import ( + get_credit_limit, + get_customer_outstanding, + parse_full_name, +) from erpnext.tests.utils import create_test_contact_and_address test_ignore = ["Price List"] @@ -373,6 +377,22 @@ class TestCustomer(FrappeTestCase): frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name") + def test_parse_full_name(self): + first, middle, last = parse_full_name("John") + self.assertEqual(first, "John") + self.assertEqual(middle, None) + self.assertEqual(last, None) + + first, middle, last = parse_full_name("John Doe") + self.assertEqual(first, "John") + self.assertEqual(middle, None) + self.assertEqual(last, "Doe") + + first, middle, last = parse_full_name("John Michael Doe") + self.assertEqual(first, "John") + self.assertEqual(middle, "Michael") + self.assertEqual(last, "Doe") + def get_customer_dict(customer_name): return { diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index da7edbf8144..5a60d2ff967 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -1,5 +1,5 @@ import frappe -from frappe.utils import cint +from frappe.utils.deprecations import deprecated def get_leaderboards(): @@ -54,12 +54,13 @@ def get_leaderboards(): @frappe.whitelist() def get_all_customers(date_range, company, field, limit=None): + filters = [["docstatus", "=", "1"], ["company", "=", company]] + from_date, to_date = parse_date_range(date_range) if field == "outstanding_amount": - 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( + if from_date and to_date: + filters.append(["posting_date", "between", [from_date, to_date]]) + + return frappe.get_list( "Sales Invoice", fields=["customer as name", "sum(outstanding_amount) as value"], filters=filters, @@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None): ) else: if field == "total_sales_amount": - select_field = "sum(so_item.base_net_amount)" + select_field = "base_net_total" elif field == "total_qty_sold": - select_field = "sum(so_item.stock_qty)" + select_field = "total_qty" - date_condition = get_date_condition(date_range, "so.transaction_date") + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - 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 - where so.docstatus = 1 {1} and so.company = %s - group by so.customer - order by value DESC - limit %s - """.format( - select_field, date_condition - ), - (company, cint(limit)), - as_dict=1, + return frappe.get_list( + "Sales Order", + fields=["customer as name", f"sum({select_field}) as value"], + filters=filters, + group_by="customer", + order_by="value desc", + limit=limit, ) @@ -96,55 +91,58 @@ def get_all_customers(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( + results = 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, ) + readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name")) + return [item for item in results if item["name"] in readable_active_items] else: if field == "total_sales_amount": - select_field = "sum(order_item.base_net_amount)" + select_field = "base_net_amount" select_doctype = "Sales Order" elif field == "total_purchase_amount": - select_field = "sum(order_item.base_net_amount)" + select_field = "base_net_amount" select_doctype = "Purchase Order" elif field == "total_qty_sold": - select_field = "sum(order_item.stock_qty)" + select_field = "stock_qty" select_doctype = "Sales Order" elif field == "total_qty_purchased": - select_field = "sum(order_item.stock_qty)" + select_field = "stock_qty" select_doctype = "Purchase Order" - date_condition = get_date_condition(date_range, "sales_order.transaction_date") + filters = [["docstatus", "=", "1"], ["company", "=", company]] + from_date, to_date = parse_date_range(date_range) + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - 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 - where sales_order.docstatus = 1 - and sales_order.company = %s {2} - 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 + child_doctype = f"{select_doctype} Item" + return frappe.get_list( + select_doctype, + fields=[ + f"`tab{child_doctype}`.item_code as name", + f"sum(`tab{child_doctype}`.{select_field}) as value", + ], + filters=filters, + order_by="value desc", + group_by=f"`tab{child_doctype}`.item_code", + limit=limit, + ) @frappe.whitelist() def get_all_suppliers(date_range, company, field, limit=None): + filters = [["docstatus", "=", "1"], ["company", "=", company]] + from_date, to_date = parse_date_range(date_range) + if field == "outstanding_amount": - 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( + if from_date and to_date: + filters.append(["posting_date", "between", [from_date, to_date]]) + + return frappe.get_list( "Purchase Invoice", fields=["supplier as name", "sum(outstanding_amount) as value"], filters=filters, @@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None): ) else: if field == "total_purchase_amount": - select_field = "sum(purchase_order_item.base_net_amount)" + select_field = "base_net_total" elif field == "total_qty_purchased": - select_field = "sum(purchase_order_item.stock_qty)" + select_field = "total_qty" - date_condition = get_date_condition(date_range, "purchase_order.modified") + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - 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 - where - purchase_order.docstatus = 1 - {1} - 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 + return frappe.get_list( + "Purchase Order", + fields=["supplier as name", f"sum({select_field}) as value"], + filters=filters, + group_by="supplier", + order_by="value desc", + limit=limit, + ) @frappe.whitelist() def get_all_sales_partner(date_range, company, field, limit=None): if field == "total_sales_amount": - select_field = "sum(`base_net_total`)" + select_field = "base_net_total" elif field == "total_commission": - select_field = "sum(`total_commission`)" + select_field = "total_commission" - 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 = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]] + from_date, to_date = parse_date_range(date_range) + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) return frappe.get_list( "Sales Order", fields=[ - "`sales_partner` as name", - "{} as value".format(select_field), + "sales_partner as name", + f"sum({select_field}) as value", ], filters=filters, group_by="sales_partner", @@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None): @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") + filters = [ + ["docstatus", "=", "1"], + ["company", "=", company], + ["Sales Team", "sales_person", "is", "set"], + ] + from_date, to_date = parse_date_range(date_range) + if from_date and to_date: + filters.append(["transaction_date", "between", [from_date, to_date]]) - 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' - where sales_order.docstatus = 1 - and sales_order.company = %s - {date_condition} - group by sales_team.sales_person - order by value DESC - limit %s - """.format( - date_condition=date_condition - ), - (company, cint(limit)), - as_dict=1, + return frappe.get_list( + "Sales Order", + fields=[ + "`tabSales Team`.sales_person as name", + "sum(`tabSales Team`.allocated_amount) as value", + ], + filters=filters, + group_by="`tabSales Team`.sales_person", + order_by="value desc", + limit=limit, ) +@deprecated def get_date_condition(date_range, field): date_condition = "" if date_range: @@ -236,3 +228,11 @@ def get_date_condition(date_range, field): field, frappe.db.escape(from_date), frappe.db.escape(to_date) ) return date_condition + + +def parse_date_range(date_range): + if date_range: + date_range = frappe.parse_json(date_range) + return date_range[0], date_range[1] + + return None, None diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 11f2cafc35d..5731bda495e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -301,7 +301,8 @@ "no_copy": 1, "options": "Delivery Note", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "collapsible": 1, @@ -1400,7 +1401,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-09-04 14:15:28.363184", + "modified": "2023-12-18 17:19:39.368239", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index d9d9a52482a..4216ca0cdc3 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -997,7 +997,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): ) 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/patches/__init__.py b/erpnext/stock/doctype/delivery_note/patches/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py new file mode 100644 index 00000000000..8fe4ffb58f1 --- /dev/null +++ b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py @@ -0,0 +1,15 @@ +import frappe + + +def execute(): + """Drop unused return_against index""" + + try: + frappe.db.sql_ddl( + "ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`" + ) + frappe.db.sql_ddl( + "ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`" + ) + except Exception: + frappe.log_error("Failed to drop unused index") diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 65828f3a4ac..476b4959813 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1332,6 +1332,56 @@ class TestDeliveryNote(FrappeTestCase): dn.reload() self.assertFalse(dn.items[0].target_warehouse) + def test_sales_return_valuation_for_moving_average(self): + item_code = make_item( + "_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"} + ).name + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=100.0, + posting_date=add_days(nowdate(), -5), + ) + dn = create_delivery_note( + item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4) + ) + self.assertEqual(dn.items[0].incoming_rate, 100.0) + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=200.0, + posting_date=add_days(nowdate(), -3), + ) + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=300.0, + posting_date=add_days(nowdate(), -2), + ) + + dn1 = create_delivery_note( + is_return=1, + item_code=item_code, + return_against=dn.name, + qty=-5, + rate=500, + company=dn.company, + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + do_not_submit=1, + posting_date=add_days(nowdate(), -1), + ) + + # (300 * 5) + (200 * 5) = 2500 + # 2500 / 10 = 250 + + self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 9aa66a9a1ec..7c665b973d8 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -522,39 +522,25 @@ class TestItem(FrappeTestCase): self.assertEqual(factor, 1.0) def test_item_variant_by_manufacturer(self): - fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}] - set_item_variant_settings(fields) + template = make_item( + "_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"} + ).name - 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") + for manufacturer in ["DFSS", "DASA", "ASAAS"]: + if not frappe.db.exists("Manufacturer", manufacturer): + m_doc = frappe.new_doc("Manufacturer") + m_doc.short_name = manufacturer + m_doc.insert() - template = frappe.get_doc( - dict( - doctype="Item", - item_code="_Test Variant Mfg", - has_variant=1, - item_group="Products", - variant_based_on="Manufacturer", - ) - ).insert() + self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"})) + variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123") - manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert() + item_manufacturer = frappe.db.exists( + "Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name} + ) + self.assertTrue(item_manufacturer) - 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") - 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") + frappe.delete_doc("Item Manufacturer", item_manufacturer) def test_stock_exists_against_template_item(self): stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index ec075bb6bad..5922af25879 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -199,9 +199,8 @@ frappe.ui.form.on('Material Request', { get_item_data: function(frm, item, overwrite_warehouse=false) { if (item && !item.item_code) { return; } - frm.call({ + frappe.call({ method: "erpnext.stock.get_item_details.get_item_details", - child: item, args: { args: { item_code: item.item_code, diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 659bc42f0a6..b84ccf770b2 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -123,7 +123,9 @@ class MaterialRequest(BuyingController): def on_submit(self): self.update_requested_qty_in_production_plan() self.update_requested_qty() - if self.material_request_type == "Purchase": + if self.material_request_type == "Purchase" and frappe.db.exists( + "Budget", {"applicable_on_material_request": 1, "docstatus": 1} + ): self.validate_budget() def before_save(self): diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index e89e22ab336..3c5384d4a88 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc from frappe.query_builder import Case from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum -from frappe.utils import cint, floor, flt, today +from frappe.utils import ceil, cint, floor, flt, today from frappe.utils.nestedset import get_descendants_of from erpnext.selling.doctype.sales_order.sales_order import ( @@ -605,7 +605,7 @@ def get_available_item_locations_for_serialized_item( .select(sn.name, sn.warehouse) .where((sn.item_code == item_code) & (sn.company == company)) .orderby(sn.purchase_date) - .limit(cint(required_qty + total_picked_qty)) + .limit(ceil(required_qty + total_picked_qty)) ) if from_warehouses: @@ -647,7 +647,7 @@ def get_available_item_locations_for_batched_item( .groupby(sle.warehouse, sle.batch_no, sle.item_code) .having(Sum(sle.actual_qty) > 0) .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse) - .limit(cint(required_qty + total_picked_qty)) + .limit(ceil(required_qty + total_picked_qty)) ) if from_warehouses: @@ -680,7 +680,7 @@ def get_available_item_locations_for_serial_and_batched_item( (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) ) .orderby(sn.purchase_date) - .limit(cint(location.qty + total_picked_qty)) + .limit(ceil(location.qty + total_picked_qty)) ).run(as_dict=True) serial_nos = [sn.name for sn in serial_nos] @@ -699,7 +699,7 @@ def get_available_item_locations_for_other_item( .select(bin.warehouse, bin.actual_qty.as_("qty")) .where((bin.item_code == item_code) & (bin.actual_qty > 0)) .orderby(bin.creation) - .limit(cint(required_qty + total_picked_qty)) + .limit(ceil(required_qty + total_picked_qty)) ) if from_warehouses: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index aae1bad0977..de1263d8f66 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -288,7 +288,8 @@ "no_copy": 1, "options": "Purchase Receipt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "section_addresses", @@ -1241,7 +1242,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2023-10-01 21:00:44.556816", + "modified": "2023-12-18 17:26:41.279663", "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 c12048077e7..f35dc136990 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1199,10 +1199,6 @@ def get_item_account_wise_additional_cost(purchase_document): return item_account_wise_cost -def on_doctype_update(): - frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"]) - - @erpnext.allow_regional def update_regional_gl_entries(gl_list, doc): return diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e47971d03e8..10d3ef4b7a9 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -8,6 +8,7 @@ import frappe from frappe import _, throw from frappe.model import child_table_fields, default_fields from frappe.model.meta import get_field_precision +from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, getdate @@ -608,6 +609,9 @@ 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 + if args.get("child_doctype") and item_tax_template: + out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template)) + def _get_item_tax_template(args, taxes, out=None, for_validate=False): if out is None: diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 80bf8508cf3..7a5a8615d0c 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -397,7 +397,7 @@ class StockBalanceReport(object): "fieldname": "bal_val", "fieldtype": "Currency", "width": 100, - "options": "currency", + "options": "Company:company:default_currency", }, { "label": _("Opening Qty"), @@ -411,7 +411,7 @@ class StockBalanceReport(object): "fieldname": "opening_val", "fieldtype": "Currency", "width": 110, - "options": "currency", + "options": "Company:company:default_currency", }, { "label": _("In Qty"), diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index c0064996099..4ed8a0eab16 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created +from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin @@ -234,6 +235,9 @@ class SubcontractingOrder(SubcontractingController): "Subcontracting Order", self.name, "status", status, update_modified=update_modified ) + if status == "Closed": + update_po_status("Closed", self.purchase_order) + @frappe.whitelist() def make_subcontracting_receipt(source_name, target_doc=None): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 7b679d907e8..b84cbac4843 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -111,13 +111,13 @@ class SubcontractingReceipt(SubcontractingController): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.update_status_updater_args() self.update_prevdoc_status() - self.delete_auto_created_batches() self.set_consumed_qty_in_subcontract_order() self.set_subcontracting_order_status() self.update_stock_ledger() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() self.update_status() + self.delete_auto_created_batches() @frappe.whitelist() def set_missing_values(self): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 6c962531dfa..b05ed755c7f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -655,6 +655,79 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(rm_item.rate, 100) self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate) + def test_subcontracting_receipt_cancel_with_batch(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + # Step - 1: Set Backflush Based On as "BOM" + set_backflush_based_on("BOM") + + # Step - 2: Create FG and RM Items + fg_item = make_item( + properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1} + ).name + rm_item1 = make_item(properties={"is_stock_item": 1}).name + rm_item2 = make_item(properties={"is_stock_item": 1}).name + make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0}) + + # Step - 3: Create BOM for FG Item + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]) + for rm_item in bom.items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + bom = bom.name + + # Step - 4: Create PO and SCO + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item Test For Batch 1", + "qty": 100, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 100, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + for rm_item in sco.supplied_items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + + # Step - 5: Inward Raw Materials + rm_items = get_rm_items(sco.supplied_items) + for rm_item in rm_items: + rm_item["rate"] = 100 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + # Step - 6: Transfer RM's to Subcontractor + se = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + for item in se.items: + self.assertEqual(item.qty, 100) + self.assertEqual(item.basic_rate, 100) + self.assertEqual(item.amount, item.qty * item.basic_rate) + + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "item": fg_item, + "batch_id": frappe.generate_hash(length=10), + } + ).insert(ignore_permissions=True) + + # Step - 7: Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.items[0].batch_no = batch_doc.batch_id + scr.save() + scr.submit() + scr.load_from_db() + + # Step - 8: Cancel Subcontracting Receipt + scr.cancel() + self.assertTrue(scr.docstatus == 2) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index f96823b2908..9f91dc1726d 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", { frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", { reason: values.reason, - user: frappe.session.user_email + user: frappe.session.user_email, + doctype: frm.doc.doctype, + docname: frm.doc.name, }, () => { reset_sla.enable_primary_action(); frm.refresh(); diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 2a023e09c49..54d3c31cd80 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -739,10 +739,12 @@ def get_response_and_resolution_duration(doc): return priority -def reset_service_level_agreement(doc, reason, user): +@frappe.whitelist() +def reset_service_level_agreement(doctype: str, docname: str, 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.")) + doc = frappe.get_doc(doctype, docname) frappe.get_doc( { "doctype": "Comment",