diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 81c2d8bb738..b528ee58e23 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -12,6 +12,7 @@ from frappe.utils import flt, get_link_to_form import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on +from erpnext.accounts.utils import get_currency_precision from erpnext.setup.utils import get_exchange_rate @@ -170,6 +171,15 @@ class ExchangeRateRevaluation(Document): .run(as_dict=True) ) + # round off balance based on currency precision + currency_precision = get_currency_precision() + for acc in account_details: + acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision) + acc.balance = flt(acc.balance, currency_precision) + acc.zero_balance = ( + True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False + ) + return account_details @staticmethod diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 866a94d04b1..46e2c50e40b 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -169,21 +169,18 @@ class PeriodClosingVoucher(AccountsController): return frappe.db.sql( """ select - t2.account_currency, + t1.account_currency, {dimension_fields}, sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, sum(t1.debit) - sum(t1.credit) as bal_in_company_currency - from `tabGL Entry` t1, `tabAccount` t2 + from `tabGL Entry` t1 where t1.is_cancelled = 0 - and t1.account = t2.name - and t2.report_type = 'Profit and Loss' - and t2.docstatus < 2 - and t2.company = %s + and t1.account in (select name from `tabAccount` where report_type = 'Profit and Loss' and docstatus < 2 and company = %s) and t1.posting_date between %s and %s group by {dimension_fields} """.format( - dimension_fields=", ".join(dimension_fields) + dimension_fields=", ".join(dimension_fields), ), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1, diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index a63039e0e3a..e8e80449292 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -469,7 +469,7 @@ "options": "UOM" }, { - "description": "If rate is zero them item will be treated as \"Free Item\"", + "description": "If rate is zero then item will be treated as \"Free Item\"", "fieldname": "free_item_rate", "fieldtype": "Currency", "label": "Free Item Rate" @@ -670,4 +670,4 @@ "sort_order": "DESC", "states": [], "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 946170340ee..25ea903dfec 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -647,12 +647,12 @@ def set_taxes( else: args.update(get_party_details(party, party_type)) - if party_type in ("Customer", "Lead"): + if party_type in ("Customer", "Lead", "Prospect"): args.update({"tax_type": "Sales"}) - if party_type == "Lead": + if party_type in ["Lead", "Prospect"]: args["customer"] = None - del args["lead"] + del args[frappe.scrub(party_type)] else: args.update({"tax_type": "Purchase"}) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2d56b597b0c..2bfb4105c1d 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from collections import OrderedDict import frappe from frappe import _, qb, scrub @@ -849,30 +850,30 @@ class GrossProfitGenerator(object): Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. """ - parents = [] + grouped = OrderedDict() for row in self.si_list: - if row.parent not in parents: - parents.append(row.parent) + # initialize list with a header row for each new parent + grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append( + row.update( + {"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code} + ) # descendant rows will have indent: 1.0 or greater + ) - parents_index = 0 - for index, row in enumerate(self.si_list): - if parents_index < len(parents) and row.parent == parents[parents_index]: - invoice = self.get_invoice_row(row) - self.si_list.insert(index, invoice) - parents_index += 1 + # if item is a bundle, add it's components as seperate rows + if frappe.db.exists("Product Bundle", row.item_code): + bundled_items = self.get_bundle_items(row) + for x in bundled_items: + bundle_item = self.get_bundle_item_row(row, x) + grouped.get(row.parent).append(bundle_item) - else: - # skipping the bundle items rows - if not row.indent: - row.indent = 1.0 - row.parent_invoice = row.parent - row.invoice_or_item = row.item_code + self.si_list.clear() - if frappe.db.exists("Product Bundle", row.item_code): - self.add_bundle_items(row, index) + for items in grouped.values(): + self.si_list.extend(items) def get_invoice_row(self, row): + # header row format return frappe._dict( { "parent_invoice": "", @@ -901,13 +902,6 @@ class GrossProfitGenerator(object): } ) - def add_bundle_items(self, product_bundle, index): - bundle_items = self.get_bundle_items(product_bundle) - - for i, item in enumerate(bundle_items): - bundle_item = self.get_bundle_item_row(product_bundle, item) - self.si_list.insert((index + i + 1), bundle_item) - def get_bundle_items(self, product_bundle): return frappe.get_all( "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index a4d2c82845a..2e1def98fc3 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -96,7 +96,6 @@ class AssetCategory(Document): frappe.throw(msg, title=_("Missing Account")) -@frappe.whitelist() def get_asset_category_account( fieldname, item=None, asset=None, account=None, asset_category=None, company=None ): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 78bb05671d8..10788c36eec 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -758,6 +758,7 @@ class AccountsController(TransactionBase): } ) + update_gl_dict_with_regional_fields(self, gl_dict) accounting_dimensions = get_accounting_dimensions() dimension_dict = frappe._dict() @@ -2835,3 +2836,8 @@ def validate_regional(doc): @erpnext.allow_regional def validate_einvoice_fields(doc): pass + + +@erpnext.allow_regional +def update_gl_dict_with_regional_fields(doc, gl_dict): + pass diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3d47ee797da..ef289ff6a67 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -43,7 +43,6 @@ class SellingController(StockController): self.validate_auto_repeat_subscription_dates() def set_missing_values(self, for_validate=False): - super(SellingController, self).set_missing_values(for_validate) # set contact and address details for customer, if they are not mentioned @@ -62,7 +61,7 @@ class SellingController(StockController): elif self.doctype == "Quotation" and self.party_name: if self.quotation_to == "Customer": customer = self.party_name - else: + elif self.quotation_to == "Lead": lead = self.party_name if customer: diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 4daa2edb28a..9cc6ec9d4b4 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -160,4 +160,3 @@ class TestLoanDisbursement(unittest.TestCase): interest = per_day_interest * 15 self.assertEqual(amounts["pending_principal_amount"], 1500000) - self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2)) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 15e9e3f4c1a..9d31ee195fc 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 @@ -22,7 +22,7 @@ class LoanInterestAccrual(AccountsController): frappe.throw(_("Interest Amount or Principal Amount is mandatory")) if not self.last_accrual_date: - self.last_accrual_date = get_last_accrual_date(self.loan) + self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date) def on_submit(self): self.make_gl_entries() @@ -271,14 +271,14 @@ def make_loan_interest_accrual_entry(args): def get_no_of_days_for_interest_accural(loan, posting_date): - last_interest_accrual_date = get_last_accrual_date(loan.name) + last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date) no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 return no_of_days -def get_last_accrual_date(loan): +def get_last_accrual_date(loan, posting_date): last_posting_date = frappe.db.sql( """ SELECT MAX(posting_date) from `tabLoan Interest Accrual` WHERE loan = %s and docstatus = 1""", @@ -286,12 +286,30 @@ def get_last_accrual_date(loan): ) if last_posting_date[0][0]: + last_interest_accrual_date = last_posting_date[0][0] # interest for last interest accrual date is already booked, so add 1 day - return add_days(last_posting_date[0][0], 1) + last_disbursement_date = get_last_disbursement_date(loan, posting_date) + + if last_disbursement_date and getdate(last_disbursement_date) > getdate( + last_interest_accrual_date + ): + last_interest_accrual_date = last_disbursement_date + + return add_days(last_interest_accrual_date, 1) else: return frappe.db.get_value("Loan", loan, "disbursement_date") +def get_last_disbursement_date(loan, posting_date): + last_disbursement_date = frappe.db.get_value( + "Loan Disbursement", + {"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)}, + "MAX(posting_date)", + ) + + return last_disbursement_date + + def days_in_year(year): days = 365 diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 017724da806..d7e11aafa81 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -101,7 +101,7 @@ class LoanRepayment(AccountsController): if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision): if not self.is_term_loan: # get last loan interest accrual date - last_accrual_date = get_last_accrual_date(self.against_loan) + last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date) # get posting date upto which interest has to be accrued per_day_interest = get_per_day_interest( @@ -722,7 +722,7 @@ def get_amounts(amounts, against_loan, posting_date): if due_date: pending_days = date_diff(posting_date, due_date) + 1 else: - last_accrual_date = get_last_accrual_date(against_loan_doc.name) + last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date) pending_days = date_diff(posting_date, last_accrual_date) + 1 if pending_days > 0: diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 22177f44148..a7f06486abc 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -7,13 +7,14 @@ from typing import Literal import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string -from frappe.utils.data import add_to_date, now +from frappe.utils.data import add_to_date, now, today from erpnext.manufacturing.doctype.job_card.job_card import ( JobCardOverTransferError, OperationMismatchError, OverlapError, make_corrective_job_card, + make_material_request, ) from erpnext.manufacturing.doctype.job_card.job_card import ( make_stock_entry as make_stock_entry_from_jc, @@ -449,6 +450,25 @@ class TestJobCard(FrappeTestCase): jc.docstatus = 2 assertStatus("Cancelled") + def test_job_card_material_request_and_bom_details(self): + from erpnext.stock.doctype.material_request.material_request import make_stock_entry + + create_bom_with_multiple_operations() + work_order = make_wo_with_transfer_against_jc() + + job_card_name = frappe.db.get_value("Job Card", {"work_order": work_order.name}, "name") + + mr = make_material_request(job_card_name) + mr.schedule_date = today() + mr.submit() + + ste = make_stock_entry(mr.name) + self.assertEqual(ste.purpose, "Material Transfer for Manufacture") + self.assertEqual(ste.work_order, work_order.name) + self.assertEqual(ste.job_card, job_card_name) + self.assertEqual(ste.from_bom, 1.0) + self.assertEqual(ste.bom_no, work_order.bom_no) + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js index 98d2bbc81a4..5ab8bae2e1d 100644 --- a/erpnext/projects/doctype/task/task_list.js +++ b/erpnext/projects/doctype/task/task_list.js @@ -25,20 +25,38 @@ frappe.listview_settings['Task'] = { } return [__(doc.status), colors[doc.status], "status,=," + doc.status]; }, - gantt_custom_popup_html: function(ganttobj, task) { - var html = `
${ganttobj.name}
`; + gantt_custom_popup_html: function (ganttobj, task) { + let html = ` + + ${ganttobj.name} + + `; - if(task.project) html += `

Project: ${task.project}

`; - html += `

Progress: ${ganttobj.progress}

`; + if (task.project) { + html += `

${__("Project")}: + + ${task.project} + +

`; + } + html += `

+ ${__("Progress")}: + ${ganttobj.progress}% +

`; - if(task._assign_list) { - html += task._assign_list.reduce( - (html, user) => html + frappe.avatar(user) - , ''); + if (task._assign) { + const assign_list = JSON.parse(task._assign); + const assignment_wrapper = ` + Assigned to: + + ${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")} + + `; + html += assignment_wrapper; } - return html; - } - + return `
${html}
`; + }, }; diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 644adff1e27..5c41aa06804 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -16,8 +16,8 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { let party_type = "Customer"; - if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") { - party_type = "Lead"; + if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) { + party_type = frm.doc.quotation_to; } args = { diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 83fa472d68b..2d5c3fa961d 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Quotation', { frm.set_query("quotation_to", function() { return{ "filters": { - "name": ["in", ["Customer", "Lead"]], + "name": ["in", ["Customer", "Lead", "Prospect"]], } } }); @@ -160,19 +160,16 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } set_dynamic_field_label(){ - if (this.frm.doc.quotation_to == "Customer") - { + if (this.frm.doc.quotation_to == "Customer") { this.frm.set_df_property("party_name", "label", "Customer"); this.frm.fields_dict.party_name.get_query = null; - } - - if (this.frm.doc.quotation_to == "Lead") - { + } else if (this.frm.doc.quotation_to == "Lead") { this.frm.set_df_property("party_name", "label", "Lead"); - this.frm.fields_dict.party_name.get_query = function() { return{ query: "erpnext.controllers.queries.lead_query" } } + } else if (this.frm.doc.quotation_to == "Prospect") { + this.frm.set_df_property("party_name", "label", "Prospect"); } } diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 06467e51a63..ceb990d61b2 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -398,10 +398,17 @@ class SalesOrder(SellingController): def update_picking_status(self): total_picked_qty = 0.0 total_qty = 0.0 + per_picked = 0.0 + for so_item in self.items: - total_picked_qty += flt(so_item.picked_qty) - total_qty += flt(so_item.stock_qty) - per_picked = total_picked_qty / total_qty * 100 + if cint( + frappe.get_cached_value("Item", so_item.item_code, "is_stock_item") + ) or self.has_product_bundle(so_item.item_code): + total_picked_qty += flt(so_item.picked_qty) + total_qty += flt(so_item.stock_qty) + + if total_picked_qty and total_qty: + per_picked = total_picked_qty / total_qty * 100 self.db_set("per_picked", flt(per_picked), update_modified=False) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 0d780c22817..cee7112c1b6 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -27,6 +27,7 @@ def after_install(): create_default_success_action() create_default_energy_point_rules() create_incoterms() + create_default_role_profiles() add_company_to_session_defaults() add_standard_navbar_items() add_app_name() @@ -211,3 +212,16 @@ def setup_log_settings(): def hide_workspaces(): for ws in ["Integration", "Settings"]: frappe.db.set_value("Workspace", ws, "public", 0) + + +def create_default_role_profiles(): + for module in ["Accounts", "Stock", "Manufacturing"]: + create_role_profile(module) + + +def create_role_profile(module): + role_profile = frappe.new_doc("Role Profile") + role_profile.role_profile = _("{0} User").format(module) + role_profile.append("roles", {"role": module + " User"}) + role_profile.append("roles", {"role": module + " Manager"}) + role_profile.insert() diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index ebd9a17c3bc..9576f409487 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -622,6 +622,16 @@ def make_stock_entry(source_name, target_doc=None): target.stock_entry_type = target.purpose target.set_job_card_data() + if source.job_card: + job_card_details = frappe.get_all( + "Job Card", filters={"name": source.job_card}, fields=["bom_no", "for_quantity"] + ) + + if job_card_details and job_card_details[0]: + target.bom_no = job_card_details[0].bom_no + target.fg_completed_qty = job_card_details[0].for_quantity + target.from_bom = 1 + doclist = get_mapped_doc( "Material Request", source_name, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index a9a9a1d6641..d3af6205083 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -265,6 +265,10 @@ class PickList(Document): for item in locations: if not item.item_code: frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) + if not cint( + frappe.get_cached_value("Item", item.item_code, "is_stock_item") + ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}): + continue item_code = item.item_code reference = item.sales_order_item or item.material_request_item key = (item_code, item.uom, item.warehouse, item.batch_no, reference) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 541d4d17e18..7d97f1667bb 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -304,6 +304,9 @@ def validate_serial_no(sle, item_det): _("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError ) + allow_existing_serial_no = cint( + frappe.get_cached_value("Stock Settings", "None", "allow_existing_serial_no") + ) for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): sr = frappe.db.get_value( @@ -332,6 +335,23 @@ def validate_serial_no(sle, item_det): SerialNoItemError, ) + if not allow_existing_serial_no and sle.voucher_type in [ + "Stock Entry", + "Purchase Receipt", + "Purchase Invoice", + ]: + msg = "" + + if sle.voucher_type == "Stock Entry": + se_purpose = frappe.db.get_value("Stock Entry", sle.voucher_no, "purpose") + if se_purpose in ["Manufacture", "Material Receipt"]: + msg = f"Cannot create a {sle.voucher_type} ({se_purpose}) for the Item {frappe.bold(sle.item_code)} with the existing Serial No {frappe.bold(serial_no)}." + else: + msg = f"Cannot create a {sle.voucher_type} for the Item {frappe.bold(sle.item_code)} with the existing Serial No {frappe.bold(serial_no)}." + + if msg: + frappe.throw(_(msg), SerialNoDuplicateError) + if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle): doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no)) frappe.throw( diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 2e5d2c3aaff..621b9df1245 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -751,6 +751,50 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) + def test_update_stock_reconciliation_while_reposting(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item().name + warehouse = "_Test Warehouse - _TC" + + # Stock Value => 100 * 100 = 10000 + se = make_stock_entry( + item_code=item_code, + target=warehouse, + qty=100, + basic_rate=100, + posting_time="10:00:00", + ) + + # Stock Value => 100 * 200 = 20000 + # Value Change => 20000 - 10000 = 10000 + sr1 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=100, + rate=200, + posting_time="12:00:00", + ) + self.assertEqual(sr1.difference_amount, 10000) + + # Stock Value => 50 * 50 = 2500 + # Value Change => 2500 - 10000 = -7500 + sr2 = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=50, + rate=50, + posting_time="11:00:00", + ) + self.assertEqual(sr2.difference_amount, -7500) + + sr1.load_from_db() + self.assertEqual(sr1.difference_amount, 17500) + + sr2.cancel() + sr1.load_from_db() + self.assertEqual(sr1.difference_amount, 10000) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index cb2bc0a7f51..af68e63f339 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -35,6 +35,7 @@ "section_break_7", "automatically_set_serial_nos_based_on_fifo", "set_qty_in_transactions_based_on_serial_no_input", + "allow_existing_serial_no", "column_break_10", "disable_serial_no_and_batch_selector", "use_naming_series", @@ -340,6 +341,12 @@ { "fieldname": "column_break_121", "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "allow_existing_serial_no", + "fieldtype": "Check", + "label": "Allow existing Serial No to be Manufactured/Received again" } ], "icon": "icon-cog", @@ -347,7 +354,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-05-29 15:09:54.959411", + "modified": "2023-05-31 14:15:14.145048", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js index 50a78a82588..254f5273be2 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.js @@ -53,11 +53,14 @@ frappe.query_reports["Stock and Account Value Comparison"] = {

Are you sure you want to create Reposting Entries?

`; + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map(i => frappe.query_report.data[i]); + + if (!selected_rows.length) { + frappe.throw(__("Please select rows to create Reposting Entries")); + } frappe.confirm(__(message), () => { - let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes.map(i => frappe.query_report.data[i]); - frappe.call({ method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries", args: { diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 2f64eddb300..272195e2a68 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -728,6 +728,8 @@ class update_entries_after(object): self.update_rate_on_purchase_receipt(sle, outgoing_rate) elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt": self.update_rate_on_subcontracting_receipt(sle, outgoing_rate) + elif sle.voucher_type == "Stock Reconciliation": + self.update_rate_on_stock_reconciliation(sle) def update_rate_on_stock_entry(self, sle, outgoing_rate): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) @@ -795,6 +797,38 @@ class update_entries_after(object): for d in scr.items: d.db_update() + def update_rate_on_stock_reconciliation(self, sle): + if not sle.serial_no and not sle.batch_no: + sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True) + + for item in sr.items: + # Skip for Serial and Batch Items + if item.serial_no or item.batch_no: + continue + + previous_sle = get_previous_sle( + { + "item_code": item.item_code, + "warehouse": item.warehouse, + "posting_date": sr.posting_date, + "posting_time": sr.posting_time, + "sle": sle.name, + } + ) + + item.current_qty = previous_sle.get("qty_after_transaction") or 0.0 + item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0 + item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate) + + item.amount = flt(item.qty) * flt(item.valuation_rate) + item.amount_difference = item.amount - item.current_amount + else: + sr.difference_amount = sum([item.amount_difference for item in sr.items]) + sr.db_update() + + for item in sr.items: + item.db_update() + def get_serialized_values(self, sle): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty)