mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-25 16:04:46 +00:00
Merge pull request #49566 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -7,6 +7,9 @@ from frappe.utils import nowdate
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||||
|
|
||||||
|
IBAN_1 = "DE02000000003716541159"
|
||||||
|
IBAN_2 = "DE02500105170137075030"
|
||||||
|
|
||||||
|
|
||||||
class TestAutoMatchParty(FrappeTestCase):
|
class TestAutoMatchParty(FrappeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -22,24 +25,24 @@ class TestAutoMatchParty(FrappeTestCase):
|
|||||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||||
|
|
||||||
def test_match_by_account_number(self):
|
def test_match_by_account_number(self):
|
||||||
create_supplier_for_match(account_no="000000003716541159")
|
create_supplier_for_match(account_no=IBAN_1[11:])
|
||||||
doc = create_bank_transaction(
|
doc = create_bank_transaction(
|
||||||
withdrawal=1200,
|
withdrawal=1200,
|
||||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||||
account_no="000000003716541159",
|
account_no=IBAN_1[11:],
|
||||||
iban="DE02000000003716541159",
|
iban=IBAN_1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(doc.party_type, "Supplier")
|
self.assertEqual(doc.party_type, "Supplier")
|
||||||
self.assertEqual(doc.party, "John Doe & Co.")
|
self.assertEqual(doc.party, "John Doe & Co.")
|
||||||
|
|
||||||
def test_match_by_iban(self):
|
def test_match_by_iban(self):
|
||||||
create_supplier_for_match(iban="DE02000000003716541159")
|
create_supplier_for_match(iban=IBAN_1)
|
||||||
doc = create_bank_transaction(
|
doc = create_bank_transaction(
|
||||||
withdrawal=1200,
|
withdrawal=1200,
|
||||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||||
account_no="000000003716541159",
|
account_no=IBAN_1[11:],
|
||||||
iban="DE02000000003716541159",
|
iban=IBAN_1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(doc.party_type, "Supplier")
|
self.assertEqual(doc.party_type, "Supplier")
|
||||||
@@ -51,7 +54,7 @@ class TestAutoMatchParty(FrappeTestCase):
|
|||||||
withdrawal=1200,
|
withdrawal=1200,
|
||||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||||
party_name="Ella Jackson",
|
party_name="Ella Jackson",
|
||||||
iban="DE04000000003716545346",
|
iban=IBAN_2,
|
||||||
)
|
)
|
||||||
self.assertEqual(doc.party_type, "Supplier")
|
self.assertEqual(doc.party_type, "Supplier")
|
||||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||||
|
|||||||
@@ -74,6 +74,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
|
|||||||
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
|
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
|
||||||
party_link.secondary_party = secondary_party
|
party_link.secondary_party = secondary_party
|
||||||
|
|
||||||
party_link.save(ignore_permissions=True)
|
party_link.save()
|
||||||
|
|
||||||
return party_link
|
return party_link
|
||||||
|
|||||||
@@ -281,6 +281,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "customer.tax_id",
|
||||||
"fieldname": "tax_id",
|
"fieldname": "tax_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@@ -2198,7 +2199,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-08-04 19:20:28.732039",
|
"modified": "2025-09-09 14:48:59.472826",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -369,6 +369,10 @@ class AssetDepreciationSchedule(Document):
|
|||||||
original_schedule_date=schedule_date,
|
original_schedule_date=schedule_date,
|
||||||
)
|
)
|
||||||
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
|
||||||
|
|
||||||
|
if depreciation_amount > row.value_after_depreciation - row.expected_value_after_useful_life:
|
||||||
|
depreciation_amount = row.value_after_depreciation - row.expected_value_after_useful_life
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
if depreciation_amount > 0:
|
||||||
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
|
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
|
||||||
|
|
||||||
@@ -654,6 +658,7 @@ def _get_pro_rata_amt(
|
|||||||
total_days = get_total_days(original_schedule_date or to_date, 12)
|
total_days = get_total_days(original_schedule_date or to_date, 12)
|
||||||
else:
|
else:
|
||||||
total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
|
total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
|
||||||
|
|
||||||
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class AssetMovement(Document):
|
|||||||
if d.source_location:
|
if d.source_location:
|
||||||
if current_location != d.source_location:
|
if current_location != d.source_location:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
_("Asset {0} does not belong to the location {1}").format(d.asset, d.source_location)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
d.source_location = current_location
|
d.source_location = current_location
|
||||||
@@ -75,11 +75,11 @@ class AssetMovement(Document):
|
|||||||
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
||||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_employee(self, d):
|
def validate_employee(self, d):
|
||||||
if self.purpose == "Tranfer and Issue":
|
if self.purpose == "Transfer and Issue":
|
||||||
if not d.from_employee:
|
if not d.from_employee:
|
||||||
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class AssetMovement(Document):
|
|||||||
|
|
||||||
if current_custodian != d.from_employee:
|
if current_custodian != d.from_employee:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
_("Asset {0} does not belong to the custodian {1}").format(d.asset, d.from_employee)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not d.to_employee:
|
if not d.to_employee:
|
||||||
@@ -96,7 +96,7 @@ class AssetMovement(Document):
|
|||||||
|
|
||||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
|||||||
@@ -133,7 +133,10 @@ frappe.ui.form.on("Supplier", {
|
|||||||
__("Actions")
|
__("Actions")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
|
if (
|
||||||
|
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||||
|
frappe.model.can_create("Party Link")
|
||||||
|
) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Link with Customer"),
|
__("Link with Customer"),
|
||||||
function () {
|
function () {
|
||||||
|
|||||||
@@ -957,7 +957,9 @@ class StockController(AccountsController):
|
|||||||
from erpnext.stock.stock_ledger import make_sl_entries
|
from erpnext.stock.stock_ledger import make_sl_entries
|
||||||
|
|
||||||
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
||||||
update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
|
update_batch_qty(
|
||||||
|
self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher
|
||||||
|
)
|
||||||
|
|
||||||
def make_gl_entries_on_cancel(self, from_repost=False):
|
def make_gl_entries_on_cancel(self, from_repost=False):
|
||||||
if not from_repost:
|
if not from_repost:
|
||||||
|
|||||||
@@ -116,20 +116,6 @@ frappe.ui.form.on("Work Order", {
|
|||||||
frm.set_indicator_formatter("operation", function (doc) {
|
frm.set_indicator_formatter("operation", function (doc) {
|
||||||
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
|
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
|
||||||
});
|
});
|
||||||
|
|
||||||
if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
|
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
|
|
||||||
args: {
|
|
||||||
bom_no: frm.doc.bom_no,
|
|
||||||
},
|
|
||||||
callback: function (r) {
|
|
||||||
if (r.message["set_scrap_wh_mandatory"]) {
|
|
||||||
frm.toggle_reqd("scrap_warehouse", true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
@@ -144,6 +130,20 @@ frappe.ui.form.on("Work Order", {
|
|||||||
});
|
});
|
||||||
erpnext.work_order.set_default_warehouse(frm);
|
erpnext.work_order.set_default_warehouse(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
|
||||||
|
args: {
|
||||||
|
bom_no: frm.doc.bom_no,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message["set_scrap_wh_mandatory"]) {
|
||||||
|
frm.toggle_reqd("scrap_warehouse", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
source_warehouse: function (frm) {
|
source_warehouse: function (frm) {
|
||||||
|
|||||||
@@ -175,7 +175,10 @@ frappe.ui.form.on("Customer", {
|
|||||||
__("Actions")
|
__("Actions")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
|
if (
|
||||||
|
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||||
|
frappe.model.can_create("Party Link")
|
||||||
|
) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Link with Supplier"),
|
__("Link with Supplier"),
|
||||||
function () {
|
function () {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
field_map = {
|
field_map = {
|
||||||
"Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
|
"Contact": ["name", "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
|
||||||
"Address": [
|
"Address": [
|
||||||
|
"name",
|
||||||
"address_line1",
|
"address_line1",
|
||||||
"address_line2",
|
"address_line2",
|
||||||
"city",
|
"city",
|
||||||
@@ -29,6 +31,12 @@ def get_columns(filters):
|
|||||||
return [
|
return [
|
||||||
f"{party_type}:Link/{party_type}",
|
f"{party_type}:Link/{party_type}",
|
||||||
f"{frappe.unscrub(str(party_type_value))}::150",
|
f"{frappe.unscrub(str(party_type_value))}::150",
|
||||||
|
{
|
||||||
|
"label": _("Address"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Address",
|
||||||
|
"hidden": 1,
|
||||||
|
},
|
||||||
"Address Line 1",
|
"Address Line 1",
|
||||||
"Address Line 2",
|
"Address Line 2",
|
||||||
"Postal Code",
|
"Postal Code",
|
||||||
@@ -36,6 +44,7 @@ def get_columns(filters):
|
|||||||
"State",
|
"State",
|
||||||
"Country",
|
"Country",
|
||||||
"Is Primary Address:Check",
|
"Is Primary Address:Check",
|
||||||
|
{"label": _("Contact"), "fieldtype": "Link", "options": "Contact", "hidden": 1},
|
||||||
"First Name",
|
"First Name",
|
||||||
"Last Name",
|
"Last Name",
|
||||||
"Phone",
|
"Phone",
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ def create_demo_company():
|
|||||||
frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name)
|
frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name)
|
||||||
frappe.db.set_default("company", new_company.name)
|
frappe.db.set_default("company", new_company.name)
|
||||||
|
|
||||||
bank_account = create_bank_account({"company_name": new_company.name})
|
bank_account = create_bank_account({"company_name": new_company.name}, demo=True)
|
||||||
frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name)
|
frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name)
|
||||||
|
|
||||||
return new_company.name
|
return new_company.name
|
||||||
|
|||||||
@@ -513,9 +513,11 @@ def update_stock_settings():
|
|||||||
stock_settings.save()
|
stock_settings.save()
|
||||||
|
|
||||||
|
|
||||||
def create_bank_account(args):
|
def create_bank_account(args, demo=False):
|
||||||
if not args.get("bank_account"):
|
if not args.get("bank_account"):
|
||||||
args["bank_account"] = _("Bank Account")
|
if not demo:
|
||||||
|
return
|
||||||
|
args["bank_account"] = _("Demo Bank Account")
|
||||||
|
|
||||||
company_name = args.get("company_name")
|
company_name = args.get("company_name")
|
||||||
bank_account_group = frappe.db.get_value(
|
bank_account_group = frappe.db.get_value(
|
||||||
|
|||||||
@@ -108,7 +108,8 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Voucher Type",
|
"label": "Voucher Type",
|
||||||
"options": "DocType",
|
"options": "DocType",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "voucher_no",
|
"fieldname": "voucher_no",
|
||||||
@@ -195,8 +196,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Type of Transaction",
|
"label": "Type of Transaction",
|
||||||
"options": "\nInward\nOutward\nMaintenance\nAsset Repair",
|
"options": "\nInward\nOutward\nMaintenance\nAsset Repair",
|
||||||
"reqd": 1,
|
"reqd": 1
|
||||||
"search_index": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-17 18:22:36.056205",
|
"modified": "2025-09-15 14:37:26.441742",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Serial and Batch Bundle",
|
"name": "Serial and Batch Bundle",
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ class SerialandBatchBundle(Document):
|
|||||||
self.allow_existing_serial_nos()
|
self.allow_existing_serial_nos()
|
||||||
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
|
if not self.flags.ignore_validate_serial_batch or frappe.flags.in_test:
|
||||||
self.validate_serial_nos_duplicate()
|
self.validate_serial_nos_duplicate()
|
||||||
self.check_future_entries_exists()
|
|
||||||
|
|
||||||
|
self.check_future_entries_exists()
|
||||||
self.set_is_outward()
|
self.set_is_outward()
|
||||||
self.calculate_total_qty()
|
self.calculate_total_qty()
|
||||||
self.set_warehouse()
|
self.set_warehouse()
|
||||||
@@ -228,7 +228,7 @@ class SerialandBatchBundle(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.voucher_type == "Stock Reconciliation":
|
if self.voucher_type == "Stock Reconciliation":
|
||||||
serial_nos = self.get_serial_nos_for_validate()
|
serial_nos, batches = self.get_serial_nos_for_validate()
|
||||||
else:
|
else:
|
||||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
|
|
||||||
@@ -716,15 +716,22 @@ class SerialandBatchBundle(Document):
|
|||||||
if self.flags and self.flags.via_landed_cost_voucher:
|
if self.flags and self.flags.via_landed_cost_voucher:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.has_serial_no:
|
serial_nos = []
|
||||||
return
|
batches = []
|
||||||
|
|
||||||
if self.voucher_type == "Stock Reconciliation":
|
if self.voucher_type == "Stock Reconciliation":
|
||||||
serial_nos = self.get_serial_nos_for_validate(is_cancelled=is_cancelled)
|
serial_nos, batches = self.get_serial_nos_for_validate(is_cancelled=is_cancelled)
|
||||||
else:
|
else:
|
||||||
|
batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.voucher_type != "Stock Reconciliation"
|
||||||
|
and not self.flags.ignore_validate_serial_batch
|
||||||
|
and self.has_serial_no
|
||||||
|
):
|
||||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
|
|
||||||
if not serial_nos:
|
if self.has_batch_no and not self.has_serial_no and not batches:
|
||||||
return
|
return
|
||||||
|
|
||||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||||
@@ -740,65 +747,117 @@ class SerialandBatchBundle(Document):
|
|||||||
.on(parent.name == child.parent)
|
.on(parent.name == child.parent)
|
||||||
.select(
|
.select(
|
||||||
child.serial_no,
|
child.serial_no,
|
||||||
|
child.batch_no,
|
||||||
parent.voucher_type,
|
parent.voucher_type,
|
||||||
parent.voucher_no,
|
parent.voucher_no,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(child.serial_no.isin(serial_nos))
|
(child.parent != self.name)
|
||||||
& (child.parent != self.name)
|
|
||||||
& (parent.item_code == self.item_code)
|
& (parent.item_code == self.item_code)
|
||||||
& (parent.docstatus == 1)
|
& (parent.docstatus == 1)
|
||||||
& (parent.is_cancelled == 0)
|
& (parent.is_cancelled == 0)
|
||||||
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
|
& (parent.type_of_transaction.isin(["Inward", "Outward"]))
|
||||||
)
|
)
|
||||||
.where(timestamp_condition)
|
.where(timestamp_condition)
|
||||||
).run(as_dict=True)
|
)
|
||||||
|
|
||||||
|
if self.has_batch_no and not self.has_serial_no:
|
||||||
|
future_entries = future_entries.where(parent.voucher_type == "Stock Reconciliation")
|
||||||
|
|
||||||
|
if serial_nos:
|
||||||
|
future_entries = future_entries.where(
|
||||||
|
(child.serial_no.isin(serial_nos))
|
||||||
|
| ((parent.warehouse == self.warehouse) & (parent.voucher_type == "Stock Reconciliation"))
|
||||||
|
)
|
||||||
|
elif self.has_serial_no:
|
||||||
|
future_entries = future_entries.where(
|
||||||
|
(parent.warehouse == self.warehouse) & (parent.voucher_type == "Stock Reconciliation")
|
||||||
|
)
|
||||||
|
elif batches:
|
||||||
|
future_entries = future_entries.where(child.batch_no.isin(batches))
|
||||||
|
|
||||||
|
future_entries = future_entries.run(as_dict=True)
|
||||||
|
|
||||||
if future_entries:
|
if future_entries:
|
||||||
msg = """The serial nos has been used in the future
|
if self.has_serial_no:
|
||||||
transactions so you need to cancel them first.
|
title = "Serial No Exists In Future Transaction(s)"
|
||||||
The list of serial nos and their respective
|
else:
|
||||||
transactions are as below."""
|
title = "Batches Exists In Future Transaction(s)"
|
||||||
|
|
||||||
|
msg = """Since the stock reconciliation exists
|
||||||
|
for future dates, cancel it first. For Serial/Batch,
|
||||||
|
if you want to make a backdated transaction,
|
||||||
|
avoid using stock reconciliation.
|
||||||
|
For more details about the transaction,
|
||||||
|
please refer to the list below.
|
||||||
|
"""
|
||||||
|
|
||||||
msg += "<br><br><ul>"
|
msg += "<br><br><ul>"
|
||||||
|
|
||||||
for d in future_entries:
|
for d in future_entries:
|
||||||
msg += f"<li>{d.serial_no} in {get_link_to_form(d.voucher_type, d.voucher_no)}</li>"
|
if self.has_serial_no:
|
||||||
|
msg += f"<li>{d.serial_no} in {get_link_to_form(d.voucher_type, d.voucher_no)}</li>"
|
||||||
|
else:
|
||||||
|
msg += f"<li>{d.batch_no} in {get_link_to_form(d.voucher_type, d.voucher_no)}</li>"
|
||||||
msg += "</li></ul>"
|
msg += "</li></ul>"
|
||||||
|
|
||||||
title = "Serial No Exists In Future Transaction(s)"
|
|
||||||
|
|
||||||
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError)
|
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError)
|
||||||
|
|
||||||
def get_serial_nos_for_validate(self, is_cancelled=False):
|
def get_serial_nos_for_validate(self, is_cancelled=False):
|
||||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||||
skip_serial_nos = self.get_skip_serial_nos_for_stock_reconciliation(is_cancelled=is_cancelled)
|
batches = [d.batch_no for d in self.entries if d.batch_no]
|
||||||
serial_nos = list(set(sorted(serial_nos)) - set(sorted(skip_serial_nos)))
|
|
||||||
|
|
||||||
return serial_nos
|
skip_serial_nos, skip_batches = self.get_skip_serial_nos_for_stock_reconciliation(
|
||||||
|
is_cancelled=is_cancelled
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_nos = list(set(sorted(serial_nos)) - set(sorted(skip_serial_nos)))
|
||||||
|
batch_nos = list(set(sorted(batches)) - set(sorted(skip_batches)))
|
||||||
|
|
||||||
|
return serial_nos, batch_nos
|
||||||
|
|
||||||
def get_skip_serial_nos_for_stock_reconciliation(self, is_cancelled=False):
|
def get_skip_serial_nos_for_stock_reconciliation(self, is_cancelled=False):
|
||||||
data = get_stock_reco_details(self.voucher_detail_no)
|
data = get_stock_reco_details(self.voucher_detail_no)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
|
current_serial_nos = set()
|
||||||
|
serial_nos = set()
|
||||||
|
current_batches = set()
|
||||||
|
batches = set()
|
||||||
|
|
||||||
if data.current_serial_no:
|
if data.current_serial_no:
|
||||||
current_serial_nos = set(parse_serial_nos(data.current_serial_no))
|
current_serial_nos = set(parse_serial_nos(data.current_serial_no))
|
||||||
serial_nos = set(parse_serial_nos(data.serial_no)) if data.serial_no else set([])
|
serial_nos = set(parse_serial_nos(data.serial_no)) if data.serial_no else set([])
|
||||||
return list(serial_nos.intersection(current_serial_nos))
|
return list(serial_nos.intersection(current_serial_nos)), []
|
||||||
|
|
||||||
|
elif data.batch_no and data.current_qty == data.qty:
|
||||||
|
return [], [data.batch_no]
|
||||||
|
|
||||||
elif data.current_serial_and_batch_bundle:
|
elif data.current_serial_and_batch_bundle:
|
||||||
current_serial_nos = set(get_serial_nos_from_bundle(data.current_serial_and_batch_bundle))
|
if self.has_serial_no:
|
||||||
|
current_serial_nos = set(get_serial_nos_from_bundle(data.current_serial_and_batch_bundle))
|
||||||
|
else:
|
||||||
|
current_batches = set(get_batches_from_bundle(data.current_serial_and_batch_bundle))
|
||||||
|
|
||||||
if is_cancelled:
|
if is_cancelled:
|
||||||
return current_serial_nos
|
return list(current_serial_nos), list(current_batches)
|
||||||
|
|
||||||
serial_nos = (
|
if self.has_serial_no:
|
||||||
set(get_serial_nos_from_bundle(data.serial_and_batch_bundle))
|
serial_nos = (
|
||||||
if data.serial_and_batch_bundle
|
set(get_serial_nos_from_bundle(data.serial_and_batch_bundle))
|
||||||
else set([])
|
if data.serial_and_batch_bundle
|
||||||
|
else set([])
|
||||||
|
)
|
||||||
|
elif self.has_batch_no and data.serial_and_batch_bundle:
|
||||||
|
batches = set(get_batches_from_bundle(data.serial_and_batch_bundle))
|
||||||
|
|
||||||
|
return list(serial_nos.intersection(current_serial_nos)), list(
|
||||||
|
batches.intersection(current_batches)
|
||||||
)
|
)
|
||||||
return list(serial_nos.intersection(current_serial_nos))
|
|
||||||
|
|
||||||
return []
|
return [], []
|
||||||
|
|
||||||
def reset_qty(self, row, qty_field=None):
|
def reset_qty(self, row, qty_field=None):
|
||||||
qty_field = self.get_qty_field(row, qty_field=qty_field)
|
qty_field = self.get_qty_field(row, qty_field=qty_field)
|
||||||
@@ -2673,6 +2732,14 @@ def get_stock_reco_details(voucher_detail_no):
|
|||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Stock Reconciliation Item",
|
"Stock Reconciliation Item",
|
||||||
voucher_detail_no,
|
voucher_detail_no,
|
||||||
["current_serial_no", "serial_no", "serial_and_batch_bundle", "current_serial_and_batch_bundle"],
|
[
|
||||||
|
"current_serial_no",
|
||||||
|
"serial_no",
|
||||||
|
"serial_and_batch_bundle",
|
||||||
|
"current_serial_and_batch_bundle",
|
||||||
|
"batch_no",
|
||||||
|
"qty",
|
||||||
|
"current_qty",
|
||||||
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -222,7 +222,12 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
|||||||
).insert(ignore_permissions=True)
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
self.assertTrue(batch_doc.use_batchwise_valuation)
|
self.assertTrue(batch_doc.use_batchwise_valuation)
|
||||||
batch_doc.db_set("use_batchwise_valuation", 0)
|
batch_doc.db_set(
|
||||||
|
{
|
||||||
|
"use_batchwise_valuation": 0,
|
||||||
|
"batch_qty": 30,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
stock_queue = []
|
stock_queue = []
|
||||||
qty_after_transaction = 0
|
qty_after_transaction = 0
|
||||||
|
|||||||
@@ -962,6 +962,7 @@ frappe.ui.form.on("Stock Entry Detail", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.events.set_basic_rate(frm, cdt, cdn);
|
||||||
validate_sample_quantity(frm, cdt, cdn);
|
validate_sample_quantity(frm, cdt, cdn);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2123,14 +2123,16 @@ class StockEntry(StockController):
|
|||||||
"Work Order", self.work_order, "allow_alternative_item"
|
"Work Order", self.work_order, "allow_alternative_item"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
skip_transfer, from_wip_warehouse = frappe.get_value(
|
||||||
|
"Work Order", self.work_order, ["skip_transfer", "from_wip_warehouse"]
|
||||||
|
)
|
||||||
item.from_warehouse = (
|
item.from_warehouse = (
|
||||||
frappe.get_value(
|
frappe.get_value(
|
||||||
"Work Order Item",
|
"Work Order Item",
|
||||||
{"parent": self.work_order, "item_code": item.item_code},
|
{"parent": self.work_order, "item_code": item.item_code},
|
||||||
"source_warehouse",
|
"source_warehouse",
|
||||||
)
|
)
|
||||||
if frappe.get_value("Work Order", self.work_order, "skip_transfer")
|
if skip_transfer and not from_wip_warehouse
|
||||||
and not frappe.get_value("Work Order", self.work_order, "from_wip_warehouse")
|
|
||||||
else self.from_warehouse or item.source_warehouse or item.default_warehouse
|
else self.from_warehouse or item.source_warehouse or item.default_warehouse
|
||||||
)
|
)
|
||||||
if item.item_code in used_alternative_items:
|
if item.item_code in used_alternative_items:
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
# Removed 50 Qty, Balace Qty 50
|
# Removed 50 Qty, Balace Qty 50
|
||||||
se2 = make_stock_entry(
|
make_stock_entry(
|
||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
batch_no=batch_no,
|
batch_no=batch_no,
|
||||||
posting_time="10:00:00",
|
posting_time="10:00:00",
|
||||||
@@ -729,33 +729,13 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
batch_no=batch_no,
|
batch_no=batch_no,
|
||||||
posting_time="12:00:00",
|
posting_time="12:00:00",
|
||||||
source=warehouse,
|
source=warehouse,
|
||||||
qty=50,
|
qty=52,
|
||||||
basic_rate=700,
|
basic_rate=700,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
|
self.assertFalse(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
|
||||||
|
|
||||||
# Cancel the backdated Stock Entry se2,
|
self.assertRaises(frappe.ValidationError, stock_reco.cancel)
|
||||||
# Since Stock Reco entry in the future the Balace Qty should remain as it's (50)
|
|
||||||
|
|
||||||
se2.cancel()
|
|
||||||
|
|
||||||
sle = frappe.get_all(
|
|
||||||
"Stock Ledger Entry",
|
|
||||||
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
|
|
||||||
fields=["qty_after_transaction", "actual_qty", "voucher_type", "voucher_no"],
|
|
||||||
order_by="posting_time desc, creation desc",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0))
|
|
||||||
|
|
||||||
sle = frappe.get_all(
|
|
||||||
"Stock Ledger Entry",
|
|
||||||
filters={"is_cancelled": 0, "voucher_no": stock_reco.name, "actual_qty": ("<", 0)},
|
|
||||||
fields=["actual_qty"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
|
|
||||||
|
|
||||||
def test_update_stock_reconciliation_while_reposting(self):
|
def test_update_stock_reconciliation_while_reposting(self):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
@@ -905,27 +885,16 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||||
|
|
||||||
# Step - 3: Create a Purchase Receipt before the first Purchase Receipt
|
# Step - 3: Create a Purchase Receipt before the first Purchase Receipt
|
||||||
make_purchase_receipt(
|
pr = make_purchase_receipt(
|
||||||
item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=10,
|
||||||
|
rate=200,
|
||||||
|
posting_date=add_days(nowdate(), -5),
|
||||||
|
do_not_submit=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = frappe.get_all(
|
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||||
"Stock Ledger Entry",
|
|
||||||
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
|
||||||
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
|
||||||
order_by="creation",
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in data:
|
|
||||||
if d.actual_qty < 0:
|
|
||||||
self.assertEqual(d.actual_qty, -20.0)
|
|
||||||
self.assertAlmostEqual(d.stock_value_difference, -3000.0)
|
|
||||||
else:
|
|
||||||
self.assertEqual(d.actual_qty, 5.0)
|
|
||||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
|
||||||
|
|
||||||
active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code})
|
|
||||||
self.assertEqual(len(active_serial_no), 5)
|
|
||||||
|
|
||||||
def test_balance_qty_for_batch_with_backdated_stock_reco_and_future_entries(self):
|
def test_balance_qty_for_batch_with_backdated_stock_reco_and_future_entries(self):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
@@ -1463,6 +1432,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
sr = create_stock_reconciliation(
|
sr = create_stock_reconciliation(
|
||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
|
posting_date=add_days(nowdate(), -2),
|
||||||
warehouse=warehouse,
|
warehouse=warehouse,
|
||||||
qty=10,
|
qty=10,
|
||||||
rate=100,
|
rate=100,
|
||||||
@@ -1482,9 +1452,9 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertTrue(len(stock_ledgers) == 1)
|
self.assertTrue(len(stock_ledgers) == 1)
|
||||||
|
|
||||||
make_stock_entry(
|
se = make_stock_entry(
|
||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
target=warehouse,
|
source=warehouse,
|
||||||
qty=10,
|
qty=10,
|
||||||
basic_rate=100,
|
basic_rate=100,
|
||||||
use_serial_batch_fields=1,
|
use_serial_batch_fields=1,
|
||||||
@@ -1496,23 +1466,19 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
item_code=item_code,
|
item_code=item_code,
|
||||||
warehouse=warehouse,
|
warehouse=warehouse,
|
||||||
qty=10,
|
qty=10,
|
||||||
rate=100,
|
rate=200,
|
||||||
use_serial_batch_fields=1,
|
use_serial_batch_fields=1,
|
||||||
batch_no=batch_no,
|
batch_no=batch_no,
|
||||||
posting_date=add_days(nowdate(), -1),
|
posting_date=add_days(nowdate(), -1),
|
||||||
)
|
)
|
||||||
|
|
||||||
stock_ledgers = frappe.get_all(
|
stock_ledger = frappe.get_all(
|
||||||
"Stock Ledger Entry",
|
"Stock Ledger Entry",
|
||||||
filters={"voucher_no": sr.name, "is_cancelled": 0},
|
filters={"voucher_no": se.name, "is_cancelled": 0},
|
||||||
pluck="name",
|
fields=["stock_value_difference"],
|
||||||
)
|
)
|
||||||
|
|
||||||
sr.reload()
|
self.assertEqual(stock_ledger[0].stock_value_difference, 2000.0 * -1)
|
||||||
self.assertEqual(sr.items[0].current_qty, 10)
|
|
||||||
self.assertEqual(sr.items[0].current_valuation_rate, 100)
|
|
||||||
|
|
||||||
self.assertTrue(len(stock_ledgers) == 2)
|
|
||||||
|
|
||||||
def test_serial_no_backdated_stock_reco(self):
|
def test_serial_no_backdated_stock_reco(self):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
@@ -1564,7 +1530,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertTrue(status == "Active")
|
self.assertTrue(status == "Active")
|
||||||
|
|
||||||
make_stock_entry(
|
se = make_stock_entry(
|
||||||
item_code=serial_item,
|
item_code=serial_item,
|
||||||
source=warehouse,
|
source=warehouse,
|
||||||
qty=1,
|
qty=1,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ def execute(filters=None):
|
|||||||
batch_balance_dict[sle.batch_no] = [0, 0]
|
batch_balance_dict[sle.batch_no] = [0, 0]
|
||||||
|
|
||||||
batch_balance_dict[sle.batch_no][0] += sle.actual_qty
|
batch_balance_dict[sle.batch_no][0] += sle.actual_qty
|
||||||
|
batch_balance_dict[sle.batch_no][1] += stock_value
|
||||||
|
|
||||||
if filters.get("segregate_serial_batch_bundle"):
|
if filters.get("segregate_serial_batch_bundle"):
|
||||||
actual_qty = batch_balance_dict[sle.batch_no][0]
|
actual_qty = batch_balance_dict[sle.batch_no][0]
|
||||||
|
|||||||
@@ -307,6 +307,18 @@ class SerialBatchBundle:
|
|||||||
if docstatus == 0:
|
if docstatus == 0:
|
||||||
self.submit_serial_and_batch_bundle()
|
self.submit_serial_and_batch_bundle()
|
||||||
|
|
||||||
|
if (
|
||||||
|
frappe.db.count(
|
||||||
|
"Serial and Batch Entry", {"parent": self.sle.serial_and_batch_bundle, "docstatus": 0}
|
||||||
|
)
|
||||||
|
> 0
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Serial and Batch Bundle {0} is not submitted").format(
|
||||||
|
bold(self.sle.serial_and_batch_bundle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if self.item_details.has_serial_no == 1:
|
if self.item_details.has_serial_no == 1:
|
||||||
self.set_warehouse_and_status_in_serial_nos()
|
self.set_warehouse_and_status_in_serial_nos()
|
||||||
|
|
||||||
@@ -1326,40 +1338,40 @@ def get_serial_nos_batch(serial_nos):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_batch_qty(voucher_type, voucher_no, via_landed_cost_voucher=False):
|
def update_batch_qty(voucher_type, voucher_no, docstatus, via_landed_cost_voucher=False):
|
||||||
from erpnext.stock.doctype.batch.batch import get_available_batches
|
batches = get_batchwise_qty(voucher_type, voucher_no)
|
||||||
|
|
||||||
batches = get_distinct_batches(voucher_type, voucher_no)
|
|
||||||
if not batches:
|
if not batches:
|
||||||
return
|
return
|
||||||
|
|
||||||
precision = frappe.get_precision("Batch", "batch_qty")
|
precision = frappe.get_precision("Batch", "batch_qty")
|
||||||
batch_data = get_available_batches(
|
for batch, qty in batches.items():
|
||||||
frappe._dict({"batch_no": batches, "consider_negative_batches": 1, "based_on_warehouse": True})
|
current_qty = get_batch_current_qty(batch)
|
||||||
)
|
current_qty += flt(qty, precision) * (-1 if docstatus == 2 else 1)
|
||||||
batchwise_qty = defaultdict(float)
|
|
||||||
|
|
||||||
for (batch_no, warehouse), qty in batch_data.items():
|
if not via_landed_cost_voucher and current_qty < 0:
|
||||||
if not via_landed_cost_voucher and flt(qty, precision) < 0:
|
throw_negative_batch_validation(batch, current_qty)
|
||||||
throw_negative_batch_validation(batch_no, warehouse, qty)
|
|
||||||
|
|
||||||
batchwise_qty[batch_no] += qty
|
frappe.db.set_value("Batch", batch, "batch_qty", current_qty)
|
||||||
|
|
||||||
for batch_no in batches:
|
|
||||||
qty = flt(batchwise_qty.get(batch_no, 0), precision)
|
|
||||||
frappe.db.set_value("Batch", batch_no, "batch_qty", qty)
|
|
||||||
|
|
||||||
|
|
||||||
def throw_negative_batch_validation(batch_no, warehouse, qty):
|
def get_batch_current_qty(batch):
|
||||||
|
doctype = frappe.qb.DocType("Batch")
|
||||||
|
query = frappe.qb.from_(doctype).select(doctype.batch_qty).where(doctype.name == batch).for_update()
|
||||||
|
batch_qty = query.run()
|
||||||
|
|
||||||
|
return flt(batch_qty[0][0]) if batch_qty else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def throw_negative_batch_validation(batch_no, qty):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("The Batch {0} has negative quantity {1} in warehouse {2}. Please correct the quantity.").format(
|
_("The Batch {0} has negative quantity {1}. Please correct the quantity.").format(
|
||||||
bold(batch_no), bold(qty), bold(warehouse)
|
bold(batch_no), bold(qty)
|
||||||
),
|
),
|
||||||
title=_("Negative Batch Quantity"),
|
title=_("Negative Batch Quantity"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_distinct_batches(voucher_type, voucher_no):
|
def get_batchwise_qty(voucher_type, voucher_no):
|
||||||
bundles = frappe.get_all(
|
bundles = frappe.get_all(
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
filters={"voucher_no": voucher_no, "voucher_type": voucher_type},
|
filters={"voucher_no": voucher_no, "voucher_type": voucher_type},
|
||||||
@@ -1368,9 +1380,15 @@ def get_distinct_batches(voucher_type, voucher_no):
|
|||||||
if not bundles:
|
if not bundles:
|
||||||
return
|
return
|
||||||
|
|
||||||
return frappe.get_all(
|
batches = frappe.get_all(
|
||||||
"Serial and Batch Entry",
|
"Serial and Batch Entry",
|
||||||
filters={"parent": ("in", bundles), "batch_no": ("is", "set")},
|
filters={"parent": ("in", bundles), "batch_no": ("is", "set")},
|
||||||
|
fields=["batch_no", "SUM(qty) as qty"],
|
||||||
group_by="batch_no",
|
group_by="batch_no",
|
||||||
pluck="batch_no",
|
as_list=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not batches:
|
||||||
|
return frappe._dict({})
|
||||||
|
|
||||||
|
return frappe._dict(batches)
|
||||||
|
|||||||
@@ -817,7 +817,7 @@ class update_entries_after:
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
sle.voucher_type == "Stock Reconciliation"
|
sle.voucher_type == "Stock Reconciliation"
|
||||||
and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
|
and (sle.serial_and_batch_bundle)
|
||||||
and sle.voucher_detail_no
|
and sle.voucher_detail_no
|
||||||
and not self.args.get("sle_id")
|
and not self.args.get("sle_id")
|
||||||
and sle.is_cancelled == 0
|
and sle.is_cancelled == 0
|
||||||
@@ -883,10 +883,7 @@ class update_entries_after:
|
|||||||
self.wh_data.valuation_rate
|
self.wh_data.valuation_rate
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if flt(self.wh_data.qty_after_transaction, self.flt_precision) != 0:
|
||||||
sle.actual_qty < 0
|
|
||||||
and flt(self.wh_data.qty_after_transaction, self.flt_precision) != 0
|
|
||||||
):
|
|
||||||
self.wh_data.valuation_rate = flt(
|
self.wh_data.valuation_rate = flt(
|
||||||
self.wh_data.stock_value, self.currency_precision
|
self.wh_data.stock_value, self.currency_precision
|
||||||
) / flt(self.wh_data.qty_after_transaction, self.flt_precision)
|
) / flt(self.wh_data.qty_after_transaction, self.flt_precision)
|
||||||
@@ -974,10 +971,12 @@ class update_entries_after:
|
|||||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||||
|
|
||||||
def reset_actual_qty_for_stock_reco(self, sle):
|
def reset_actual_qty_for_stock_reco(self, sle):
|
||||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
doc = frappe.get_doc("Stock Reconciliation", sle.voucher_no)
|
||||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||||
|
|
||||||
if sle.actual_qty < 0:
|
if sle.actual_qty < 0:
|
||||||
|
doc.reload()
|
||||||
|
|
||||||
sle.actual_qty = (
|
sle.actual_qty = (
|
||||||
flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
|
flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
|
||||||
* -1
|
* -1
|
||||||
@@ -986,6 +985,16 @@ class update_entries_after:
|
|||||||
if abs(sle.actual_qty) == 0.0:
|
if abs(sle.actual_qty) == 0.0:
|
||||||
sle.is_cancelled = 1
|
sle.is_cancelled = 1
|
||||||
|
|
||||||
|
if sle.serial_and_batch_bundle:
|
||||||
|
for row in doc.items:
|
||||||
|
if row.name == sle.voucher_detail_no:
|
||||||
|
row.db_set("current_serial_and_batch_bundle", "")
|
||||||
|
|
||||||
|
sabb_doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||||
|
sabb_doc.voucher_detail_no = None
|
||||||
|
sabb_doc.voucher_no = None
|
||||||
|
sabb_doc.cancel()
|
||||||
|
|
||||||
if sle.serial_and_batch_bundle and frappe.get_cached_value("Item", sle.item_code, "has_serial_no"):
|
if sle.serial_and_batch_bundle and frappe.get_cached_value("Item", sle.item_code, "has_serial_no"):
|
||||||
self.update_serial_no_status(sle)
|
self.update_serial_no_status(sle)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user