mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-22 08:08:29 +00:00
Merge pull request #49227 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounting_dimension",
|
||||
"fieldname",
|
||||
"disabled",
|
||||
"column_break_2",
|
||||
"company",
|
||||
@@ -90,11 +91,17 @@
|
||||
"fieldname": "apply_restriction_on_values",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply restriction on dimension values"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-07 14:59:41.869117",
|
||||
"modified": "2025-08-08 14:13:22.203011",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
@@ -139,8 +146,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,16 @@ class AccountingDimensionFilter(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
|
||||
ApplicableOnAccount,
|
||||
)
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
|
||||
|
||||
accounting_dimension: DF.Literal
|
||||
accounting_dimension: DF.Literal[None]
|
||||
accounts: DF.Table[ApplicableOnAccount]
|
||||
allow_or_restrict: DF.Literal["Allow", "Restrict"]
|
||||
apply_restriction_on_values: DF.Check
|
||||
company: DF.Link
|
||||
dimensions: DF.Table[AllowedDimension]
|
||||
disabled: DF.Check
|
||||
fieldname: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
@@ -37,6 +36,10 @@ class AccountingDimensionFilter(Document):
|
||||
self.set("dimensions", [])
|
||||
|
||||
def validate(self):
|
||||
self.fieldname = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
|
||||
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
|
||||
|
||||
self.validate_applicable_accounts()
|
||||
|
||||
def validate_applicable_accounts(self):
|
||||
@@ -72,7 +75,7 @@ def get_dimension_filter_map():
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
p.allow_or_restrict, p.fieldname, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
@@ -87,8 +90,6 @@ def get_dimension_filter_map():
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
|
||||
@@ -462,9 +462,8 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
frappe.db.sql(
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
-> Resolves dunning automatically
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
@@ -156,40 +157,66 @@ class Dunning(AccountsController):
|
||||
]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
Check if all payments have been made and resolve dunning, if yes. Called
|
||||
when a Payment Entry is submitted.
|
||||
"""
|
||||
for reference in doc.references:
|
||||
# Consider partial and full payments:
|
||||
# Submitting full payment: outstanding_amount will be 0
|
||||
# Submitting 1st partial payment: outstanding_amount will be the pending installment
|
||||
# Cancelling full payment: outstanding_amount will revert to total amount
|
||||
# Cancelling last partial payment: outstanding_amount will revert to pending amount
|
||||
submit_condition = reference.outstanding_amount < reference.total_amount
|
||||
cancel_condition = reference.outstanding_amount <= reference.total_amount
|
||||
def update_linked_dunnings(doc, previous_outstanding_amount):
|
||||
if (
|
||||
doc.doctype != "Sales Invoice"
|
||||
or doc.is_return
|
||||
or previous_outstanding_amount == doc.outstanding_amount
|
||||
):
|
||||
return
|
||||
|
||||
if reference.reference_doctype == "Sales Invoice" and (
|
||||
submit_condition if doc.docstatus == 1 else cancel_condition
|
||||
):
|
||||
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
|
||||
to_resolve = doc.outstanding_amount < previous_outstanding_amount
|
||||
state = "Unresolved" if to_resolve else "Resolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
|
||||
if not dunnings:
|
||||
return
|
||||
|
||||
for dunning in dunnings:
|
||||
resolve = True
|
||||
dunning = frappe.get_doc("Dunning", dunning.get("name"))
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_inv = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
outstanding_ps = frappe.get_value(
|
||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
||||
)
|
||||
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
|
||||
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
|
||||
invoices = set()
|
||||
payment_schedule_ids = set()
|
||||
|
||||
dunning.status = "Resolved" if resolve else "Unresolved"
|
||||
dunning.save()
|
||||
for dunning in dunnings:
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoices.add(overdue_payment.sales_invoice)
|
||||
if overdue_payment.payment_schedule:
|
||||
payment_schedule_ids.add(overdue_payment.payment_schedule)
|
||||
|
||||
invoice_outstanding_amounts = dict(
|
||||
frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={"name": ["in", list(invoices)]},
|
||||
fields=["name", "outstanding_amount"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
ps_outstanding_amounts = (
|
||||
dict(
|
||||
frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"name": ["in", list(payment_schedule_ids)]},
|
||||
fields=["name", "outstanding"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
if payment_schedule_ids
|
||||
else {}
|
||||
)
|
||||
|
||||
for dunning in dunnings:
|
||||
has_outstanding = False
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
|
||||
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
|
||||
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
|
||||
if has_outstanding:
|
||||
break
|
||||
|
||||
new_status = "Resolved" if not has_outstanding else "Unresolved"
|
||||
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
|
||||
|
||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
@@ -139,6 +139,64 @@ class TestDunning(FrappeTestCase):
|
||||
self.assertEqual(sales_invoice.status, "Overdue")
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_resolution_from_credit_note(self):
|
||||
"""
|
||||
Test that dunning is resolved when a credit note is issued against the original invoice.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 0
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
credit_note.cancel()
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_not_affected_by_standalone_credit_note(self):
|
||||
"""
|
||||
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 1
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
|
||||
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
|
||||
def create_dunning(overdue_days, dunning_type_name=None):
|
||||
posting_date = add_days(today(), -1 * overdue_days)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, today
|
||||
|
||||
|
||||
@@ -45,22 +46,30 @@ def get_loyalty_details(
|
||||
if not expiry_date:
|
||||
expiry_date = today()
|
||||
|
||||
condition = ""
|
||||
if company:
|
||||
condition = " and company=%s " % frappe.db.escape(company)
|
||||
if not include_expired_entry:
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
|
||||
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(LoyaltyPointEntry)
|
||||
.select(
|
||||
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
|
||||
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
|
||||
)
|
||||
.where(
|
||||
(LoyaltyPointEntry.customer == customer)
|
||||
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
|
||||
& (LoyaltyPointEntry.posting_date <= expiry_date)
|
||||
)
|
||||
.groupby(LoyaltyPointEntry.customer)
|
||||
)
|
||||
|
||||
if company:
|
||||
query = query.where(LoyaltyPointEntry.company == company)
|
||||
|
||||
if not include_expired_entry:
|
||||
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
|
||||
|
||||
loyalty_point_details = query.run(as_dict=True)
|
||||
|
||||
if loyalty_point_details:
|
||||
return loyalty_point_details[0]
|
||||
else:
|
||||
|
||||
@@ -118,9 +118,9 @@ class PaymentEntry(AccountsController):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.update_payment_requests()
|
||||
self.update_payment_schedule()
|
||||
self.make_gl_entries()
|
||||
self.update_outstanding_amounts()
|
||||
self.update_payment_schedule()
|
||||
self.set_status()
|
||||
|
||||
def validate_for_repost(self):
|
||||
@@ -221,10 +221,10 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
super().on_cancel()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.set_status()
|
||||
|
||||
def update_payment_requests(self, cancel=False):
|
||||
|
||||
@@ -55,6 +55,16 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
|
||||
if (this.frm.doc.pos_profile) {
|
||||
frappe.db
|
||||
.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop")
|
||||
.then((r) => {
|
||||
if (!r.exc) {
|
||||
this.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onload_post_render(frm) {
|
||||
@@ -113,6 +123,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
this.frm.meta.default_print_format = r.message.print_format || "";
|
||||
this.frm.doc.campaign = r.message.campaign;
|
||||
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
|
||||
this.frm.skip_default_payment = r.message.skip_default_payment;
|
||||
}
|
||||
this.frm.script_manager.trigger("update_stock");
|
||||
this.calculate_taxes_and_totals();
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"items_section",
|
||||
"update_stock",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"items",
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
@@ -1569,6 +1570,13 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
|
||||
@@ -216,6 +216,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
self.validate_full_payment()
|
||||
self.update_packing_list()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
@@ -367,9 +368,9 @@ class POSInvoice(SalesInvoice):
|
||||
)
|
||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
).format(d.idx, item_code, warehouse, available_stock),
|
||||
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
@@ -680,6 +681,7 @@ class POSInvoice(SalesInvoice):
|
||||
"print_format": print_format,
|
||||
"campaign": profile.get("campaign"),
|
||||
"allow_print_before_pay": profile.get("allow_print_before_pay"),
|
||||
"skip_default_payment": profile.get("disable_grand_total_to_default_mop"),
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -774,10 +776,8 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
bundle_bin_qty = 1000000
|
||||
for item in product_bundle.items:
|
||||
item_bin_qty = get_bin_qty(item.item_code, warehouse)
|
||||
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = available_qty / item.qty
|
||||
max_available_bundles = item_bin_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
@@ -800,13 +800,49 @@ def get_bin_qty(item_code, warehouse):
|
||||
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
"""
|
||||
Calculate total quantity reserved for the given item and warehouse.
|
||||
|
||||
Includes:
|
||||
- Direct sales of the item in submitted POS Invoices
|
||||
- Sales of the item as a component of a Product Bundle
|
||||
|
||||
Excludes consolidated invoices (already merged into Sales Invoices via
|
||||
POS Closing Entry). Used to reflect near real-time availability in the
|
||||
POS UI and to prevent overselling while multiple sessions may be active.
|
||||
"""
|
||||
pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse)
|
||||
packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse)
|
||||
|
||||
reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty
|
||||
|
||||
return reserved_qty
|
||||
|
||||
|
||||
def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
|
||||
"""
|
||||
Get the total reserved quantity for a given item in POS Invoices
|
||||
from a specific child table.
|
||||
|
||||
Args:
|
||||
child_table (str): Name of the child table to query
|
||||
(e.g., "POS Invoice Item", "Packed Item").
|
||||
item_code (str): The Item Code to filter by.
|
||||
warehouse (str): The Warehouse to filter by.
|
||||
|
||||
Returns:
|
||||
float: The total reserved quantity for the item in the given
|
||||
warehouse from submitted, unconsolidated POS Invoices.
|
||||
"""
|
||||
p_inv = frappe.qb.DocType("POS Invoice")
|
||||
p_item = frappe.qb.DocType("POS Invoice Item")
|
||||
p_item = frappe.qb.DocType(child_table)
|
||||
|
||||
qty_column = "qty" if child_table == "Packed Item" else "stock_qty"
|
||||
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(p_inv)
|
||||
.from_(p_item)
|
||||
.select(Sum(p_item.stock_qty).as_("stock_qty"))
|
||||
.select(Sum(p_item[qty_column]).as_("stock_qty"))
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
|
||||
@@ -153,6 +153,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -175,6 +178,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
company: me.frm.doc.company,
|
||||
is_return: 0,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"col_break_warehouse",
|
||||
"update_stock",
|
||||
"set_warehouse",
|
||||
@@ -1644,6 +1645,13 @@
|
||||
"label": "Select Dispatch Address ",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
@@ -2070,7 +2072,12 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
@@ -2080,6 +2087,11 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Invoice",
|
||||
source_name,
|
||||
@@ -2103,7 +2115,7 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
},
|
||||
|
||||
@@ -58,6 +58,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
me.frm.script_manager.trigger("is_pos");
|
||||
me.frm.refresh_fields();
|
||||
frappe.db
|
||||
.get_value("POS Profile", this.frm.doc.pos_profile, "disable_grand_total_to_default_mop")
|
||||
.then((r) => {
|
||||
if (!r.exc) {
|
||||
me.frm.skip_default_payment = r.message.disable_grand_total_to_default_mop;
|
||||
}
|
||||
});
|
||||
}
|
||||
erpnext.queries.setup_warehouse_query(this.frm);
|
||||
}
|
||||
@@ -259,6 +266,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -288,6 +298,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
status: ["!=", "Lost"],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "rate", "amount"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -319,6 +332,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
filters: filters,
|
||||
};
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -497,8 +513,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message && r.message.print_format) {
|
||||
if (r.message) {
|
||||
me.frm.pos_print_format = r.message.print_format;
|
||||
me.frm.skip_default_payment = r.message.skip_default_payment;
|
||||
}
|
||||
me.frm.trigger("update_stock");
|
||||
if (me.frm.doc.taxes_and_charges) {
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"items_section",
|
||||
"scan_barcode",
|
||||
"update_stock",
|
||||
"last_scanned_warehouse",
|
||||
"column_break_39",
|
||||
"set_warehouse",
|
||||
"set_target_warehouse",
|
||||
@@ -2177,6 +2178,13 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -700,6 +700,7 @@ class SalesInvoice(SellingController):
|
||||
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
|
||||
"campaign": pos.get("campaign"),
|
||||
"allow_print_before_pay": pos.get("allow_print_before_pay"),
|
||||
"skip_default_payment": pos.get("disable_grand_total_to_default_mop"),
|
||||
}
|
||||
|
||||
def update_time_sheet(self, sales_invoice):
|
||||
|
||||
@@ -25,17 +25,25 @@ def get_group_by_asset_category_data(filters):
|
||||
|
||||
asset_categories = get_asset_categories_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_category(filters)
|
||||
asset_value_adjustment_map = get_asset_value_adjustment_map_by_category(filters)
|
||||
|
||||
for asset_category in asset_categories:
|
||||
row = frappe._dict()
|
||||
row.update(asset_category)
|
||||
|
||||
adjustments = asset_value_adjustment_map.get(asset_category.get("asset_category"), {})
|
||||
row.adjustment_before_from_date = flt(adjustments.get("adjustment_before_from_date", 0))
|
||||
row.adjustment_till_to_date = flt(adjustments.get("adjustment_till_to_date", 0))
|
||||
row.adjustment_during_period = row.adjustment_till_to_date - row.adjustment_before_from_date
|
||||
|
||||
row.value_as_on_from_date += row.adjustment_before_from_date
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
+ flt(row.adjustment_during_period)
|
||||
)
|
||||
|
||||
row.update(
|
||||
@@ -229,26 +237,93 @@ def get_assets_for_grouped_by_category(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_asset_value_adjustment_map_by_category(filters):
|
||||
asset_value_adjustments = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
a.asset_category AS asset_category,
|
||||
IFNULL(
|
||||
SUM(
|
||||
CASE
|
||||
WHEN gle.posting_date < %(from_date)s
|
||||
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
|
||||
THEN gle.debit - gle.credit
|
||||
ELSE 0
|
||||
END
|
||||
),
|
||||
0) AS value_adjustment_before_from_date,
|
||||
IFNULL(
|
||||
SUM(
|
||||
CASE
|
||||
WHEN gle.posting_date <= %(to_date)s
|
||||
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
|
||||
THEN gle.debit - gle.credit
|
||||
ELSE 0
|
||||
END
|
||||
),
|
||||
0) AS value_adjustment_till_to_date
|
||||
|
||||
FROM `tabGL Entry` gle
|
||||
JOIN `tabAsset` a ON gle.against_voucher = a.name
|
||||
JOIN `tabAsset Category Account` aca
|
||||
ON aca.parent = a.asset_category
|
||||
AND aca.company_name = %(company)s
|
||||
WHERE gle.is_cancelled = 0
|
||||
AND a.docstatus = 1
|
||||
AND a.company = %(company)s
|
||||
AND a.purchase_date <= %(to_date)s
|
||||
AND gle.account = aca.fixed_asset_account
|
||||
GROUP BY a.asset_category
|
||||
""",
|
||||
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
category_value_adjustment_map = {}
|
||||
|
||||
for r in asset_value_adjustments:
|
||||
category_value_adjustment_map[r["asset_category"]] = {
|
||||
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
|
||||
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
|
||||
}
|
||||
|
||||
return category_value_adjustment_map
|
||||
|
||||
|
||||
def get_group_by_asset_data(filters):
|
||||
data = []
|
||||
|
||||
asset_details = get_asset_details_for_grouped_by_category(filters)
|
||||
assets = get_assets_for_grouped_by_asset(filters)
|
||||
asset_value_adjustment_map = get_asset_value_adjustment_map(filters)
|
||||
|
||||
for asset_detail in asset_details:
|
||||
row = frappe._dict()
|
||||
row.update(asset_detail)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
adjustments = asset_value_adjustment_map.get(
|
||||
asset_detail.get("name", ""),
|
||||
{
|
||||
"adjustment_before_from_date": 0.0,
|
||||
"adjustment_till_to_date": 0.0,
|
||||
},
|
||||
)
|
||||
row.adjustment_before_from_date = adjustments["adjustment_before_from_date"]
|
||||
row.adjustment_till_to_date = adjustments["adjustment_till_to_date"]
|
||||
row.adjustment_during_period = flt(row.adjustment_till_to_date) - flt(row.adjustment_before_from_date)
|
||||
|
||||
row.value_as_on_from_date += row.adjustment_before_from_date
|
||||
|
||||
row.value_as_on_to_date = (
|
||||
flt(row.value_as_on_from_date)
|
||||
+ flt(row.value_of_new_purchase)
|
||||
- flt(row.value_of_sold_asset)
|
||||
- flt(row.value_of_scrapped_asset)
|
||||
- flt(row.value_of_capitalized_asset)
|
||||
+ flt(row.adjustment_during_period)
|
||||
)
|
||||
|
||||
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
|
||||
|
||||
row.accumulated_depreciation_as_on_to_date = (
|
||||
flt(row.accumulated_depreciation_as_on_from_date)
|
||||
+ flt(row.depreciation_amount_during_the_period)
|
||||
@@ -432,6 +507,59 @@ def get_assets_for_grouped_by_asset(filters):
|
||||
)
|
||||
|
||||
|
||||
def get_asset_value_adjustment_map(filters):
|
||||
asset_with_value_adjustments = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
a.name AS asset,
|
||||
IFNULL(
|
||||
SUM(
|
||||
CASE
|
||||
WHEN gle.posting_date < %(from_date)s
|
||||
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
|
||||
THEN gle.debit - gle.credit
|
||||
ELSE 0
|
||||
END
|
||||
),
|
||||
0) AS value_adjustment_before_from_date,
|
||||
IFNULL(
|
||||
SUM(
|
||||
CASE
|
||||
WHEN gle.posting_date <= %(to_date)s
|
||||
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
|
||||
THEN gle.debit - gle.credit
|
||||
ELSE 0
|
||||
END
|
||||
),
|
||||
0) AS value_adjustment_till_to_date
|
||||
|
||||
FROM `tabGL Entry` gle
|
||||
JOIN `tabAsset` a ON gle.against_voucher = a.name
|
||||
JOIN `tabAsset Category Account` aca
|
||||
ON aca.parent = a.asset_category
|
||||
AND aca.company_name = %(company)s
|
||||
WHERE gle.is_cancelled = 0
|
||||
AND a.docstatus = 1
|
||||
AND a.company = %(company)s
|
||||
AND a.purchase_date <= %(to_date)s
|
||||
AND gle.account = aca.fixed_asset_account
|
||||
GROUP BY a.name
|
||||
""",
|
||||
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
asset_value_adjustment_map = {}
|
||||
|
||||
for r in asset_with_value_adjustments:
|
||||
asset_value_adjustment_map[r["asset"]] = {
|
||||
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
|
||||
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
|
||||
}
|
||||
|
||||
return asset_value_adjustment_map
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
columns = []
|
||||
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-12-06 13:22:23",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:17:51.995451",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General Ledger",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "General Ledger",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-12-06 13:22:23",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-08-13 12:47:27.645023",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General Ledger",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "General Ledger",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -355,7 +355,13 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
|
||||
query = query.where(si.posting_date <= filters.get("to_date"))
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
query = query.where(sip.mode_of_payment == filters.get("mode_of_payment"))
|
||||
subquery = (
|
||||
frappe.qb.from_(sip)
|
||||
.select(sip.parent)
|
||||
.where(sip.mode_of_payment == filters.get("mode_of_payment"))
|
||||
.groupby(sip.parent)
|
||||
)
|
||||
query = query.where(si.name.isin(subquery))
|
||||
|
||||
if filters.get("warehouse"):
|
||||
if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"):
|
||||
@@ -424,8 +430,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
frappe.qb.from_(si)
|
||||
.join(sii)
|
||||
.on(si.name == sii.parent)
|
||||
.left_join(sip)
|
||||
.on(sip.parent == si.name)
|
||||
.left_join(item)
|
||||
.on(sii.item_code == item.name)
|
||||
.select(
|
||||
@@ -465,7 +469,6 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
sii.qty,
|
||||
sip.mode_of_payment,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
|
||||
@@ -1906,6 +1906,8 @@ def create_payment_ledger_entry(
|
||||
|
||||
|
||||
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
|
||||
from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings
|
||||
|
||||
if not voucher_type or not voucher_no:
|
||||
return
|
||||
|
||||
@@ -1938,6 +1940,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
):
|
||||
outstanding = voucher_outstanding[0]
|
||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
previous_outstanding_amount = ref_doc.outstanding_amount
|
||||
outstanding_amount = flt(
|
||||
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
|
||||
)
|
||||
@@ -1951,6 +1954,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
||||
outstanding_amount,
|
||||
)
|
||||
|
||||
update_linked_dunnings(ref_doc, previous_outstanding_amount)
|
||||
ref_doc.set_status(update=True)
|
||||
ref_doc.notify_update()
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ class Asset(AccountsController):
|
||||
def validate(self):
|
||||
self.validate_category()
|
||||
self.validate_precision()
|
||||
self.validate_linked_purchase_docs()
|
||||
self.set_purchase_doc_row_item()
|
||||
self.validate_asset_values()
|
||||
self.validate_asset_and_reference()
|
||||
@@ -409,6 +410,21 @@ class Asset(AccountsController):
|
||||
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
|
||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||
|
||||
def validate_linked_purchase_docs(self):
|
||||
for doctype_field, doctype_name in [
|
||||
("purchase_receipt", "Purchase Receipt"),
|
||||
("purchase_invoice", "Purchase Invoice"),
|
||||
]:
|
||||
linked_doc = getattr(self, doctype_field, None)
|
||||
if linked_doc:
|
||||
docstatus = frappe.db.get_value(doctype_name, linked_doc, "docstatus")
|
||||
if docstatus == 0:
|
||||
frappe.throw(
|
||||
_("{0} is still in Draft. Please submit it before saving the Asset.").format(
|
||||
get_link_to_form(doctype_name, linked_doc)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_gross_and_purchase_amount(self):
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
@@ -1083,7 +1099,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
|
||||
def make_journal_entry(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
_,
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
|
||||
@@ -463,8 +463,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
if (internal) {
|
||||
let button_label =
|
||||
me.frm.doc.company === me.frm.doc.represents_company
|
||||
? "Internal Sales Order"
|
||||
: "Inter Company Sales Order";
|
||||
? __("Internal Sales Order")
|
||||
: __("Inter Company Sales Order");
|
||||
|
||||
me.frm.add_custom_button(
|
||||
button_label,
|
||||
|
||||
@@ -41,8 +41,9 @@
|
||||
"ignore_pricing_rule",
|
||||
"before_items_section",
|
||||
"scan_barcode",
|
||||
"set_from_warehouse",
|
||||
"last_scanned_warehouse",
|
||||
"items_col_break",
|
||||
"set_from_warehouse",
|
||||
"set_warehouse",
|
||||
"items_section",
|
||||
"items",
|
||||
@@ -1294,6 +1295,13 @@
|
||||
"hidden": 1,
|
||||
"label": "Has Unit Price Items",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1301,7 +1309,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-09 16:54:08.836106",
|
||||
"modified": "2025-07-31 17:19:40.816883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -724,7 +724,12 @@ def set_missing_values(source, target):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items")
|
||||
|
||||
def is_unit_price_row(source):
|
||||
@@ -738,6 +743,11 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
@@ -765,7 +775,8 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
"condition": lambda doc: (
|
||||
True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty)
|
||||
)
|
||||
and doc.delivered_by_supplier != 1,
|
||||
and doc.delivered_by_supplier != 1
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
},
|
||||
@@ -777,8 +788,8 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(source_name, target_doc=None):
|
||||
return get_mapped_purchase_invoice(source_name, target_doc)
|
||||
def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
return get_mapped_purchase_invoice(source_name, target_doc, args=args)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -792,7 +803,12 @@ def make_purchase_invoice_from_portal(purchase_order_name):
|
||||
frappe.response.location = "/purchase-invoices/" + doc.name
|
||||
|
||||
|
||||
def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def postprocess(source, target):
|
||||
target.flags.ignore_permissions = ignore_permissions
|
||||
set_missing_values(source, target)
|
||||
@@ -832,6 +848,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
or item_group.get("buying_cost_center")
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
fields = {
|
||||
"Purchase Order": {
|
||||
"doctype": "Purchase Invoice",
|
||||
@@ -854,7 +875,8 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe import _
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.desk.form.load import get_attachments
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Order
|
||||
from frappe.utils import get_url
|
||||
from frappe.utils.print_format import download_pdf
|
||||
from frappe.utils.user import get_user_fullname
|
||||
@@ -582,35 +583,32 @@ def get_supplier_tag():
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions = ""
|
||||
if txt:
|
||||
conditions += "and rfq.name like '%%" + txt + "%%' "
|
||||
rfq = frappe.qb.DocType("Request for Quotation")
|
||||
rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
conditions += "and rfq.transaction_date = '{}'".format(filters.get("transaction_date"))
|
||||
|
||||
rfq_data = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
distinct rfq.name, rfq.transaction_date,
|
||||
rfq.company
|
||||
from
|
||||
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
|
||||
where
|
||||
rfq.name = rfq_supplier.parent
|
||||
and rfq_supplier.supplier = %(supplier)s
|
||||
and rfq.docstatus = 1
|
||||
and rfq.company = %(company)s
|
||||
{conditions}
|
||||
order by rfq.transaction_date ASC
|
||||
limit %(page_len)s offset %(start)s """,
|
||||
{
|
||||
"page_len": page_len,
|
||||
"start": start,
|
||||
"company": filters.get("company"),
|
||||
"supplier": filters.get("supplier"),
|
||||
},
|
||||
as_dict=1,
|
||||
query = (
|
||||
frappe.qb.from_(rfq)
|
||||
.from_(rfq_supplier)
|
||||
.select(rfq.name)
|
||||
.distinct()
|
||||
.select(rfq.transaction_date, rfq.company)
|
||||
.where(
|
||||
(rfq.name == rfq_supplier.parent)
|
||||
& (rfq_supplier.supplier == filters.get("supplier"))
|
||||
& (rfq.docstatus == 1)
|
||||
& (rfq.company == filters.get("company"))
|
||||
)
|
||||
.orderby(rfq.transaction_date, order=Order.asc)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
if txt:
|
||||
query = query.where(rfq.name.like(f"%%{txt}%%"))
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
query = query.where(rfq.transaction_date == filters.get("transaction_date"))
|
||||
|
||||
rfq_data = query.run(as_dict=1)
|
||||
|
||||
return rfq_data
|
||||
|
||||
@@ -583,21 +583,27 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select distinct bo.name, bo.blanket_order_type, bo.to_date
|
||||
from `tabBlanket Order` bo, `tabBlanket Order Item` boi
|
||||
where
|
||||
boi.parent = bo.name
|
||||
and boi.item_code = {item_code}
|
||||
and bo.blanket_order_type = '{blanket_order_type}'
|
||||
and bo.company = {company}
|
||||
and bo.docstatus = 1""".format(
|
||||
item_code=frappe.db.escape(filters.get("item")),
|
||||
blanket_order_type=filters.get("blanket_order_type"),
|
||||
company=frappe.db.escape(filters.get("company")),
|
||||
bo = frappe.qb.DocType("Blanket Order")
|
||||
bo_item = frappe.qb.DocType("Blanket Order Item")
|
||||
|
||||
blanket_orders = (
|
||||
frappe.qb.from_(bo)
|
||||
.from_(bo_item)
|
||||
.select(bo.name)
|
||||
.distinct()
|
||||
.select(bo.blanket_order_type, bo.to_date)
|
||||
.where(
|
||||
(bo_item.parent == bo.name)
|
||||
& (bo_item.item_code == filters.get("item"))
|
||||
& (bo.blanket_order_type == filters.get("blanket_order_type"))
|
||||
& (bo.company == filters.get("company"))
|
||||
& (bo.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
return blanket_orders
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -615,7 +621,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
|
||||
condition += f"and tabAccount.disabled = {filters.get('disabled', 0)}"
|
||||
condition += " and tabAccount.disabled = %(disabled)s"
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select tabAccount.name from `tabAccount`
|
||||
@@ -625,7 +631,11 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
and tabAccount.`{searchfield}` LIKE %(txt)s
|
||||
{condition} {get_match_cond(doctype)}
|
||||
order by idx desc, name""",
|
||||
{"txt": "%" + txt + "%", "company": filters.get("company", "")},
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"company": filters.get("company", ""),
|
||||
"disabled": cint(filters.get("disabled", 0)),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1854,6 +1854,7 @@ def make_bundle_for_material_transfer(**kwargs):
|
||||
|
||||
row.warehouse = kwargs.warehouse
|
||||
|
||||
bundle_doc.set_incoming_rate()
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
|
||||
@@ -363,7 +363,9 @@ doc_events = {
|
||||
"erpnext.regional.create_transaction_log",
|
||||
"erpnext.regional.italy.utils.sales_invoice_on_submit",
|
||||
],
|
||||
"on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"],
|
||||
"on_cancel": [
|
||||
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
|
||||
],
|
||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||
},
|
||||
"Purchase Invoice": {
|
||||
@@ -375,9 +377,7 @@ doc_events = {
|
||||
"Payment Entry": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
||||
],
|
||||
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||
},
|
||||
"Address": {
|
||||
|
||||
@@ -420,3 +420,4 @@ erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08-
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction", 1)
|
||||
erpnext.patches.v15_0.add_company_payment_gateway_account
|
||||
erpnext.patches.v15_0.update_uae_zero_rated_fetch
|
||||
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
|
||||
|
||||
@@ -48,6 +48,7 @@ def execute():
|
||||
dunning.validate()
|
||||
|
||||
dunning.flags.ignore_validate_update_after_submit = True
|
||||
dunning.flags.ignore_links = True
|
||||
dunning.save()
|
||||
|
||||
# Reverse entries only if dunning is submitted and not resolved
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import frappe
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
|
||||
def execute():
|
||||
default_accounting_dimension()
|
||||
ADF = DocType("Accounting Dimension Filter")
|
||||
AD = DocType("Accounting Dimension")
|
||||
|
||||
accounting_dimension_filter = (
|
||||
frappe.qb.from_(ADF)
|
||||
.join(AD)
|
||||
.on(AD.document_type == ADF.accounting_dimension)
|
||||
.select(ADF.name, AD.fieldname, ADF.accounting_dimension)
|
||||
).run(as_dict=True)
|
||||
|
||||
for doc in accounting_dimension_filter:
|
||||
value = doc.fieldname or frappe.scrub(doc.accounting_dimension)
|
||||
frappe.db.set_value(
|
||||
"Accounting Dimension Filter",
|
||||
doc.name,
|
||||
"fieldname",
|
||||
value,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
|
||||
def default_accounting_dimension():
|
||||
ADF = DocType("Accounting Dimension Filter")
|
||||
for dim in ("Cost Center", "Project"):
|
||||
(
|
||||
frappe.qb.update(ADF)
|
||||
.set(ADF.fieldname, frappe.scrub(dim))
|
||||
.where(ADF.accounting_dimension == dim)
|
||||
.run()
|
||||
)
|
||||
@@ -342,12 +342,16 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_timesheet_detail_rate(timelog, currency):
|
||||
timelog_detail = frappe.db.sql(
|
||||
f"""SELECT tsd.billing_amount as billing_amount,
|
||||
ts.currency as currency FROM `tabTimesheet Detail` tsd
|
||||
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent
|
||||
WHERE tsd.name = '{timelog}'""",
|
||||
as_dict=1,
|
||||
ts = frappe.qb.DocType("Timesheet")
|
||||
ts_detail = frappe.qb.DocType("Timesheet Detail")
|
||||
|
||||
timelog_detail = (
|
||||
frappe.qb.from_(ts_detail)
|
||||
.inner_join(ts)
|
||||
.on(ts.name == ts_detail.parent)
|
||||
.select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency"))
|
||||
.where(ts_detail.name == timelog)
|
||||
.run(as_dict=1)
|
||||
)[0]
|
||||
|
||||
if timelog_detail.currency:
|
||||
|
||||
@@ -931,8 +931,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
set_default_payment(total_amount_to_pay, update_paid_amount) {
|
||||
var me = this;
|
||||
var payment_status = true;
|
||||
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
|
||||
|
||||
if (
|
||||
this.frm.doc.is_pos
|
||||
&& !cint(this.frm.skip_default_payment)
|
||||
&& (update_paid_amount===undefined || update_paid_amount)
|
||||
) {
|
||||
$.each(this.frm.doc['payments'] || [], function(index, data) {
|
||||
if(data.default && payment_status && total_amount_to_pay > 0) {
|
||||
let base_amount, amount;
|
||||
|
||||
@@ -6,6 +6,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
setup() {
|
||||
super.setup();
|
||||
let me = this;
|
||||
this.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm });
|
||||
|
||||
this.set_fields_onload_for_line_item();
|
||||
this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
@@ -473,8 +474,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
scan_barcode() {
|
||||
frappe.flags.dialog_set = false;
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
|
||||
barcode_scanner.process_scan();
|
||||
this.barcode_scanner.process_scan();
|
||||
}
|
||||
|
||||
barcode(doc, cdt, cdn) {
|
||||
@@ -923,8 +923,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
return;
|
||||
}
|
||||
|
||||
var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
|
||||
var party_name = me.frm.doc[party_type.toLowerCase()];
|
||||
var party_type, party_name;
|
||||
if( me.frm.doc.doctype == "Quotation" && me.frm.doc.quotation_to == "Customer"){
|
||||
party_type = "Customer",
|
||||
party_name = me.frm.doc.party_name
|
||||
}
|
||||
else{
|
||||
party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
|
||||
party_name = me.frm.doc[party_type.toLowerCase()];
|
||||
}
|
||||
if (party_name) {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
|
||||
@@ -12,6 +12,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
this.batch_no_field = opts.batch_no_field || "batch_no";
|
||||
this.uom_field = opts.uom_field || "uom";
|
||||
this.qty_field = opts.qty_field || "qty";
|
||||
this.warehouse_field = opts.warehouse_field || "warehouse";
|
||||
// field name on row which defines max quantity to be scanned e.g. picklist
|
||||
this.max_qty_field = opts.max_qty_field;
|
||||
// scanner won't add a new row if this flag is set.
|
||||
@@ -20,7 +21,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
this.prompt_qty = opts.prompt_qty;
|
||||
|
||||
this.items_table_name = opts.items_table_name || "items";
|
||||
this.items_table = this.frm.doc[this.items_table_name];
|
||||
|
||||
// optional sound name to play when scan either fails or passes.
|
||||
// see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds
|
||||
@@ -34,8 +34,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
// batch_no: "LOT12", // present if batch was scanned
|
||||
// serial_no: "987XYZ", // present if serial no was scanned
|
||||
// uom: "Kg", // present if barcode UOM is different from default
|
||||
// warehouse: "Store-001", // present if warehouse was found (location-first scanning)
|
||||
// }
|
||||
this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode";
|
||||
this.has_last_scanned_warehouse = frappe.meta.has_field(this.frm.doctype, "last_scanned_warehouse");
|
||||
}
|
||||
|
||||
process_scan() {
|
||||
@@ -50,14 +52,31 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
|
||||
this.scan_api_call(input, (r) => {
|
||||
const data = r && r.message;
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
this.show_alert(__("Cannot find Item with this Barcode"), "red");
|
||||
if (
|
||||
!data ||
|
||||
Object.keys(data).length === 0 ||
|
||||
(data.warehouse && !this.has_last_scanned_warehouse)
|
||||
) {
|
||||
this.show_alert(
|
||||
this.has_last_scanned_warehouse
|
||||
? __("Cannot find Item or Warehouse with this Barcode")
|
||||
: __("Cannot find Item with this Barcode"),
|
||||
"red"
|
||||
);
|
||||
this.clean_up();
|
||||
this.play_fail_sound();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle warehouse scanning
|
||||
if (data.warehouse) {
|
||||
this.handle_warehouse_scan(data);
|
||||
this.play_success_sound();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
me.update_table(data)
|
||||
.then((row) => {
|
||||
this.play_success_sound();
|
||||
@@ -77,6 +96,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
method: this.scan_api,
|
||||
args: {
|
||||
search_value: input,
|
||||
ctx: {
|
||||
set_warehouse: this.frm.doc.set_warehouse,
|
||||
company: this.frm.doc.company,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
@@ -89,11 +112,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||
frappe.flags.trigger_from_barcode_scanner = true;
|
||||
|
||||
const { item_code, barcode, batch_no, serial_no, uom } = data;
|
||||
const { item_code, barcode, batch_no, serial_no, uom, default_warehouse } = data;
|
||||
|
||||
let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode);
|
||||
const warehouse = this.has_last_scanned_warehouse
|
||||
? this.frm.doc.last_scanned_warehouse || default_warehouse
|
||||
: null;
|
||||
|
||||
this.is_new_row = false;
|
||||
let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse);
|
||||
const is_new_row = !row?.item_code;
|
||||
if (!row) {
|
||||
if (this.dont_allow_new_row) {
|
||||
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
|
||||
@@ -101,7 +127,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
this.is_new_row = true;
|
||||
|
||||
// add new row if new item/batch is scanned
|
||||
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
|
||||
@@ -120,12 +145,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
() => this.set_selector_trigger_flag(data),
|
||||
() =>
|
||||
this.set_item(row, item_code, barcode, batch_no, serial_no).then((qty) => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
this.show_scan_message(row.idx, !is_new_row, qty);
|
||||
}),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.set_serial_no(row, serial_no),
|
||||
() => this.set_batch_no(row, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.set_warehouse(row, warehouse),
|
||||
() => this.clean_up(),
|
||||
() => this.revert_selector_flag(),
|
||||
() => resolve(row),
|
||||
@@ -386,9 +412,17 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
}
|
||||
}
|
||||
|
||||
show_scan_message(idx, exist = null, qty = 1) {
|
||||
async set_warehouse(row, warehouse) {
|
||||
const warehouse_field = this.get_warehouse_field();
|
||||
|
||||
if (warehouse && frappe.meta.has_field(row.doctype, warehouse_field)) {
|
||||
await frappe.model.set_value(row.doctype, row.name, warehouse_field, warehouse);
|
||||
}
|
||||
}
|
||||
|
||||
show_scan_message(idx, is_existing_row = false, qty = 1) {
|
||||
// show new row or qty increase toast
|
||||
if (exist) {
|
||||
if (is_existing_row) {
|
||||
this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green");
|
||||
} else {
|
||||
this.show_alert(__("Row #{0}: Item added", [idx]), "green");
|
||||
@@ -404,13 +438,16 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
return is_duplicate;
|
||||
}
|
||||
|
||||
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
|
||||
get_row_to_modify_on_scan(item_code, batch_no, uom, barcode, warehouse) {
|
||||
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||
|
||||
// Check if batch is scanned and table has batch no field
|
||||
let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
|
||||
let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field);
|
||||
|
||||
const warehouse_field = this.get_warehouse_field();
|
||||
let has_warehouse_field = frappe.meta.has_field(cur_grid.doctype, warehouse_field);
|
||||
|
||||
const matching_row = (row) => {
|
||||
const item_match = row.item_code == item_code;
|
||||
const batch_match = !row[this.batch_no_field] || row[this.batch_no_field] == batch_no;
|
||||
@@ -418,20 +455,94 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
|
||||
const item_scanned = row.has_item_scanned;
|
||||
|
||||
let warehouse_match = true;
|
||||
if (has_warehouse_field) {
|
||||
if (warehouse) {
|
||||
warehouse_match = row[warehouse_field] === warehouse;
|
||||
} else {
|
||||
warehouse_match = !row[warehouse_field];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
item_match &&
|
||||
uom_match &&
|
||||
warehouse_match &&
|
||||
!item_scanned &&
|
||||
(!is_batch_no_scan || batch_match) &&
|
||||
(!check_max_qty || qty_in_limit)
|
||||
);
|
||||
};
|
||||
|
||||
return this.items_table.find(matching_row) || this.get_existing_blank_row();
|
||||
const items_table = this.frm.doc[this.items_table_name] || [];
|
||||
|
||||
return items_table.find(matching_row) || items_table.find((d) => !d.item_code);
|
||||
}
|
||||
|
||||
get_existing_blank_row() {
|
||||
return this.items_table.find((d) => !d.item_code);
|
||||
setup_last_scanned_warehouse() {
|
||||
this.frm.set_df_property("last_scanned_warehouse", "options", "Warehouse");
|
||||
this.frm.set_df_property("last_scanned_warehouse", "fieldtype", "Link");
|
||||
this.frm.set_df_property("last_scanned_warehouse", "formatter", function (value, df, options, doc) {
|
||||
const link_formatter = frappe.form.get_formatter(df.fieldtype);
|
||||
const link_value = link_formatter(value, df, options, doc);
|
||||
|
||||
if (!value) {
|
||||
return link_value;
|
||||
}
|
||||
|
||||
const clear_btn = `
|
||||
<a class="btn-clear-last-scanned-warehouse" title="${__("Clear Last Scanned Warehouse")}">
|
||||
${frappe.utils.icon("close", "xs", "es-icon")}
|
||||
</a>
|
||||
`;
|
||||
|
||||
return link_value + clear_btn;
|
||||
});
|
||||
|
||||
this.frm.$wrapper.on("click", ".btn-clear-last-scanned-warehouse", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.clear_warehouse_context();
|
||||
});
|
||||
}
|
||||
|
||||
handle_warehouse_scan(data) {
|
||||
const warehouse = data.warehouse;
|
||||
const warehouse_field = this.get_warehouse_field();
|
||||
const warehouse_field_label = frappe.meta.get_label(this.items_table_name, warehouse_field);
|
||||
|
||||
if (!this.last_scanned_warehouse_initialized) {
|
||||
this.setup_last_scanned_warehouse();
|
||||
this.last_scanned_warehouse_initialized = true;
|
||||
}
|
||||
|
||||
this.frm.set_value("last_scanned_warehouse", warehouse);
|
||||
this.show_alert(
|
||||
__("{0} will be set as the {1} in subsequently scanned items", [
|
||||
__(warehouse).bold(),
|
||||
__(warehouse_field_label).bold(),
|
||||
]),
|
||||
"green",
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
clear_warehouse_context() {
|
||||
this.frm.set_value("last_scanned_warehouse", null);
|
||||
this.show_alert(
|
||||
__(
|
||||
"The last scanned warehouse has been cleared and won't be set in the subsequently scanned items"
|
||||
),
|
||||
"blue",
|
||||
6
|
||||
);
|
||||
}
|
||||
|
||||
get_warehouse_field() {
|
||||
if (typeof this.warehouse_field === "function") {
|
||||
return this.warehouse_field(this.frm.doc);
|
||||
}
|
||||
return this.warehouse_field;
|
||||
}
|
||||
|
||||
play_success_sound() {
|
||||
|
||||
@@ -593,3 +593,11 @@ body[data-route="pos"] {
|
||||
.frappe-control[data-fieldname="other_charges_calculation"] .ql-editor {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.btn-clear-last-scanned-warehouse {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ class ProductBundle(Document):
|
||||
def validate(self):
|
||||
self.validate_main_item()
|
||||
self.validate_child_items()
|
||||
self.validate_child_items_qty_non_zero()
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
|
||||
validate_uom_is_integer(self, "uom", "qty")
|
||||
@@ -88,6 +89,15 @@ class ProductBundle(Document):
|
||||
).format(item.idx, frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
def validate_child_items_qty_non_zero(self):
|
||||
for item in self.items:
|
||||
if item.qty <= 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Quantity cannot be a non-positive number. Please increase the quantity or remove the Item {1}"
|
||||
).format(item.idx, frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"items_section",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"items",
|
||||
"sec_break23",
|
||||
"total_qty",
|
||||
@@ -1094,13 +1095,20 @@
|
||||
"hidden": 1,
|
||||
"label": "Has Unit Price Items",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-27 16:04:39.208077",
|
||||
"modified": "2025-07-31 17:23:48.875382",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
@@ -1199,4 +1207,4 @@
|
||||
"states": [],
|
||||
"timeline_field": "party_name",
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
@@ -347,7 +349,7 @@ def get_list_context(context=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_order(source_name: str, target_doc=None):
|
||||
def make_sales_order(source_name: str, target_doc=None, args=None):
|
||||
if not frappe.db.get_singles_value(
|
||||
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
|
||||
):
|
||||
@@ -359,10 +361,15 @@ def make_sales_order(source_name: str, target_doc=None):
|
||||
):
|
||||
frappe.throw(_("Validity period of this quotation has ended."))
|
||||
|
||||
return _make_sales_order(source_name, target_doc)
|
||||
return _make_sales_order(source_name, target_doc, args=args)
|
||||
|
||||
|
||||
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
customer = _make_customer(source_name, ignore_permissions)
|
||||
ordered_items = get_ordered_items(source_name)
|
||||
|
||||
@@ -430,6 +437,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
# Simple row
|
||||
return True
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Quotation",
|
||||
source_name,
|
||||
@@ -439,7 +451,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
"doctype": "Sales Order Item",
|
||||
"field_map": {"parent": "prevdoc_docname", "name": "quotation_item"},
|
||||
"postprocess": update_item,
|
||||
"condition": can_map_row,
|
||||
"condition": lambda d: can_map_row(d) and select_item(d),
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||
@@ -476,11 +488,16 @@ def set_expired_status():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(source_name, target_doc=None):
|
||||
return _make_sales_invoice(source_name, target_doc)
|
||||
def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
return _make_sales_invoice(source_name, target_doc, args=args)
|
||||
|
||||
|
||||
def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
customer = _make_customer(source_name, ignore_permissions)
|
||||
|
||||
def set_missing_values(source, target):
|
||||
@@ -496,6 +513,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.cost_center = None
|
||||
target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Quotation",
|
||||
source_name,
|
||||
@@ -504,7 +526,7 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
"Quotation Item": {
|
||||
"doctype": "Sales Invoice Item",
|
||||
"postprocess": update_item,
|
||||
"condition": lambda row: not row.is_alternative,
|
||||
"condition": lambda row: not row.is_alternative and select_item(row),
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
|
||||
|
||||
@@ -738,8 +738,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
if (internal) {
|
||||
let button_label =
|
||||
me.frm.doc.company === me.frm.doc.represents_company
|
||||
? "Internal Purchase Order"
|
||||
: "Inter Company Purchase Order";
|
||||
? __("Internal Purchase Order")
|
||||
: __("Inter Company Purchase Order");
|
||||
|
||||
me.frm.add_custom_button(
|
||||
button_label,
|
||||
@@ -793,6 +793,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
docstatus: 1,
|
||||
status: ["!=", "Lost"],
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "rate", "amount"],
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"column_break_28",
|
||||
"set_warehouse",
|
||||
"reserve_stock",
|
||||
@@ -1657,6 +1658,13 @@
|
||||
"hidden": 1,
|
||||
"label": "Has Unit Price Items",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
|
||||
@@ -977,6 +977,11 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
||||
def is_unit_price_row(source):
|
||||
return has_unit_price_items and source.qty == 0
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = kwargs.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
def set_missing_values(source, target):
|
||||
if kwargs.get("ignore_pricing_rule"):
|
||||
# Skip pricing rule when the dn is creating from the pick list
|
||||
@@ -1042,7 +1047,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
||||
"name": "so_detail",
|
||||
"parent": "against_sales_order",
|
||||
},
|
||||
"condition": condition,
|
||||
"condition": lambda d: condition(d) and select_item(d),
|
||||
"postprocess": update_item,
|
||||
}
|
||||
|
||||
@@ -1098,7 +1103,12 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
# 0 qty is accepted, as the qty is uncertain for some items
|
||||
has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items")
|
||||
|
||||
@@ -1158,6 +1168,11 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
if cost_center:
|
||||
target.cost_center = cost_center
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Sales Order",
|
||||
source_name,
|
||||
@@ -1182,7 +1197,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False):
|
||||
True
|
||||
if is_unit_price_row(doc)
|
||||
else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)))
|
||||
),
|
||||
)
|
||||
and select_item(doc),
|
||||
},
|
||||
"Sales Taxes and Charges": {
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
|
||||
@@ -342,7 +342,6 @@ erpnext.PointOfSale.Payment = class {
|
||||
}
|
||||
|
||||
render_payment_section() {
|
||||
this.remove_grand_total_from_default_mop();
|
||||
this.render_payment_mode_dom();
|
||||
this.make_invoice_fields_control();
|
||||
this.update_totals_section();
|
||||
|
||||
@@ -480,14 +480,19 @@ def install_defaults(args=None): # nosemgrep
|
||||
create_bank_account(args)
|
||||
|
||||
|
||||
def set_global_defaults(args):
|
||||
def set_global_defaults(kwargs):
|
||||
global_defaults = frappe.get_doc("Global Defaults", "Global Defaults")
|
||||
company = frappe.db.get_value(
|
||||
"Company",
|
||||
{"company_name": kwargs.get("company_name")},
|
||||
"name",
|
||||
)
|
||||
|
||||
global_defaults.update(
|
||||
{
|
||||
"default_currency": args.get("currency"),
|
||||
"default_company": args.get("company_name"),
|
||||
"country": args.get("country"),
|
||||
"default_currency": kwargs.get("currency"),
|
||||
"default_company": company,
|
||||
"country": kwargs.get("country"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -182,6 +182,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
||||
company: me.frm.doc.company,
|
||||
project: me.frm.doc.project || undefined,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "delivered_qty"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -231,6 +234,9 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
||||
},
|
||||
get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query",
|
||||
size: "extra-large",
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "locations",
|
||||
child_columns: ["item_code", "item_name", "stock_qty", "delivered_qty"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"items_section",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"col_break_warehouse",
|
||||
"set_warehouse",
|
||||
"set_target_warehouse",
|
||||
@@ -1390,6 +1391,13 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
@@ -824,6 +826,11 @@ def get_returned_qty_map(delivery_note):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
doc = frappe.get_doc("Delivery Note", source_name)
|
||||
|
||||
to_make_invoice_qty_map = {}
|
||||
@@ -875,6 +882,11 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
|
||||
return pending_qty
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Delivery Note",
|
||||
source_name,
|
||||
@@ -897,6 +909,7 @@ def make_sales_invoice(source_name, target_doc=None, args=None):
|
||||
"filter": lambda d: get_pending_qty(d) <= 0
|
||||
if not doc.get("is_return")
|
||||
else get_pending_qty(d) > 0,
|
||||
"condition": select_item,
|
||||
},
|
||||
"Sales Taxes and Charges": {
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
|
||||
@@ -674,13 +674,13 @@ def prepare_data_for_internal_transfer():
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
customer = create_internal_customer(
|
||||
"_Test Internal Customer 3",
|
||||
"_Test Internal Customer 2",
|
||||
company,
|
||||
company,
|
||||
)
|
||||
|
||||
supplier = create_internal_supplier(
|
||||
"_Test Internal Supplier 3",
|
||||
"_Test Internal Supplier 2",
|
||||
company,
|
||||
company,
|
||||
)
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
"amended_from",
|
||||
"warehouse_section",
|
||||
"scan_barcode",
|
||||
"column_break_13",
|
||||
"set_from_warehouse",
|
||||
"last_scanned_warehouse",
|
||||
"column_break5",
|
||||
"set_from_warehouse",
|
||||
"set_warehouse",
|
||||
"items_section",
|
||||
"items",
|
||||
@@ -350,22 +350,25 @@
|
||||
"fieldname": "column_break_35",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "buying_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-ticket",
|
||||
"idx": 70,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-28 15:13:49.000037",
|
||||
"modified": "2025-07-31 17:19:01.166208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request",
|
||||
|
||||
@@ -11,6 +11,7 @@ import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
|
||||
|
||||
@@ -555,39 +556,44 @@ def get_items_based_on_default_supplier(supplier):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters):
|
||||
conditions = ""
|
||||
if txt:
|
||||
conditions += "and mr.name like '%%" + txt + "%%' "
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
date = filters.get("transaction_date")[1]
|
||||
conditions += f"and mr.transaction_date between '{date[0]}' and '{date[1]}' "
|
||||
|
||||
supplier = filters.get("supplier")
|
||||
supplier_items = get_items_based_on_default_supplier(supplier)
|
||||
|
||||
if not supplier_items:
|
||||
frappe.throw(_("{0} is not the default supplier for any items.").format(supplier))
|
||||
|
||||
material_requests = frappe.db.sql(
|
||||
"""select distinct mr.name, transaction_date,company
|
||||
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
||||
where mr.name = mr_item.parent
|
||||
and mr_item.item_code in ({})
|
||||
and mr.material_request_type = 'Purchase'
|
||||
and mr.per_ordered < 99.99
|
||||
and mr.docstatus = 1
|
||||
and mr.status != 'Stopped'
|
||||
and mr.company = %s
|
||||
{}
|
||||
order by mr_item.item_code ASC
|
||||
limit {} offset {} """.format(
|
||||
", ".join(["%s"] * len(supplier_items)), conditions, cint(page_len), cint(start)
|
||||
),
|
||||
(*tuple(supplier_items), filters.get("company")),
|
||||
as_dict=1,
|
||||
mr = frappe.qb.DocType("Material Request")
|
||||
mr_item = frappe.qb.DocType("Material Request Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(mr)
|
||||
.from_(mr_item)
|
||||
.select(mr.name)
|
||||
.distinct()
|
||||
.select(mr.transaction_date, mr.company)
|
||||
.where(
|
||||
(mr.name == mr_item.parent)
|
||||
& (mr_item.item_code.isin(supplier_items))
|
||||
& (mr.material_request_type == "Purchase")
|
||||
& (mr.per_ordered < 99.99)
|
||||
& (mr.docstatus == 1)
|
||||
& (mr.status != "Stopped")
|
||||
& (mr.company == filters.get("company"))
|
||||
)
|
||||
.orderby(mr_item.item_code, order=Order.asc)
|
||||
.limit(cint(page_len))
|
||||
.offset(cint(start))
|
||||
)
|
||||
|
||||
if txt:
|
||||
query = query.where(mr.name.like(f"%%{txt}%%"))
|
||||
|
||||
if filters.get("transaction_date"):
|
||||
date = filters.get("transaction_date")[1]
|
||||
query = query.where(mr.transaction_date[date[0] : date[1]])
|
||||
|
||||
material_requests = query.run(as_dict=True)
|
||||
|
||||
return material_requests
|
||||
|
||||
|
||||
|
||||
@@ -1252,11 +1252,16 @@ def create_dn_wo_so(pick_list, delivery_note=None):
|
||||
@frappe.whitelist()
|
||||
def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
|
||||
"""Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
if isinstance(kwargs, str):
|
||||
kwargs = json.loads(kwargs)
|
||||
|
||||
pick_list = frappe.get_doc("Pick List", source_name)
|
||||
validate_item_locations(pick_list)
|
||||
|
||||
sales_order_arg = kwargs.get("sales_order") if kwargs else None
|
||||
customer_arg = kwargs.get("customer") if kwargs else None
|
||||
sales_order_arg = kwargs.get("sales_order")
|
||||
customer_arg = kwargs.get("customer")
|
||||
|
||||
if sales_order_arg:
|
||||
sales_orders = {sales_order_arg}
|
||||
@@ -1270,7 +1275,7 @@ def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
|
||||
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc, kwargs=kwargs)
|
||||
|
||||
if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
|
||||
if isinstance(delivery_note, str):
|
||||
@@ -1296,10 +1301,15 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
|
||||
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None):
|
||||
if not sales_order_list:
|
||||
return delivery_note
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = kwargs.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
item_table_mapper = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
@@ -1307,7 +1317,9 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
|
||||
"name": "so_detail",
|
||||
"parent": "against_sales_order",
|
||||
},
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
|
||||
and doc.delivered_by_supplier != 1
|
||||
and select_item(doc),
|
||||
}
|
||||
|
||||
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
|
||||
|
||||
@@ -150,7 +150,11 @@ frappe.ui.form.on("Purchase Receipt", {
|
||||
docstatus: 1,
|
||||
per_received: ["<", 100],
|
||||
company: frm.doc.company,
|
||||
update_stock: 0,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "received_qty"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -255,6 +259,9 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
per_received: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "received_qty"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"column_break_31",
|
||||
"set_warehouse",
|
||||
"set_from_warehouse",
|
||||
@@ -1285,6 +1286,13 @@
|
||||
"label": "Dispatch Address",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
@@ -1225,6 +1227,11 @@ def get_item_wise_returned_qty(pr_doc):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
from erpnext.accounts.party import get_payment_terms_template
|
||||
|
||||
doc = frappe.get_doc("Purchase Receipt", source_name)
|
||||
@@ -1279,6 +1286,11 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
|
||||
return pending_qty, returned_qty
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Purchase Receipt",
|
||||
source_name,
|
||||
@@ -1308,9 +1320,10 @@ def make_purchase_invoice(source_name, target_doc=None, args=None):
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"filter": lambda d: get_pending_qty(d)[0] <= 0
|
||||
if not doc.get("is_return")
|
||||
else get_pending_qty(d)[0] > 0,
|
||||
"filter": lambda d: (
|
||||
get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0
|
||||
)
|
||||
and select_item(d),
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
|
||||
@@ -131,7 +131,12 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
|
||||
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
|
||||
|
||||
if not rules:
|
||||
warehouse = source_warehouse or item.get("warehouse")
|
||||
warehouse = (
|
||||
(source_warehouse or item.get("warehouse"))
|
||||
if not item.get("t_warehouse")
|
||||
else item.get("t_warehouse")
|
||||
)
|
||||
|
||||
if at_capacity:
|
||||
# rules available, but no free space
|
||||
items_not_accomodated.append([item_code, pending_qty])
|
||||
|
||||
@@ -1003,6 +1003,13 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
setup() {
|
||||
var me = this;
|
||||
|
||||
this.barcode_scanner = new erpnext.utils.BarcodeScanner({
|
||||
frm: this.frm,
|
||||
warehouse_field: (doc) => {
|
||||
return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse";
|
||||
},
|
||||
});
|
||||
|
||||
this.setup_posting_date_time_check();
|
||||
|
||||
this.frm.fields_dict.bom_no.get_query = function () {
|
||||
@@ -1130,8 +1137,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
|
||||
scan_barcode() {
|
||||
frappe.flags.dialog_set = false;
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: this.frm });
|
||||
barcode_scanner.process_scan();
|
||||
this.barcode_scanner.process_scan();
|
||||
}
|
||||
|
||||
on_submit() {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"target_address_display",
|
||||
"sb0",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"items_section",
|
||||
"items",
|
||||
"get_stock_and_rate",
|
||||
@@ -691,6 +692,13 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
|
||||
@@ -2020,6 +2020,70 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
self.assertEqual(se.items[0].basic_rate, 300)
|
||||
|
||||
def test_batch_item_additional_cost_for_material_transfer_entry(self):
|
||||
item_code = "_Test Batch Item Additional Cost MTE"
|
||||
make_item(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_naming_series": "BT-MTE.#####",
|
||||
},
|
||||
)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=2,
|
||||
basic_rate=100,
|
||||
use_serial_batch_fields=1,
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
source="_Test Warehouse - _TC",
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
qty=2,
|
||||
purpose="Material Transfer",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
se.append(
|
||||
"additional_costs",
|
||||
{
|
||||
"cost_center": "Main - _TC",
|
||||
"amount": 50,
|
||||
"expense_account": "Stock Adjustment - _TC",
|
||||
"description": "Test Additional Cost",
|
||||
},
|
||||
)
|
||||
se.save()
|
||||
self.assertEqual(se.additional_costs[0].amount, 50)
|
||||
self.assertEqual(se.items[0].basic_rate, 100)
|
||||
self.assertEqual(se.items[0].valuation_rate, 125)
|
||||
|
||||
se.submit()
|
||||
self.assertEqual(se.items[0].basic_rate, 100)
|
||||
self.assertEqual(se.items[0].valuation_rate, 125)
|
||||
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse 1 - _TC",
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": se.name,
|
||||
},
|
||||
"incoming_rate",
|
||||
)
|
||||
|
||||
self.assertEqual(incoming_rate, 125.0)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -7,6 +7,7 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
frappe.ui.form.on("Stock Reconciliation", {
|
||||
setup(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
frm.barcode_scanner = new erpnext.utils.BarcodeScanner({ frm });
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
@@ -96,8 +97,7 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
},
|
||||
|
||||
scan_barcode: function (frm) {
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
|
||||
barcode_scanner.process_scan();
|
||||
frm.barcode_scanner.process_scan();
|
||||
},
|
||||
|
||||
scan_mode: function (frm) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"set_warehouse",
|
||||
"section_break_22",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"column_break_12",
|
||||
"scan_mode",
|
||||
"sb9",
|
||||
@@ -178,6 +179,13 @@
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-upload-alt",
|
||||
|
||||
@@ -185,7 +185,21 @@ class SerialBatchBundle:
|
||||
}
|
||||
|
||||
if self.sle.actual_qty < 0 and self.is_material_transfer():
|
||||
values_to_update["valuation_rate"] = flt(sn_doc.avg_rate)
|
||||
basic_rate = flt(sn_doc.avg_rate)
|
||||
ste_detail = frappe.db.get_value(
|
||||
"Stock Entry Detail",
|
||||
self.sle.voucher_detail_no,
|
||||
["additional_cost", "transfer_qty"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
additional_cost = 0.0
|
||||
|
||||
if ste_detail:
|
||||
additional_cost = flt(ste_detail.additional_cost) / flt(ste_detail.transfer_qty)
|
||||
|
||||
values_to_update["basic_rate"] = basic_rate
|
||||
values_to_update["valuation_rate"] = basic_rate + additional_cost
|
||||
|
||||
if not frappe.db.get_single_value(
|
||||
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"
|
||||
|
||||
@@ -81,3 +81,44 @@ class TestStockUtilities(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(serial_scan["serial_no"], serial.name)
|
||||
self.assertEqual(serial_scan["has_batch_no"], 0)
|
||||
self.assertEqual(serial_scan["has_serial_no"], 1)
|
||||
|
||||
def test_barcode_scanning_of_warehouse(self):
|
||||
warehouse = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Warehouse",
|
||||
"warehouse_name": "Test Warehouse for Barcode",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
warehouse_2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Warehouse",
|
||||
"warehouse_name": "Test Warehouse for Barcode 2",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
warehouse_scan = scan_barcode(warehouse.name)
|
||||
self.assertEqual(warehouse_scan["warehouse"], warehouse.name)
|
||||
|
||||
item_with_warehouse = self.make_item(
|
||||
properties={
|
||||
"item_defaults": [{"company": "_Test Company", "default_warehouse": warehouse.name}],
|
||||
"barcodes": [{"barcode": "w12345"}],
|
||||
}
|
||||
)
|
||||
|
||||
item_scan = scan_barcode("w12345")
|
||||
self.assertEqual(item_scan["item_code"], item_with_warehouse.name)
|
||||
self.assertEqual(item_scan.get("default_warehouse"), None)
|
||||
|
||||
ctx = {"company": "_Test Company"}
|
||||
item_scan_with_ctx = scan_barcode("w12345", ctx=ctx)
|
||||
self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name)
|
||||
self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse.name)
|
||||
|
||||
ctx = {"company": "_Test Company", "set_warehouse": warehouse_2.name}
|
||||
item_scan_with_ctx = scan_barcode("w12345", ctx=ctx)
|
||||
self.assertEqual(item_scan_with_ctx["item_code"], item_with_warehouse.name)
|
||||
self.assertEqual(item_scan_with_ctx["default_warehouse"], warehouse_2.name)
|
||||
|
||||
@@ -126,8 +126,9 @@ def get_stock_balance(
|
||||
extra_cond = ""
|
||||
if inventory_dimensions_dict:
|
||||
for field, value in inventory_dimensions_dict.items():
|
||||
column = frappe.utils.sanitize_column(field)
|
||||
args[field] = value
|
||||
extra_cond += f" and {field} = %({field})s"
|
||||
extra_cond += f" and {column} = %({field})s"
|
||||
|
||||
last_entry = get_previous_sle(args, extra_cond=extra_cond)
|
||||
|
||||
@@ -584,13 +585,24 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def scan_barcode(search_value: str) -> BarcodeScanResult:
|
||||
def scan_barcode(search_value: str, ctx: dict | str | None = None) -> BarcodeScanResult:
|
||||
def set_cache(data: BarcodeScanResult):
|
||||
frappe.cache().set_value(f"erpnext:barcode_scan:{search_value}", data, expires_in_sec=120)
|
||||
_update_item_info(data, ctx)
|
||||
|
||||
def get_cache() -> BarcodeScanResult | None:
|
||||
if data := frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}"):
|
||||
return data
|
||||
data = frappe.cache().get_value(f"erpnext:barcode_scan:{search_value}")
|
||||
if not data:
|
||||
return
|
||||
|
||||
_update_item_info(data, ctx)
|
||||
return data
|
||||
|
||||
if ctx is None:
|
||||
ctx = frappe._dict()
|
||||
|
||||
else:
|
||||
ctx = frappe.parse_json(ctx)
|
||||
|
||||
if scan_data := get_cache():
|
||||
return scan_data
|
||||
@@ -603,7 +615,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
|
||||
as_dict=True,
|
||||
)
|
||||
if barcode_data:
|
||||
_update_item_info(barcode_data)
|
||||
set_cache(barcode_data)
|
||||
return barcode_data
|
||||
|
||||
@@ -615,7 +626,6 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
|
||||
as_dict=True,
|
||||
)
|
||||
if serial_no_data:
|
||||
_update_item_info(serial_no_data)
|
||||
set_cache(serial_no_data)
|
||||
return serial_no_data
|
||||
|
||||
@@ -634,22 +644,36 @@ def scan_barcode(search_value: str) -> BarcodeScanResult:
|
||||
).format(search_value, batch_no_data.item_code)
|
||||
)
|
||||
|
||||
_update_item_info(batch_no_data)
|
||||
set_cache(batch_no_data)
|
||||
return batch_no_data
|
||||
|
||||
warehouse = frappe.get_cached_value("Warehouse", search_value, ("name", "disabled"), as_dict=True)
|
||||
if warehouse and not warehouse.disabled:
|
||||
warehouse_data = {"warehouse": warehouse.name}
|
||||
set_cache(warehouse_data)
|
||||
return warehouse_data
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _update_item_info(scan_result: dict[str, str | None]) -> dict[str, str | None]:
|
||||
if item_code := scan_result.get("item_code"):
|
||||
if item_info := frappe.get_cached_value(
|
||||
"Item",
|
||||
item_code,
|
||||
["has_batch_no", "has_serial_no"],
|
||||
as_dict=True,
|
||||
):
|
||||
scan_result.update(item_info)
|
||||
def _update_item_info(scan_result: dict[str, str | None], ctx: dict | None = None) -> dict[str, str | None]:
|
||||
from erpnext.stock.get_item_details import get_item_warehouse
|
||||
|
||||
item_code = scan_result.get("item_code")
|
||||
if not item_code:
|
||||
return scan_result
|
||||
|
||||
if item_info := frappe.get_cached_value(
|
||||
"Item",
|
||||
item_code,
|
||||
("has_batch_no", "has_serial_no"),
|
||||
as_dict=True,
|
||||
):
|
||||
scan_result.update(item_info)
|
||||
|
||||
if ctx and (warehouse := get_item_warehouse(frappe._dict(name=item_code), ctx, overwrite_warehouse=True)):
|
||||
scan_result["default_warehouse"] = warehouse
|
||||
|
||||
return scan_result
|
||||
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"description": "Time at which materials were received",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
|
||||
Reference in New Issue
Block a user