mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-19 23:12:13 +00:00
Merge pull request #41666 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -59,12 +59,14 @@ repos:
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: "Run ruff linter and apply fixes"
|
||||
args: ["--fix"]
|
||||
name: "Run ruff import sorter"
|
||||
args: ["--select=I", "--fix"]
|
||||
|
||||
- id: ruff
|
||||
name: "Run ruff linter"
|
||||
|
||||
- id: ruff-format
|
||||
name: "Format Python code"
|
||||
|
||||
name: "Run ruff formatter"
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: weekly
|
||||
|
||||
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
|
||||
@@ -228,6 +228,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.submit_serial_batch_bundle()
|
||||
|
||||
if self.coupon_code:
|
||||
|
||||
@@ -318,29 +318,28 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
pos.reload()
|
||||
|
||||
pos_return1 = make_sales_return(pos.name)
|
||||
|
||||
# partial return 1
|
||||
pos_return1.get("items")[0].qty = -1
|
||||
pos_return1.submit()
|
||||
pos_return1.reload()
|
||||
|
||||
bundle_id = frappe.get_doc(
|
||||
"Serial and Batch Bundle", pos_return1.get("items")[0].serial_and_batch_bundle
|
||||
)
|
||||
|
||||
bundle_id.remove(bundle_id.entries[1])
|
||||
bundle_id.save()
|
||||
|
||||
bundle_id.load_from_db()
|
||||
|
||||
serial_no = bundle_id.entries[0].serial_no
|
||||
self.assertEqual(serial_no, serial_nos[0])
|
||||
|
||||
pos_return1.insert()
|
||||
pos_return1.submit()
|
||||
|
||||
# partial return 2
|
||||
pos_return2 = make_sales_return(pos.name)
|
||||
pos_return2.submit()
|
||||
|
||||
self.assertEqual(pos_return2.get("items")[0].qty, -1)
|
||||
serial_no = get_serial_nos_from_bundle(pos_return2.get("items")[0].serial_and_batch_bundle)[0]
|
||||
self.assertEqual(serial_no, serial_nos[1])
|
||||
|
||||
@@ -54,7 +54,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -481,7 +481,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Failed")
|
||||
if isinstance(error_message, list):
|
||||
error_message = frappe.json.dumps(error_message)
|
||||
error_message = json.dumps(error_message)
|
||||
closing_entry.db_set("error_message", error_message)
|
||||
raise
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -504,7 +506,7 @@ def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if isinstance(for_filter, str):
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
for_filter = json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
|
||||
@@ -448,7 +448,7 @@ class PurchaseInvoice(BuyingController):
|
||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
asset_received_but_not_billed = None
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
|
||||
if self.update_stock:
|
||||
self.validate_item_code()
|
||||
@@ -531,26 +531,40 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
if not asset_received_but_not_billed:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif item.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
asset_category_account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not asset_category_account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||
title=_("Missing Account"),
|
||||
account = None
|
||||
if item.pr_detail:
|
||||
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
|
||||
arbnb_booked_in_pr = frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": item.purchase_receipt,
|
||||
"account": asset_received_but_not_billed,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
if arbnb_booked_in_pr:
|
||||
account = asset_received_but_not_billed
|
||||
|
||||
if not account:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(item.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
account = get_asset_category_account(
|
||||
account_type, item=item.item_code, company=self.company
|
||||
)
|
||||
if not account:
|
||||
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||
throw(
|
||||
_("Please set Fixed Asset Account in {} against {}.").format(
|
||||
form_link, self.company
|
||||
),
|
||||
title=_("Missing Account"),
|
||||
)
|
||||
item.expense_account = account
|
||||
elif not item.expense_account and for_validate:
|
||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||
|
||||
@@ -707,6 +721,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_stock_ledger()
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:ACC-REPOST-{#####}",
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -55,11 +53,10 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:27.362567",
|
||||
"modified": "2024-05-23 17:00:42.984798",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-10-19 21:59:33.553852",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -99,7 +98,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-26 14:21:35.719727",
|
||||
"modified": "2024-05-23 17:00:31.540640",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
|
||||
@@ -450,6 +450,7 @@ class SalesInvoice(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
self.update_stock_ledger()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
@@ -3720,9 +3721,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
map_docs(
|
||||
method="erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_names=frappe.json.dumps([dn1.name, dn2.name]),
|
||||
source_names=json.dumps([dn1.name, dn2.name]),
|
||||
target_doc=si,
|
||||
args=frappe.json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
args=json.dumps({"customer": dn1.customer, "merge_taxes": 1, "filtered_children": []}),
|
||||
)
|
||||
si.save().submit()
|
||||
|
||||
|
||||
@@ -870,7 +870,8 @@
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_92",
|
||||
@@ -926,7 +927,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-25 15:56:44.828634",
|
||||
"modified": "2024-05-23 16:36:18.970862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -936,4 +937,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -161,7 +163,7 @@ def get_linked_payments_for_doc(
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
selections = json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
@@ -21,16 +23,12 @@ def execute(filters=None):
|
||||
|
||||
data = []
|
||||
for d in entries:
|
||||
invoice = invoice_details.get(d.against_voucher) or frappe._dict()
|
||||
|
||||
if d.reference_type == "Purchase Invoice":
|
||||
payment_amount = flt(d.debit) or -1 * flt(d.credit)
|
||||
else:
|
||||
payment_amount = flt(d.credit) or -1 * flt(d.debit)
|
||||
invoice = invoice_details.get(d.against_voucher_no) or frappe._dict()
|
||||
payment_amount = d.amount
|
||||
|
||||
d.update({"range1": 0, "range2": 0, "range3": 0, "range4": 0, "outstanding": payment_amount})
|
||||
|
||||
if d.against_voucher:
|
||||
if d.against_voucher_no:
|
||||
ReceivablePayableReport(filters).get_ageing_data(invoice.posting_date, d)
|
||||
|
||||
row = [
|
||||
@@ -39,11 +37,10 @@ def execute(filters=None):
|
||||
d.party_type,
|
||||
d.party,
|
||||
d.posting_date,
|
||||
d.against_voucher,
|
||||
d.against_voucher_no,
|
||||
invoice.posting_date,
|
||||
invoice.due_date,
|
||||
d.debit,
|
||||
d.credit,
|
||||
d.amount,
|
||||
d.remarks,
|
||||
d.age,
|
||||
d.range1,
|
||||
@@ -111,8 +108,7 @@ def get_columns(filters):
|
||||
"width": 100,
|
||||
},
|
||||
{"fieldname": "due_date", "label": _("Payment Due Date"), "fieldtype": "Date", "width": 100},
|
||||
{"fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency", "width": 140},
|
||||
{"fieldname": "remarks", "label": _("Remarks"), "fieldtype": "Data", "width": 200},
|
||||
{"fieldname": "age", "label": _("Age"), "fieldtype": "Int", "width": 50},
|
||||
{"fieldname": "range1", "label": _("0-30"), "fieldtype": "Currency", "width": 140},
|
||||
@@ -129,51 +125,68 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = []
|
||||
|
||||
if not filters.party_type:
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
filters.party_type = "Supplier"
|
||||
else:
|
||||
filters.party_type = "Customer"
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("party_type=%(party_type)s")
|
||||
conditions.append(ple.delinked.eq(0))
|
||||
if filters.payment_type == _("Outgoing"):
|
||||
conditions.append(ple.party_type.eq("Supplier"))
|
||||
conditions.append(ple.against_voucher_type.eq("Purchase Invoice"))
|
||||
else:
|
||||
conditions.append(ple.party_type.eq("Customer"))
|
||||
conditions.append(ple.against_voucher_type.eq("Sales Invoice"))
|
||||
|
||||
if filters.party:
|
||||
conditions.append("party=%(party)s")
|
||||
|
||||
if filters.party_type:
|
||||
conditions.append("against_voucher_type=%(reference_type)s")
|
||||
filters["reference_type"] = (
|
||||
"Sales Invoice" if filters.party_type == "Customer" else "Purchase Invoice"
|
||||
)
|
||||
conditions.append(ple.party.eq(filters.party))
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions.append("posting_date >= %(from_date)s")
|
||||
conditions.append(ple.posting_date.gte(filters.get("from_date")))
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions.append("posting_date <= %(to_date)s")
|
||||
conditions.append(ple.posting_date.lte(filters.get("to_date")))
|
||||
|
||||
return "and " + " and ".join(conditions) if conditions else ""
|
||||
if filters.get("company"):
|
||||
conditions.append(ple.company.eq(filters.get("company")))
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""select
|
||||
voucher_type, voucher_no, party_type, party, posting_date, debit, credit, remarks, against_voucher
|
||||
from `tabGL Entry`
|
||||
where company=%(company)s and voucher_type in ('Journal Entry', 'Payment Entry') and is_cancelled = 0 {}
|
||||
""".format(get_conditions(filters)),
|
||||
filters,
|
||||
as_dict=1,
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
conditions = get_conditions(filters)
|
||||
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
Abs(ple.amount).as_("amount"),
|
||||
ple.remarks,
|
||||
ple.against_voucher_no,
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
res = query.run(as_dict=True)
|
||||
return res
|
||||
|
||||
|
||||
def get_invoice_posting_date_map(filters):
|
||||
invoice_details = {}
|
||||
dt = "Sales Invoice" if filters.get("payment_type") == _("Incoming") else "Purchase Invoice"
|
||||
for t in frappe.db.sql(f"select name, posting_date, due_date from `tab{dt}`", as_dict=1):
|
||||
dt = (
|
||||
qb.DocType("Sales Invoice")
|
||||
if filters.get("payment_type") == _("Incoming")
|
||||
else qb.DocType("Purchase Invoice")
|
||||
)
|
||||
res = (
|
||||
qb.from_(dt)
|
||||
.select(dt.name, dt.posting_date, dt.due_date)
|
||||
.where((dt.docstatus.eq(1)) & (dt.company.eq(filters.get("company"))))
|
||||
.run(as_dict=1)
|
||||
)
|
||||
for t in res:
|
||||
invoice_details[t.name] = t
|
||||
|
||||
return invoice_details
|
||||
|
||||
@@ -56,7 +56,7 @@ def get_fiscal_year(
|
||||
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
|
||||
):
|
||||
if isinstance(boolean, str):
|
||||
boolean = frappe.json.loads(boolean)
|
||||
boolean = loads(boolean)
|
||||
|
||||
fiscal_years = get_fiscal_years(
|
||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
|
||||
|
||||
@@ -513,7 +513,7 @@ erpnext.buying.RequestforQuotationController = class RequestforQuotationControll
|
||||
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
||||
args: {
|
||||
doctype: "Supplier",
|
||||
tag: args.tag,
|
||||
tag: "%" + args.tag + "%",
|
||||
},
|
||||
callback: load_suppliers,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import flt, format_datetime, get_datetime
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
|
||||
@@ -513,6 +514,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.rejected_warehouse = ""
|
||||
target_doc.warehouse = source_doc.rejected_warehouse
|
||||
target_doc.received_qty = target_doc.qty
|
||||
target_doc.return_qty_from_rejected_warehouse = 1
|
||||
|
||||
elif doctype == "Purchase Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
@@ -570,7 +572,14 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if default_warehouse_for_sales_return:
|
||||
target_doc.warehouse = default_warehouse_for_sales_return
|
||||
|
||||
if source_doc.item_code:
|
||||
if (
|
||||
(source_doc.serial_no or source_doc.batch_no)
|
||||
and not source_doc.serial_and_batch_bundle
|
||||
and not source_doc.use_serial_batch_fields
|
||||
):
|
||||
target_doc.set("use_serial_batch_fields", 1)
|
||||
|
||||
if source_doc.item_code and target_doc.get("use_serial_batch_fields"):
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
@@ -578,14 +587,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
return
|
||||
|
||||
if not target_doc.get("use_serial_batch_fields"):
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if not target_doc.get(qty_field):
|
||||
continue
|
||||
|
||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||
elif target_doc.get("use_serial_batch_fields"):
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
|
||||
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -839,3 +841,229 @@ def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_vouc
|
||||
batches.update(get_batches_from_bundle(ids))
|
||||
|
||||
return batches
|
||||
|
||||
|
||||
def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected=False):
|
||||
available_dict = get_available_serial_batches(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not available_dict:
|
||||
frappe.throw(_("No Serial / Batches are available for return"))
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
if not _bundle_ids:
|
||||
return frappe._dict({})
|
||||
|
||||
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||
|
||||
|
||||
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
available_dict = frappe._dict({})
|
||||
batch_serial_nos = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"`tabSerial and Batch Entry`.`serial_no`",
|
||||
"`tabSerial and Batch Entry`.`batch_no`",
|
||||
"`tabSerial and Batch Entry`.`qty`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||
],
|
||||
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||
)
|
||||
|
||||
for row in batch_serial_nos:
|
||||
key = row.voucher_detail_no
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if key not in available_dict:
|
||||
available_dict[key] = frappe._dict(
|
||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
||||
)
|
||||
|
||||
available_dict[key]["qty"] += row.qty
|
||||
|
||||
if row.serial_no:
|
||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||
elif row.batch_no:
|
||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||
|
||||
return available_dict
|
||||
|
||||
|
||||
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
pluck_field = "serial_and_batch_bundle"
|
||||
if is_rejected:
|
||||
del filters["serial_and_batch_bundle"]
|
||||
filters["rejected_serial_and_batch_bundle"] = ("is", "set")
|
||||
pluck_field = "rejected_serial_and_batch_bundle"
|
||||
|
||||
_bundle_ids = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck=pluck_field,
|
||||
)
|
||||
|
||||
if not _bundle_ids:
|
||||
return {}
|
||||
|
||||
del filters["name"]
|
||||
|
||||
filters[field] = ("in", reference_ids)
|
||||
|
||||
if not is_rejected:
|
||||
_bundle_ids.extend(
|
||||
frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
pluck="serial_and_batch_bundle",
|
||||
)
|
||||
)
|
||||
else:
|
||||
fields = [
|
||||
"serial_and_batch_bundle",
|
||||
]
|
||||
|
||||
if is_rejected:
|
||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
||||
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if is_rejected:
|
||||
if d.get("return_qty_from_rejected_warehouse"):
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("rejected_serial_and_batch_bundle"))
|
||||
else:
|
||||
_bundle_ids.append(d.get("serial_and_batch_bundle"))
|
||||
|
||||
return _bundle_ids
|
||||
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = row.get(warehouse_field)
|
||||
qty = abs(row.get(qty_field))
|
||||
|
||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
||||
|
||||
if data.serial_nos:
|
||||
available_serial_nos = []
|
||||
for serial_no, sn_qty in data.serial_nos.items():
|
||||
if sn_qty != 0:
|
||||
available_serial_nos.append(serial_no)
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||
else:
|
||||
filterd_serial_batch["serial_nos"] = available_serial_nos
|
||||
|
||||
elif data.batches:
|
||||
for batch_no, batch_qty in data.batches.items():
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
batch_qty = batch_qty * -1
|
||||
|
||||
if batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
batch_qty = get_available_batch_qty(
|
||||
parent_doc,
|
||||
batch_no,
|
||||
warehouse,
|
||||
)
|
||||
|
||||
if batch_qty <= 0:
|
||||
frappe.throw(
|
||||
_("Batch {0} is not available in warehouse {1}").format(batch_no, warehouse),
|
||||
title=_("Batch Not Available for Return"),
|
||||
)
|
||||
|
||||
if qty <= 0:
|
||||
break
|
||||
|
||||
if batch_qty > qty:
|
||||
filterd_serial_batch["batches"][batch_no] = qty
|
||||
qty = 0
|
||||
else:
|
||||
filterd_serial_batch["batches"][batch_no] += batch_qty
|
||||
qty -= batch_qty
|
||||
|
||||
return filterd_serial_batch
|
||||
|
||||
|
||||
def get_available_batch_qty(parent_doc, batch_no, warehouse):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
return get_batch_qty(
|
||||
batch_no,
|
||||
warehouse,
|
||||
posting_date=parent_doc.posting_date,
|
||||
posting_time=parent_doc.posting_time,
|
||||
for_stock_levels=True,
|
||||
)
|
||||
|
||||
|
||||
def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_field=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
type_of_transaction = "Outward"
|
||||
if parent_doc.doctype in ["Sales Invoice", "Delivery Note", "POS Invoice"]:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"item_code": child_doc.item_code,
|
||||
"warehouse": warehouse,
|
||||
"serial_nos": data.get("serial_nos"),
|
||||
"batches": data.get("batches"),
|
||||
"posting_date": parent_doc.posting_date,
|
||||
"posting_time": parent_doc.posting_time,
|
||||
"voucher_type": parent_doc.doctype,
|
||||
"voucher_no": parent_doc.name,
|
||||
"voucher_detail_no": child_doc.name,
|
||||
"qty": child_doc.qty,
|
||||
"company": parent_doc.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
|
||||
return cls_obj.name
|
||||
|
||||
|
||||
def get_available_serial_nos(serial_nos, warehouse):
|
||||
return frappe.get_all(
|
||||
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ from erpnext.accounts.general_ledger import (
|
||||
)
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.controllers.sales_and_purchase_return import (
|
||||
available_serial_batch_for_return,
|
||||
filter_serial_batches,
|
||||
make_serial_batch_bundle_for_return,
|
||||
)
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
@@ -217,6 +222,125 @@ class StockController(AccountsController):
|
||||
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
|
||||
self.create_serial_batch_bundle(bundle_details, row)
|
||||
|
||||
def make_bundle_for_sales_purchase_return(self, table_name=None):
|
||||
if not self.get("is_return"):
|
||||
return
|
||||
|
||||
if not table_name:
|
||||
table_name = "items"
|
||||
|
||||
self.make_bundle_for_non_rejected_qty(table_name)
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
self.make_bundle_for_rejected_qty(table_name)
|
||||
|
||||
def make_bundle_for_rejected_qty(self, table_name=None):
|
||||
field, reference_ids = self.get_reference_ids(
|
||||
table_name, "rejected_qty", "rejected_serial_and_batch_bundle"
|
||||
)
|
||||
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(
|
||||
field, child_doctype, reference_ids, is_rejected=True
|
||||
)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
qty_field = "rejected_qty"
|
||||
warehouse_field = "rejected_warehouse"
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
qty_field = "qty"
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
data = filter_serial_batches(
|
||||
self, data, row, warehouse_field=warehouse_field, qty_field=qty_field
|
||||
)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self, warehouse_field)
|
||||
if row.get("return_qty_from_rejected_warehouse"):
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
row.db_set(
|
||||
{
|
||||
"rejected_serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"rejected_serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def make_bundle_for_non_rejected_qty(self, table_name):
|
||||
field, reference_ids = self.get_reference_ids(table_name)
|
||||
if not reference_ids:
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
data = filter_serial_batches(self, data, row)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||
row.db_set(
|
||||
{
|
||||
"serial_and_batch_bundle": bundle,
|
||||
"batch_no": "",
|
||||
"serial_no": "",
|
||||
}
|
||||
)
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Delivery Note": "dn_detail",
|
||||
"Purchase Receipt": "purchase_receipt_item",
|
||||
"Purchase Invoice": "purchase_invoice_item",
|
||||
"POS Invoice": "pos_invoice_item",
|
||||
}.get(self.doctype)
|
||||
|
||||
if not bundle_field:
|
||||
bundle_field = "serial_and_batch_bundle"
|
||||
|
||||
if not qty_field:
|
||||
qty_field = "qty"
|
||||
|
||||
reference_ids = []
|
||||
|
||||
for row in self.get(table_name):
|
||||
if not self.is_serial_batch_item(row.item_code):
|
||||
continue
|
||||
|
||||
if (
|
||||
row.get(field)
|
||||
and (
|
||||
qty_field == "qty"
|
||||
and not row.get("return_qty_from_rejected_warehouse")
|
||||
or qty_field == "rejected_qty"
|
||||
and (row.get("return_qty_from_rejected_warehouse") or row.get("rejected_warehouse"))
|
||||
)
|
||||
and not row.get("use_serial_batch_fields")
|
||||
and not row.get(bundle_field)
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
return field, reference_ids
|
||||
|
||||
@frappe.request_cache
|
||||
def is_serial_batch_item(self, item_code) -> bool:
|
||||
item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1)
|
||||
|
||||
if item_details.has_serial_no or item_details.has_batch_no:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
@@ -611,35 +735,16 @@ class StockController(AccountsController):
|
||||
def make_package_for_transfer(
|
||||
self, serial_and_batch_bundle, warehouse, type_of_transaction=None, do_not_submit=None
|
||||
):
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", serial_and_batch_bundle)
|
||||
|
||||
if not type_of_transaction:
|
||||
type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = warehouse
|
||||
bundle_doc.type_of_transaction = type_of_transaction
|
||||
bundle_doc.voucher_type = self.doctype
|
||||
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
return make_bundle_for_material_transfer(
|
||||
is_new=self.is_new(),
|
||||
docstatus=self.docstatus,
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.name,
|
||||
serial_and_batch_bundle=serial_and_batch_bundle,
|
||||
warehouse=warehouse,
|
||||
type_of_transaction=type_of_transaction,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
sl_dict = frappe._dict(
|
||||
@@ -1557,3 +1662,38 @@ def create_item_wise_repost_entries(
|
||||
repost_entries.append(repost_entry)
|
||||
|
||||
return repost_entries
|
||||
|
||||
|
||||
def make_bundle_for_material_transfer(**kwargs):
|
||||
if isinstance(kwargs, dict):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", kwargs.serial_and_batch_bundle)
|
||||
|
||||
if not kwargs.type_of_transaction:
|
||||
kwargs.type_of_transaction = "Inward"
|
||||
|
||||
bundle_doc = frappe.copy_doc(bundle_doc)
|
||||
bundle_doc.warehouse = kwargs.warehouse
|
||||
bundle_doc.type_of_transaction = kwargs.type_of_transaction
|
||||
bundle_doc.voucher_type = kwargs.voucher_type
|
||||
bundle_doc.voucher_no = "" if kwargs.is_new or kwargs.docstatus == 2 else kwargs.voucher_no
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
row.is_outward = 0
|
||||
row.qty = abs(row.qty)
|
||||
row.stock_value_difference = abs(row.stock_value_difference)
|
||||
if kwargs.type_of_transaction == "Outward":
|
||||
row.qty *= -1
|
||||
row.stock_value_difference *= row.stock_value_difference
|
||||
row.is_outward = 1
|
||||
|
||||
row.warehouse = kwargs.warehouse
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -327,13 +327,13 @@ class SubcontractingController(StockController):
|
||||
consumed_bundles.batch_nos[batch_no] += abs(qty)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.serial_no:
|
||||
if row.serial_no and not consumed_bundles.serial_nos:
|
||||
self.available_materials[key]["serial_no"] = list(
|
||||
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
||||
)
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.batch_no:
|
||||
if row.batch_no and not consumed_bundles.batch_nos:
|
||||
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
|
||||
|
||||
def get_available_materials(self):
|
||||
|
||||
@@ -55,6 +55,14 @@ frappe.ui.form.on("Project", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -83,7 +83,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
||||
}
|
||||
|
||||
this.frm.refresh_fields();
|
||||
this.frm.refresh_field("taxes");
|
||||
}
|
||||
|
||||
calculate_discount_amount() {
|
||||
@@ -841,7 +841,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
});
|
||||
}
|
||||
|
||||
this.frm.refresh_fields();
|
||||
this.frm.refresh_field("taxes");
|
||||
}
|
||||
|
||||
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
@@ -321,7 +323,7 @@ class TestCustomer(FrappeTestCase):
|
||||
frappe.ValidationError,
|
||||
update_child_qty_rate,
|
||||
so.doctype,
|
||||
frappe.json.dumps([modified_item]),
|
||||
json.dumps([modified_item]),
|
||||
so.name,
|
||||
)
|
||||
|
||||
|
||||
@@ -9,5 +9,13 @@ frappe.ui.form.on("Product Bundle", {
|
||||
query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code",
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("item_code", "items", () => {
|
||||
return {
|
||||
filters: {
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1139,7 +1139,8 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Inter Company Order Reference",
|
||||
"options": "Purchase Order",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
@@ -1645,7 +1646,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-29 16:27:41.539613",
|
||||
"modified": "2024-05-23 16:35:54.905804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -8,8 +8,11 @@ from pypika import Order
|
||||
class DeprecatedSerialNoValuation:
|
||||
@deprecated
|
||||
def calculate_stock_value_from_deprecarated_ledgers(self):
|
||||
if not frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name"
|
||||
if not frappe.db.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["name"],
|
||||
filters={"serial_no": ("is", "set"), "is_cancelled": 0, "item_code": self.sle.item_code},
|
||||
limit=1,
|
||||
):
|
||||
return
|
||||
|
||||
@@ -41,6 +44,12 @@ class DeprecatedSerialNoValuation:
|
||||
# get rate from serial nos within same company
|
||||
incoming_values = 0.0
|
||||
for serial_no in serial_nos:
|
||||
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
|
||||
if sn_details and sn_details.purchase_rate and sn_details.company == self.sle.company:
|
||||
self.serial_no_incoming_rate[serial_no] += flt(sn_details.purchase_rate)
|
||||
incoming_values += self.serial_no_incoming_rate[serial_no]
|
||||
continue
|
||||
|
||||
table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
stock_ledgers = (
|
||||
frappe.qb.from_(table)
|
||||
|
||||
@@ -208,7 +208,8 @@ def get_batch_qty(
|
||||
|
||||
:param batch_no: Optional - give qty for this batch no
|
||||
:param warehouse: Optional - give qty for this warehouse
|
||||
:param item_code: Optional - give qty for this item"""
|
||||
:param item_code: Optional - give qty for this item
|
||||
:param for_stock_levels: True consider expired batches"""
|
||||
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
|
||||
@@ -467,6 +467,7 @@ class DeliveryNote(SellingController):
|
||||
if not self.get(table_name):
|
||||
continue
|
||||
|
||||
self.make_bundle_for_sales_purchase_return(table_name)
|
||||
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
@@ -1371,6 +1372,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
if source_parent.doctype == "Delivery Note" and source.received_qty:
|
||||
target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty)
|
||||
|
||||
if source.get("use_serial_batch_fields"):
|
||||
target.set("use_serial_batch_fields", 1)
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
doctype,
|
||||
source_name,
|
||||
|
||||
@@ -249,18 +249,15 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertTrue(dn.items[0].serial_no)
|
||||
|
||||
frappe.flags.ignore_serial_batch_bundle_validation = False
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
# return entry
|
||||
dn1 = make_sales_return(dn.name)
|
||||
|
||||
dn1.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn1.save()
|
||||
dn1.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[0:2])
|
||||
dn1.submit()
|
||||
dn1.reload()
|
||||
|
||||
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos1:
|
||||
@@ -269,21 +266,15 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn2 = make_sales_return(dn.name)
|
||||
|
||||
dn2.items[0].qty = -2
|
||||
|
||||
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
|
||||
bundle_doc.set("entries", bundle_doc.entries[:2])
|
||||
bundle_doc.save()
|
||||
|
||||
dn2.save()
|
||||
dn2.items[0].serial_no = "\n".join(get_serial_nos(serial_nos)[2:4])
|
||||
dn2.submit()
|
||||
dn2.reload()
|
||||
|
||||
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
|
||||
for serial_no in returned_serial_nos2:
|
||||
self.assertTrue(serial_no in serial_nos)
|
||||
self.assertFalse(serial_no in returned_serial_nos1)
|
||||
|
||||
frappe.flags.use_serial_and_batch_fields = False
|
||||
|
||||
def test_sales_return_for_non_bundled_items_partial(self):
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -428,7 +419,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(dn.per_returned, 100)
|
||||
self.assertEqual(dn.status, "Return Issued")
|
||||
|
||||
def test_delivery_note_return_valuation_on_different_warehuose(self):
|
||||
def test_delivery_note_return_valuation_on_different_warehouse(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
|
||||
|
||||
@@ -370,6 +370,7 @@ class PurchaseReceipt(BuyingController):
|
||||
else:
|
||||
self.db_set("status", "Completed")
|
||||
|
||||
self.make_bundle_for_sales_purchase_return()
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating ordered qty, reserved_qty_for_subcontract in bin
|
||||
|
||||
@@ -2645,7 +2645,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
for row in inter_transfer_dn_return.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self):
|
||||
def test_internal_transfer_with_serial_batch_items_without_use_serial_batch_fields(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"purchase_invoice",
|
||||
"column_break_40",
|
||||
"allow_zero_valuation_rate",
|
||||
"return_qty_from_rejected_warehouse",
|
||||
"is_fixed_asset",
|
||||
"asset_location",
|
||||
"asset_category",
|
||||
@@ -1116,12 +1117,19 @@
|
||||
"hidden": 1,
|
||||
"label": "Apply TDS",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "return_qty_from_rejected_warehouse",
|
||||
"fieldtype": "Check",
|
||||
"label": "Return Qty from Rejected Warehouse",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-08 20:00:16.278292",
|
||||
"modified": "2024-05-28 09:48:24.448815",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
@@ -1132,4 +1140,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class PurchaseReceiptItem(Document):
|
||||
rejected_serial_no: DF.Text | None
|
||||
rejected_warehouse: DF.Link | None
|
||||
retain_sample: DF.Check
|
||||
return_qty_from_rejected_warehouse: DF.Check
|
||||
returned_qty: DF.Float
|
||||
rm_supp_cost: DF.Currency
|
||||
sales_order: DF.Link | None
|
||||
|
||||
@@ -377,7 +377,7 @@ class TestPutawayRule(FrappeTestCase):
|
||||
apply_putaway_rule=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
stock_entry.save()
|
||||
stock_entry.submit()
|
||||
stock_entry.load_from_db()
|
||||
|
||||
self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
|
||||
@@ -398,11 +398,17 @@ class TestPutawayRule(FrappeTestCase):
|
||||
|
||||
self.assertUnchangedItemsOnResave(stock_entry)
|
||||
|
||||
for row in stock_entry.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
frappe.delete_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
|
||||
stock_entry.load_from_db()
|
||||
stock_entry.cancel()
|
||||
|
||||
rivs = frappe.get_all("Repost Item Valuation", filters={"voucher_no": stock_entry.name})
|
||||
for row in rivs:
|
||||
riv_doc = frappe.get_doc("Repost Item Valuation", row.name)
|
||||
riv_doc.cancel()
|
||||
riv_doc.delete()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1)
|
||||
|
||||
stock_entry.delete()
|
||||
pr.cancel()
|
||||
rule_1.delete()
|
||||
|
||||
@@ -156,6 +156,8 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
def validate_serial_nos_duplicate(self):
|
||||
# Don't inward same serial number multiple times
|
||||
if self.voucher_type in ["POS Invoice", "Pick List"]:
|
||||
return
|
||||
|
||||
if not self.warehouse:
|
||||
return
|
||||
|
||||
@@ -111,6 +111,8 @@ frappe.ui.form.on("Stock Entry", {
|
||||
// or a pre-existing batch
|
||||
if (frm.doc.purpose != "Material Receipt") {
|
||||
filters["warehouse"] = item.s_warehouse || item.t_warehouse;
|
||||
} else {
|
||||
filters["is_inward"] = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1110,6 +1112,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
|
||||
on_submit() {
|
||||
this.clean_up();
|
||||
this.refresh_serial_batch_bundle_field();
|
||||
}
|
||||
|
||||
refresh_serial_batch_bundle_field() {
|
||||
frappe.route_hooks.after_submit = (frm_obj) => {
|
||||
frm_obj.reload_doc();
|
||||
};
|
||||
}
|
||||
|
||||
after_cancel() {
|
||||
|
||||
@@ -226,6 +226,7 @@ class StockEntry(StockController):
|
||||
if not self.from_bom:
|
||||
self.fg_completed_qty = 0.0
|
||||
|
||||
self.make_serial_and_batch_bundle_for_outward()
|
||||
self.validate_serialized_batch()
|
||||
self.calculate_rate_and_amount()
|
||||
self.validate_putaway_capacity()
|
||||
@@ -289,9 +290,6 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
|
||||
self.set_material_request_transfer_status("In Transit")
|
||||
|
||||
def before_save(self):
|
||||
self.make_serial_and_batch_bundle_for_outward()
|
||||
|
||||
def on_update(self):
|
||||
self.set_serial_and_batch_bundle()
|
||||
|
||||
@@ -992,7 +990,7 @@ class StockEntry(StockController):
|
||||
self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
|
||||
|
||||
def make_serial_and_batch_bundle_for_outward(self):
|
||||
if self.docstatus == 1:
|
||||
if self.docstatus == 0:
|
||||
return
|
||||
|
||||
serial_or_batch_items = get_serial_or_batch_items(self.items)
|
||||
@@ -1045,12 +1043,11 @@ class StockEntry(StockController):
|
||||
if not bundle_doc:
|
||||
continue
|
||||
|
||||
if self.docstatus == 0:
|
||||
for entry in bundle_doc.entries:
|
||||
if not entry.serial_no:
|
||||
continue
|
||||
for entry in bundle_doc.entries:
|
||||
if not entry.serial_no:
|
||||
continue
|
||||
|
||||
already_picked_serial_nos.append(entry.serial_no)
|
||||
already_picked_serial_nos.append(entry.serial_no)
|
||||
|
||||
row.serial_and_batch_bundle = bundle_doc.name
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, get_table_name, getdate
|
||||
from frappe.utils import add_to_date, cint, flt, get_datetime, get_table_name, getdate
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from pypika import functions as fn
|
||||
|
||||
@@ -107,6 +107,8 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
||||
if not filters.get("to_date"):
|
||||
frappe.throw(_("'To Date' is required"))
|
||||
|
||||
posting_datetime = get_datetime(add_to_date(filters["to_date"], days=1))
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
@@ -121,7 +123,7 @@ def get_stock_ledger_entries_for_batch_no(filters):
|
||||
(sle.docstatus < 2)
|
||||
& (sle.is_cancelled == 0)
|
||||
& (sle.batch_no != "")
|
||||
& (sle.posting_date <= filters["to_date"])
|
||||
& (sle.posting_datetime < posting_datetime)
|
||||
)
|
||||
.groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse)
|
||||
.orderby(sle.item_code, sle.warehouse)
|
||||
|
||||
@@ -9,6 +9,9 @@ from frappe import _
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
|
||||
from erpnext.stock.stock_ledger import get_stock_ledger_entries
|
||||
|
||||
BUYING_VOUCHER_TYPES = ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]
|
||||
SELLING_VOUCHER_TYPES = ["Sales Invoice", "Delivery Note"]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns(filters)
|
||||
@@ -72,6 +75,20 @@ def get_columns(filters):
|
||||
"fieldname": "qty",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "party_type",
|
||||
"options": "DocType",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Party"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"fieldname": "party",
|
||||
"options": "party_type",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
@@ -102,6 +119,17 @@ def get_data(filters):
|
||||
}
|
||||
)
|
||||
|
||||
# get party details depending on the voucher type
|
||||
party_field = (
|
||||
"supplier"
|
||||
if row.voucher_type in BUYING_VOUCHER_TYPES
|
||||
else ("customer" if row.voucher_type in SELLING_VOUCHER_TYPES else None)
|
||||
)
|
||||
args.party_type = party_field.title() if party_field else None
|
||||
args.party = (
|
||||
frappe.db.get_value(row.voucher_type, row.voucher_no, party_field) if party_field else None
|
||||
)
|
||||
|
||||
serial_nos = []
|
||||
if row.serial_no:
|
||||
parsed_serial_nos = get_serial_nos_from_sle(row.serial_no)
|
||||
|
||||
@@ -27,7 +27,11 @@ def execute(filters=None):
|
||||
items = get_items(filters)
|
||||
sl_entries = get_stock_ledger_entries(filters, items)
|
||||
item_details = get_item_details(items, sl_entries, include_uom)
|
||||
opening_row = get_opening_balance(filters, columns, sl_entries)
|
||||
if filters.get("batch_no"):
|
||||
opening_row = get_opening_balance_from_batch(filters, columns, sl_entries)
|
||||
else:
|
||||
opening_row = get_opening_balance(filters, columns, sl_entries)
|
||||
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||
bundle_details = {}
|
||||
|
||||
@@ -48,13 +52,16 @@ def execute(filters=None):
|
||||
available_serial_nos = {}
|
||||
inventory_dimension_filters_applied = check_inventory_dimension_filters_applied(filters)
|
||||
|
||||
batch_balance_dict = defaultdict(float)
|
||||
batch_balance_dict = frappe._dict({})
|
||||
if actual_qty and filters.get("batch_no"):
|
||||
batch_balance_dict[filters.batch_no] = [actual_qty, stock_value]
|
||||
|
||||
for sle in sl_entries:
|
||||
item_detail = item_details[sle.item_code]
|
||||
|
||||
sle.update(item_detail)
|
||||
if bundle_info := bundle_details.get(sle.serial_and_batch_bundle):
|
||||
data.extend(get_segregated_bundle_entries(sle, bundle_info, batch_balance_dict))
|
||||
data.extend(get_segregated_bundle_entries(sle, bundle_info, batch_balance_dict, filters))
|
||||
continue
|
||||
|
||||
if filters.get("batch_no") or inventory_dimension_filters_applied:
|
||||
@@ -90,7 +97,7 @@ def execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
|
||||
def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict, filters):
|
||||
segregated_entries = []
|
||||
qty_before_transaction = sle.qty_after_transaction - sle.actual_qty
|
||||
stock_value_before_transaction = sle.stock_value - sle.stock_value_difference
|
||||
@@ -109,9 +116,19 @@ def get_segregated_bundle_entries(sle, bundle_details, batch_balance_dict):
|
||||
}
|
||||
)
|
||||
|
||||
if row.batch_no:
|
||||
batch_balance_dict[row.batch_no] += row.qty
|
||||
new_sle.update({"qty_after_transaction": batch_balance_dict[row.batch_no]})
|
||||
if filters.get("batch_no") and row.batch_no:
|
||||
if not batch_balance_dict.get(row.batch_no):
|
||||
batch_balance_dict[row.batch_no] = [0, 0]
|
||||
|
||||
batch_balance_dict[row.batch_no][0] += row.qty
|
||||
batch_balance_dict[row.batch_no][1] += row.stock_value_difference
|
||||
|
||||
new_sle.update(
|
||||
{
|
||||
"qty_after_transaction": batch_balance_dict[row.batch_no][0],
|
||||
"stock_value": batch_balance_dict[row.batch_no][1],
|
||||
}
|
||||
)
|
||||
|
||||
qty_before_transaction += row.qty
|
||||
stock_value_before_transaction += new_sle.stock_value_difference
|
||||
@@ -504,6 +521,62 @@ def get_sle_conditions(filters):
|
||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
|
||||
def get_opening_balance_from_batch(filters, columns, sl_entries):
|
||||
query_filters = {
|
||||
"batch_no": filters.batch_no,
|
||||
"docstatus": 1,
|
||||
"posting_date": ("<", filters.from_date),
|
||||
}
|
||||
|
||||
for fields in ["item_code", "warehouse"]:
|
||||
if filters.get(fields):
|
||||
query_filters[fields] = filters.get(fields)
|
||||
|
||||
opening_data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["sum(actual_qty) as qty_after_transaction", "sum(stock_value_difference) as stock_value"],
|
||||
filters=query_filters,
|
||||
)[0]
|
||||
|
||||
for field in ["qty_after_transaction", "stock_value", "valuation_rate"]:
|
||||
if opening_data.get(field) is None:
|
||||
opening_data[field] = 0.0
|
||||
|
||||
query_filters = [
|
||||
["Serial and Batch Entry", "batch_no", "=", filters.batch_no],
|
||||
["Serial and Batch Bundle", "docstatus", "=", 1],
|
||||
["Serial and Batch Bundle", "posting_date", "<", filters.from_date],
|
||||
]
|
||||
|
||||
for fields in ["item_code", "warehouse"]:
|
||||
if filters.get(fields):
|
||||
query_filters.append(["Serial and Batch Bundle", fields, "=", filters.get(fields)])
|
||||
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
fields=[
|
||||
"sum(`tabSerial and Batch Entry`.`qty`) as qty",
|
||||
"sum(`tabSerial and Batch Entry`.`stock_value_difference`) as stock_value",
|
||||
],
|
||||
filters=query_filters,
|
||||
)
|
||||
|
||||
if bundle_data:
|
||||
opening_data.qty_after_transaction += flt(bundle_data[0].qty)
|
||||
opening_data.stock_value += flt(bundle_data[0].stock_value)
|
||||
if opening_data.qty_after_transaction:
|
||||
opening_data.valuation_rate = flt(opening_data.stock_value) / flt(
|
||||
opening_data.qty_after_transaction
|
||||
)
|
||||
|
||||
return {
|
||||
"item_code": _("'Opening'"),
|
||||
"qty_after_transaction": opening_data.qty_after_transaction,
|
||||
"valuation_rate": opening_data.valuation_rate,
|
||||
"stock_value": opening_data.stock_value,
|
||||
}
|
||||
|
||||
|
||||
def get_opening_balance(filters, columns, sl_entries):
|
||||
if not (filters.item_code and filters.warehouse and filters.from_date):
|
||||
return
|
||||
|
||||
@@ -55,8 +55,45 @@ class SerialBatchBundle:
|
||||
elif not self.sle.is_cancelled:
|
||||
self.validate_item_and_warehouse()
|
||||
|
||||
def is_material_transfer(self):
|
||||
allowed_types = [
|
||||
"Material Transfer",
|
||||
"Send to Subcontractor",
|
||||
"Material Transfer for Manufacture",
|
||||
]
|
||||
|
||||
if (
|
||||
self.sle.voucher_type == "Stock Entry"
|
||||
and not self.sle.is_cancelled
|
||||
and frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose") in allowed_types
|
||||
):
|
||||
return True
|
||||
|
||||
def make_serial_batch_no_bundle_for_material_transfer(self):
|
||||
from erpnext.controllers.stock_controller import make_bundle_for_material_transfer
|
||||
|
||||
bundle = frappe.db.get_value(
|
||||
"Stock Entry Detail", self.sle.voucher_detail_no, "serial_and_batch_bundle"
|
||||
)
|
||||
|
||||
if bundle:
|
||||
new_bundle_id = make_bundle_for_material_transfer(
|
||||
is_new=False,
|
||||
docstatus=1,
|
||||
voucher_type=self.sle.voucher_type,
|
||||
voucher_no=self.sle.voucher_no,
|
||||
serial_and_batch_bundle=bundle,
|
||||
warehouse=self.sle.warehouse,
|
||||
type_of_transaction="Inward" if self.sle.actual_qty > 0 else "Outward",
|
||||
do_not_submit=0,
|
||||
)
|
||||
self.sle.db_set({"serial_and_batch_bundle": new_bundle_id})
|
||||
|
||||
def make_serial_batch_no_bundle(self):
|
||||
self.validate_item()
|
||||
if self.sle.actual_qty > 0 and self.is_material_transfer():
|
||||
self.make_serial_batch_no_bundle_for_material_transfer()
|
||||
return
|
||||
|
||||
sn_doc = SerialBatchCreation(
|
||||
{
|
||||
@@ -143,6 +180,9 @@ class SerialBatchBundle:
|
||||
"serial_and_batch_bundle": sn_doc.name,
|
||||
}
|
||||
|
||||
if self.sle.actual_qty < 0 and self.is_material_transfer():
|
||||
values_to_update["valuation_rate"] = sn_doc.avg_rate
|
||||
|
||||
if not frappe.db.get_single_value(
|
||||
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"
|
||||
):
|
||||
@@ -341,11 +381,9 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
|
||||
if serial_nos:
|
||||
filters["serial_no"] = ("in", serial_nos)
|
||||
|
||||
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx")
|
||||
if not entries:
|
||||
return []
|
||||
serial_nos = frappe.get_all("Serial and Batch Entry", filters=filters, order_by="idx", pluck="serial_no")
|
||||
|
||||
return [d.serial_no for d in entries if d.serial_no]
|
||||
return serial_nos
|
||||
|
||||
|
||||
def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
|
||||
|
||||
@@ -308,7 +308,15 @@ def get_reposting_data(file_path) -> dict:
|
||||
|
||||
attached_file = frappe.get_doc("File", file_name)
|
||||
|
||||
data = gzip.decompress(attached_file.get_content())
|
||||
content = attached_file.get_content()
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
|
||||
try:
|
||||
data = gzip.decompress(content)
|
||||
except Exception:
|
||||
return frappe._dict()
|
||||
|
||||
if data := json.loads(data.decode("utf-8")):
|
||||
data = data
|
||||
|
||||
@@ -1428,7 +1436,11 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
order by posting_datetime desc, creation desc
|
||||
limit 1
|
||||
for update""",
|
||||
args,
|
||||
{
|
||||
"item_code": args.get("item_code"),
|
||||
"warehouse": args.get("warehouse"),
|
||||
"posting_datetime": args.get("posting_datetime"),
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -302,6 +302,21 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
reset_raw_materials_table: (frm) => {
|
||||
frm.clear_table("supplied_items");
|
||||
|
||||
frm.call({
|
||||
method: "reset_raw_materials",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frm.save();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Landed Cost Taxes and Charges", {
|
||||
|
||||
@@ -47,8 +47,11 @@
|
||||
"total_qty",
|
||||
"column_break_27",
|
||||
"total",
|
||||
"raw_material_details",
|
||||
"raw_materials_consumed_section",
|
||||
"reset_raw_materials_table",
|
||||
"column_break_uinr",
|
||||
"get_current_stock",
|
||||
"raw_material_details",
|
||||
"supplied_items",
|
||||
"additional_costs_section",
|
||||
"distribute_additional_costs_based_on",
|
||||
@@ -300,6 +303,7 @@
|
||||
"depends_on": "supplied_items",
|
||||
"fieldname": "raw_material_details",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Raw Materials Consumed",
|
||||
"options": "fa fa-table",
|
||||
"print_hide": 1,
|
||||
@@ -640,12 +644,26 @@
|
||||
"fieldname": "supplier_delivery_note",
|
||||
"fieldtype": "Data",
|
||||
"label": "Supplier Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "raw_materials_consumed_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Raw Materials Actions"
|
||||
},
|
||||
{
|
||||
"fieldname": "reset_raw_materials_table",
|
||||
"fieldtype": "Button",
|
||||
"label": "Reset Raw Materials Table"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uinr",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-16 13:04:00.710534",
|
||||
"modified": "2024-05-28 15:02:13.517969",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt",
|
||||
@@ -714,4 +732,4 @@
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,11 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.update_status()
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_raw_materials(self):
|
||||
self.supplied_items = []
|
||||
self.create_raw_materials_supplied()
|
||||
|
||||
def validate_closed_subcontracting_order(self):
|
||||
for item in self.items:
|
||||
if item.subcontracting_order:
|
||||
|
||||
@@ -1235,6 +1235,116 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertTrue(scr.items[0].serial_and_batch_bundle)
|
||||
self.assertTrue(scr.items[0].rejected_serial_and_batch_bundle)
|
||||
|
||||
def test_subcontracting_receipt_for_batch_materials_without_use_serial_batch_fields(self):
|
||||
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
fg_item = make_item(
|
||||
"Test Subcontracted FG Item With Batch No and Without Use Serial Batch Fields",
|
||||
properties={"is_stock_item": 1, "is_sub_contracted_item": 1},
|
||||
).name
|
||||
|
||||
rm_item1 = make_item(
|
||||
"Test Subcontracted RM Item With Batch No and Without Use Serial Batch Fields",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-RM-BNGS-.####",
|
||||
},
|
||||
).name
|
||||
|
||||
make_item(
|
||||
"Subcontracted Service Item 21",
|
||||
properties={
|
||||
"is_stock_item": 0,
|
||||
},
|
||||
)
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item1])
|
||||
|
||||
rm_batch_no = None
|
||||
for row in bom.items:
|
||||
se = make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=300,
|
||||
)
|
||||
|
||||
se.reload()
|
||||
rm_batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 21",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
self.assertTrue(sco.docstatus)
|
||||
rm_items = [
|
||||
{
|
||||
"name": sco.supplied_items[0].name,
|
||||
"item_code": rm_item1,
|
||||
"rm_item_code": rm_item1,
|
||||
"item_name": rm_item1,
|
||||
"qty": 10,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"rate": 100,
|
||||
"stock_uom": frappe.get_cached_value("Item", rm_item1, "stock_uom"),
|
||||
"use_serial_batch_fields": 1,
|
||||
},
|
||||
]
|
||||
se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
|
||||
se.items[0].subcontracted_item = fg_item
|
||||
se.items[0].s_warehouse = "_Test Warehouse - _TC"
|
||||
se.items[0].t_warehouse = "_Test Warehouse 1 - _TC"
|
||||
se.items[0].use_serial_batch_fields = 1
|
||||
se.items[0].batch_no = rm_batch_no
|
||||
se.submit()
|
||||
|
||||
self.assertEqual(se.items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(se.items[0].use_serial_batch_fields, 1)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty = 2
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 2)
|
||||
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty = 2
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 2)
|
||||
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty = 6
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
|
||||
self.assertEqual(scr.supplied_items[0].batch_no, rm_batch_no)
|
||||
self.assertEqual(get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle), rm_batch_no)
|
||||
|
||||
sco.reload()
|
||||
self.assertEqual(sco.status, "Completed")
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
|
||||
def make_return_subcontracting_receipt(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
Reference in New Issue
Block a user