Merge branch 'develop' into st31369

This commit is contained in:
Mihir Kandoi
2025-02-19 16:04:26 +05:30
committed by GitHub
71 changed files with 6560 additions and 5948 deletions

View File

@@ -123,3 +123,20 @@ class TestGLEntry(IntegrationTestCase):
str(e),
"Party Type and Party can only be set for Receivable / Payable account_Test Account Cost for Goods Sold - _TC",
)
def test_validate_account_party_type_shareholder(self):
jv = make_journal_entry(
"Opening Balance Equity - _TC",
"Cash - _TC",
100,
"_Test Cost Center - _TC",
save=False,
submit=False,
)
for row in jv.accounts:
row.party_type = "Shareholder"
break
jv.save().submit()
self.assertEqual(1, jv.docstatus)

View File

@@ -374,7 +374,6 @@ frappe.ui.form.on("Payment Entry", {
frm.set_df_property("total_allocated_amount", "options", currency_field);
frm.set_df_property("unallocated_amount", "options", currency_field);
frm.set_df_property("total_taxes_and_charges", "options", currency_field);
frm.set_df_property("party_balance", "options", currency_field);
frm.set_currency_labels(
["total_amount", "outstanding_amount", "allocated_amount"],
@@ -422,15 +421,7 @@ frappe.ui.form.on("Payment Entry", {
if (frm.doc.payment_type == "Internal Transfer") {
$.each(
[
"party",
"party_type",
"party_balance",
"paid_from",
"paid_to",
"references",
"total_allocated_amount",
],
["party", "party_type", "paid_from", "paid_to", "references", "total_allocated_amount"],
function (i, field) {
frm.set_value(field, null);
}
@@ -478,13 +469,10 @@ frappe.ui.form.on("Payment Entry", {
$.each(
[
"party",
"party_balance",
"paid_from",
"paid_to",
"paid_from_account_currency",
"paid_from_account_balance",
"paid_to_account_currency",
"paid_to_account_balance",
"references",
"total_allocated_amount",
],
@@ -529,17 +517,14 @@ frappe.ui.form.on("Payment Entry", {
"paid_from_account_currency",
r.message.party_account_currency
);
frm.set_value("paid_from_account_balance", r.message.account_balance);
} else if (frm.doc.payment_type == "Pay") {
frm.set_value("paid_to", r.message.party_account);
frm.set_value(
"paid_to_account_currency",
r.message.party_account_currency
);
frm.set_value("paid_to_account_balance", r.message.account_balance);
}
},
() => frm.set_value("party_balance", r.message.party_balance),
() => frm.set_value("party_name", r.message.party_name),
() => frm.clear_table("references"),
() => frm.events.hide_unhide_fields(frm),
@@ -591,7 +576,6 @@ frappe.ui.form.on("Payment Entry", {
frm,
frm.doc.paid_from,
"paid_from_account_currency",
"paid_from_account_balance",
function (frm) {
if (frm.doc.payment_type == "Pay") {
frm.events.paid_amount(frm);
@@ -607,7 +591,6 @@ frappe.ui.form.on("Payment Entry", {
frm,
frm.doc.paid_to,
"paid_to_account_currency",
"paid_to_account_balance",
function (frm) {
if (frm.doc.payment_type == "Receive") {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
@@ -623,13 +606,7 @@ frappe.ui.form.on("Payment Entry", {
);
},
set_account_currency_and_balance: function (
frm,
account,
currency_field,
balance_field,
callback_function
) {
set_account_currency_and_balance: function (frm, account, currency_field, callback_function) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) {
frappe.call({
@@ -644,8 +621,6 @@ frappe.ui.form.on("Payment Entry", {
frappe.run_serially([
() => frm.set_value(currency_field, r.message["account_currency"]),
() => {
frm.set_value(balance_field, r.message["account_balance"]);
if (
frm.doc.payment_type == "Receive" &&
currency_field == "paid_to_account_currency"
@@ -1684,37 +1659,6 @@ frappe.ui.form.on("Payment Entry", {
return current_tax_amount;
},
cost_center: function (frm) {
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center,
},
callback: function (r, rt) {
if (r.message) {
frappe.run_serially([
() => {
frm.set_value(
"paid_from_account_balance",
r.message.paid_from_account_balance
);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
},
]);
}
},
});
}
},
after_save: function (frm) {
const { matched_payment_requests } = frappe.last_response;
if (!matched_payment_requests) return;

View File

@@ -28,16 +28,13 @@
"contact_person",
"contact_email",
"payment_accounts_section",
"party_balance",
"paid_from",
"paid_from_account_type",
"paid_from_account_currency",
"paid_from_account_balance",
"column_break_18",
"paid_to",
"paid_to_account_type",
"paid_to_account_currency",
"paid_to_account_balance",
"payment_amounts_section",
"paid_amount",
"paid_amount_after_tax",
@@ -223,16 +220,6 @@
"fieldtype": "Section Break",
"label": "Accounts"
},
{
"allow_on_submit": 1,
"depends_on": "party",
"fieldname": "party_balance",
"fieldtype": "Currency",
"label": "Party Balance",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"bold": 1,
"depends_on": "eval:(in_list([\"Internal Transfer\", \"Pay\"], doc.payment_type) || doc.party)",
@@ -254,16 +241,6 @@
"read_only": 1,
"reqd": 1
},
{
"allow_on_submit": 1,
"depends_on": "paid_from",
"fieldname": "paid_from_account_balance",
"fieldtype": "Currency",
"label": "Account Balance (From)",
"options": "paid_from_account_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
@@ -288,16 +265,6 @@
"read_only": 1,
"reqd": 1
},
{
"allow_on_submit": 1,
"depends_on": "paid_to",
"fieldname": "paid_to_account_balance",
"fieldtype": "Currency",
"label": "Account Balance (To)",
"options": "paid_to_account_currency",
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval:(doc.paid_to && doc.paid_from)",
"fieldname": "payment_amounts_section",
@@ -810,7 +777,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-01-31 17:27:28.555246",
"modified": "2025-01-31 11:24:58.076393",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -856,4 +823,4 @@
"states": [],
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -108,15 +108,12 @@ class PaymentEntry(AccountsController):
paid_amount: DF.Currency
paid_amount_after_tax: DF.Currency
paid_from: DF.Link
paid_from_account_balance: DF.Currency
paid_from_account_currency: DF.Link
paid_from_account_type: DF.Data | None
paid_to: DF.Link
paid_to_account_balance: DF.Currency
paid_to_account_currency: DF.Link
paid_to_account_type: DF.Data | None
party: DF.DynamicLink | None
party_balance: DF.Currency
party_bank_account: DF.Link | None
party_name: DF.Data | None
party_type: DF.Link | None
@@ -506,7 +503,6 @@ class PaymentEntry(AccountsController):
if self.payment_type == "Internal Transfer":
for field in (
"party",
"party_balance",
"total_allocated_amount",
"base_total_allocated_amount",
"unallocated_amount",
@@ -534,25 +530,19 @@ class PaymentEntry(AccountsController):
)
else:
complete_contact_details(self)
if not self.party_balance:
self.party_balance = get_balance_on(
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
)
if not self.party_account:
party_account = get_party_account(self.party_type, self.party, self.company)
self.set(self.party_account_field, party_account)
self.party_account = party_account
if self.paid_from and not self.paid_from_account_currency and not self.paid_from_account_balance:
if self.paid_from and not self.paid_from_account_currency:
acc = get_account_details(self.paid_from, self.posting_date, self.cost_center)
self.paid_from_account_currency = acc.account_currency
self.paid_from_account_balance = acc.account_balance
if self.paid_to and not self.paid_to_account_currency and not self.paid_to_account_balance:
if self.paid_to and not self.paid_to_account_currency:
acc = get_account_details(self.paid_to, self.posting_date, self.cost_center)
self.paid_to_account_currency = acc.account_currency
self.paid_to_account_balance = acc.account_balance
self.party_account_currency = (
self.paid_from_account_currency
@@ -2721,9 +2711,7 @@ def get_party_details(company, party_type, party, date, cost_center=None):
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(
party_type=party_type, party=party, company=company, cost_center=cost_center
)
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party)
@@ -2732,7 +2720,6 @@ def get_party_details(company, party_type, party, date, cost_center=None):
"party_account": party_account,
"party_name": party_name,
"party_account_currency": account_currency,
"party_balance": party_balance,
"account_balance": account_balance,
"party_bank_account": party_bank_account,
"bank_account": bank_account,
@@ -3558,19 +3545,6 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
return paid_amount[0][0] if paid_amount else 0
@frappe.whitelist()
def get_party_and_account_balance(
company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
):
return frappe._dict(
{
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
}
)
@frappe.whitelist()
def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc

View File

@@ -781,7 +781,10 @@ def get_existing_paid_amount(doctype, name):
frappe.qb.from_(PL)
.left_join(PER)
.on(
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
(PL.against_voucher_type == PER.reference_doctype)
& (PL.against_voucher_no == PER.reference_name)
& (PL.voucher_type == PER.parenttype)
& (PL.voucher_no == PER.parent)
)
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
.where(PL.against_voucher_type.eq(doctype))

View File

@@ -709,6 +709,45 @@ class TestPaymentRequest(IntegrationTestCase):
self.assertEqual(pr.grand_total, si.outstanding_amount)
def test_partial_paid_invoice_with_more_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
pi.submit()
pi_1 = make_purchase_invoice(currency="INR", qty=1, rate=300)
pi_1.submit()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1)
pr.grand_total = 200
pr.submit()
pr.create_payment_entry()
pr_1 = make_payment_request(
dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1
)
pr_1.grand_total = 200
pr_1.submit()
pr_1.create_payment_entry()
pe = get_payment_entry(dt="Purchase Invoice", dn=pi.name)
pe.paid_amount = 200
pe.references[0].reference_doctype = pi.doctype
pe.references[0].reference_name = pi.name
pe.references[0].grand_total = pi.grand_total
pe.references[0].outstanding_amount = pi.outstanding_amount
pe.references[0].allocated_amount = 100
pe.append(
"references",
{
"reference_doctype": pi_1.doctype,
"reference_name": pi_1.name,
"grand_total": pi_1.grand_total,
"outstanding_amount": pi_1.outstanding_amount,
"allocated_amount": 100,
},
)
pr_2 = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
pi.load_from_db()
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)

View File

@@ -39,10 +39,12 @@ class TestPOSClosingEntry(IntegrationTestCase):
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry)
@@ -68,6 +70,7 @@ class TestPOSClosingEntry(IntegrationTestCase):
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv.save()
pos_inv.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry)
@@ -86,10 +89,12 @@ class TestPOSClosingEntry(IntegrationTestCase):
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit()
# make return entry of pos_inv2
@@ -111,10 +116,12 @@ class TestPOSClosingEntry(IntegrationTestCase):
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit()
pcv_doc = make_closing_entry_from_opening(opening_entry)
@@ -165,6 +172,7 @@ class TestPOSClosingEntry(IntegrationTestCase):
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
pos_inv1.submit()
# if in between a mandatory accounting dimension is added to the POS Profile then
@@ -226,6 +234,7 @@ class TestPOSClosingEntry(IntegrationTestCase):
do_not_submit=True,
)
pos_inv.payments[0].amount = pos_inv.grand_total
pos_inv.save()
pos_inv.submit()
pos_inv2 = create_pos_invoice(
item_code=item_code,
@@ -236,6 +245,7 @@ class TestPOSClosingEntry(IntegrationTestCase):
do_not_submit=True,
)
pos_inv2.payments[0].amount = pos_inv2.grand_total
pos_inv2.save()
pos_inv2.submit()
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)

View File

@@ -20,6 +20,10 @@ from erpnext.controllers.queries import item_query as _item_query
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class PartialPaymentValidationError(frappe.ValidationError):
pass
class POSInvoice(SalesInvoice):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -210,6 +214,7 @@ class POSInvoice(SalesInvoice):
self.validate_payment_amount()
self.validate_loyalty_transaction()
self.validate_company_with_pos_company()
self.validate_full_payment()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -484,6 +489,20 @@ class POSInvoice(SalesInvoice):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
validate_loyalty_points(self, self.loyalty_points)
def validate_full_payment(self):
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
if self.docstatus == 1:
if self.is_return and self.paid_amount != invoice_total:
frappe.throw(
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
)
if self.paid_amount < invoice_total:
frappe.throw(
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
)
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get("amended_from"):

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.tests import IntegrationTestCase
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import make_item
@@ -317,7 +317,7 @@ class TestPOSInvoice(IntegrationTestCase):
)
pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2000, "default": 1}
)
pos.insert()
@@ -328,6 +328,11 @@ class TestPOSInvoice(IntegrationTestCase):
# partial return 1
pos_return1.get("items")[0].qty = -1
pos_return1.set("payments", [])
pos_return1.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
)
pos_return1.paid_amount = -1000
pos_return1.submit()
pos_return1.reload()
@@ -342,6 +347,11 @@ class TestPOSInvoice(IntegrationTestCase):
# partial return 2
pos_return2 = make_sales_return(pos.name)
pos_return2.set("payments", [])
pos_return2.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": -1000, "default": 1}
)
pos_return2.paid_amount = -1000
pos_return2.submit()
self.assertEqual(pos_return2.get("items")[0].qty, -1)
@@ -377,6 +387,15 @@ class TestPOSInvoice(IntegrationTestCase):
inv.payments = []
self.assertRaises(frappe.ValidationError, inv.insert)
def test_partial_payment(self):
pos_inv = create_pos_invoice(rate=10000, do_not_save=1)
pos_inv.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 9000},
)
pos_inv.insert()
self.assertRaises(PartialPaymentValidationError, pos_inv.submit)
def test_serialized_item_transaction(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -589,7 +608,13 @@ class TestPOSInvoice(IntegrationTestCase):
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
)
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
inv.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
)
inv.insert()
inv.submit()
lpe = frappe.get_doc(
"Loyalty Point Entry",
@@ -615,7 +640,13 @@ class TestPOSInvoice(IntegrationTestCase):
)
# add 10 loyalty points
create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
pos_inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
pos_inv.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000},
)
pos_inv.paid_amount = 10000
pos_inv.submit()
before_lp_details = get_loyalty_program_details_with_points(
"Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty"
@@ -649,10 +680,12 @@ class TestPOSInvoice(IntegrationTestCase):
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270})
pos_inv.save()
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit()
consolidate_pos_invoices()
@@ -684,6 +717,7 @@ class TestPOSInvoice(IntegrationTestCase):
"included_in_print_rate": 1,
},
)
pos_inv.save()
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
@@ -700,6 +734,7 @@ class TestPOSInvoice(IntegrationTestCase):
"included_in_print_rate": 1,
},
)
pos_inv2.save()
pos_inv2.submit()
consolidate_pos_invoices()
@@ -752,6 +787,7 @@ class TestPOSInvoice(IntegrationTestCase):
"included_in_print_rate": 1,
},
)
pos_inv2.save()
pos_inv2.submit()
consolidate_pos_invoices()
@@ -782,7 +818,10 @@ class TestPOSInvoice(IntegrationTestCase):
# POS Invoice 1, for the batch without bundle
pos_inv1 = create_pos_invoice(item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1)
pos_inv1.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 4500},
)
pos_inv1.items[0].batch_no = batch_no
pos_inv1.save()
pos_inv1.submit()
@@ -798,8 +837,14 @@ class TestPOSInvoice(IntegrationTestCase):
# POS Invoice 2, for the batch with bundle
pos_inv2 = create_pos_invoice(
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no, do_not_save=1
)
pos_inv2.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3000},
)
pos_inv2.save()
pos_inv2.submit()
pos_inv2.reload()
self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle)
@@ -834,6 +879,10 @@ class TestPOSInvoice(IntegrationTestCase):
pos_inv1 = create_pos_invoice(
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
)
pos_inv1.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300},
)
pos_inv1.save()
pos_inv1.submit()

View File

@@ -40,14 +40,17 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.save()
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
pos_inv3.save()
pos_inv3.submit()
consolidate_pos_invoices()
@@ -73,14 +76,17 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
pos_inv.save()
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
pos_inv2.save()
pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
pos_inv3.save()
pos_inv3.submit()
pos_inv_cn = make_sales_return(pos_inv.name)
@@ -135,6 +141,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
)
inv.insert()
inv.payments[0].amount = inv.grand_total
inv.save()
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
@@ -152,6 +159,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
)
inv2.insert()
inv2.payments[0].amount = inv.grand_total
inv2.save()
inv2.submit()
consolidate_pos_invoices()
@@ -291,7 +299,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
inv2.submit()
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1800})
inv3.insert()
inv3.submit()
@@ -299,8 +307,8 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
inv.load_from_db()
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
self.assertNotEqual(consolidated_invoice.status, "Paid")
self.assertNotEqual(consolidated_invoice.outstanding_amount, 800)
self.assertEqual(consolidated_invoice.status, "Paid")
finally:
frappe.set_user("Administrator")
@@ -435,6 +443,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
do_not_submit=1,
)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
pos_inv.save()
pos_inv.submit()
pos_inv_cn = make_sales_return(pos_inv.name)
@@ -449,6 +458,7 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
do_not_submit=1,
)
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
pos_inv2.save()
pos_inv2.submit()
consolidate_pos_invoices()

View File

@@ -78,7 +78,11 @@ class POSProfile(Document):
def validate_accounting_dimensions(self):
acc_dims = get_checks_for_pl_and_bs_accounts()
for acc_dim in acc_dims:
if not self.get(acc_dim.fieldname) and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs):
if (
self.company == acc_dim.company
and not self.get(acc_dim.fieldname)
and (acc_dim.mandatory_for_pl or acc_dim.mandatory_for_bs)
):
frappe.throw(
_(
"{0} is a mandatory Accounting Dimension. <br>"

View File

@@ -53,6 +53,7 @@
"column_break_42",
"free_item_uom",
"round_free_qty",
"dont_enforce_free_item_qty",
"is_recursive",
"recurse_for",
"apply_recursion_over",
@@ -643,12 +644,19 @@
"fieldname": "has_priority",
"fieldtype": "Check",
"label": "Has Priority"
},
{
"default": "0",
"depends_on": "eval:doc.price_or_product_discount == 'Product'",
"fieldname": "dont_enforce_free_item_qty",
"fieldtype": "Check",
"label": "Don't Enforce Free Item Qty"
}
],
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2024-09-16 18:14:51.314765",
"modified": "2025-02-17 18:15:39.824639",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@@ -60,6 +60,7 @@ class PricingRule(Document):
disable: DF.Check
discount_amount: DF.Currency
discount_percentage: DF.Float
dont_enforce_free_item_qty: DF.Check
for_price_list: DF.Link | None
free_item: DF.Link | None
free_item_rate: DF.Currency
@@ -645,7 +646,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
if pricing_rule.margin_type in ["Percentage", "Amount"]:
item_details.margin_rate_or_amount = 0.0
item_details.margin_type = None
elif pricing_rule.get("free_item"):
elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"):
item_details.remove_free_item = (
item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
)

View File

@@ -438,6 +438,54 @@ class TestPricingRule(IntegrationTestCase):
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2")
def test_dont_enforce_free_item_qty(self):
# this test is only for testing non-enforcement as all other tests in this file already test with enforcement
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 0,
"max_qty": 7,
"discount_percentage": 17.5,
"price_or_product_discount": "Product",
"same_item": 0,
"free_item": "_Test Item 2",
"free_qty": 1,
"company": "_Test Company",
}
pricing_rule = frappe.get_doc(test_record.copy()).insert()
# With enforcement
so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True)
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2")
# Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save
so.items.pop(1)
so.save()
so.reload()
self.assertEqual(len(so.items), 2)
# Without enforcement
pricing_rule.dont_enforce_free_item_qty = 1
pricing_rule.save()
# Test 2 : Deleted free item will not be fetched again on save without enforcement
so.items.pop(1)
so.save()
so.reload()
self.assertEqual(len(so.items), 1)
def test_cumulative_pricing_rule(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
test_record = {
@@ -1461,6 +1509,7 @@ def make_pricing_rule(**args):
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
"has_priority": args.has_priority or 0,
"enforce_free_item_qty": args.dont_enforce_free_item_qty or 0,
}
)

View File

@@ -713,7 +713,10 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
args.pop((item.item_code, item.pricing_rules))
for free_item in args.values():
doc.append("items", free_item)
if doc.is_new() or not frappe.get_value(
"Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty"
):
doc.append("items", free_item)
def get_pricing_rule_items(pr_doc, other_items=False) -> list:

View File

@@ -236,17 +236,21 @@ def get_ar_filters(doc, entry):
def get_html(doc, filters, entry, col, res, ageing):
base_template_path = "frappe/www/printview.html"
template_path = (
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
if doc.report == "General Ledger"
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
)
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
if doc.report == "General Ledger":
template_path = (
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
)
process_soa_html = frappe.get_hooks("process_soa_html")
# fetching custom print format for Process Statement of Accounts
if process_soa_html and process_soa_html.get(doc.report):
template_path = process_soa_html[doc.report][-1]
if doc.letter_head:
from frappe.www.printview import get_letter_head
letter_head = get_letter_head(doc, 0)
html = frappe.render_template(
template_path,
{
@@ -262,7 +266,6 @@ def get_html(doc, filters, entry, col, res, ageing):
else None,
},
)
html = frappe.render_template(
base_template_path,
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},

View File

@@ -777,7 +777,7 @@ def validate_account_party_type(self):
if self.party_type and self.party:
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type and (account_type not in ["Receivable", "Payable"]):
if account_type and (account_type not in ["Receivable", "Payable", "Equity"]):
frappe.throw(
_(
"Party Type and Party can only be set for Receivable / Payable account<br><br>" "{0}"

View File

@@ -44,7 +44,7 @@
{% endfor %}
<tr>
<td class="right" colspan="3" ><strong>Total (debit) </strong></td>
<td class="left" >{{ gl | sum(attribute='debit') }}</td>
<td class="left" >{{ gl | sum(attribute='debit') | round(2) }}</td>
</tr>
<tr>
<td class="top-bottom" colspan="5"><strong>Credit</strong></td>
@@ -61,7 +61,7 @@
{% endfor %}
<tr>
<td class="right" colspan="3"><strong>Total (credit) </strong></td>
<td class="left" >{{ gl | sum(attribute='credit') }}</td>
<td class="left" >{{ gl | sum(attribute='credit') | round(2) }}</td>
</tr>
<tr>
<td class="top-bottom" colspan="5"><b>Narration: </b>{{ gl[0].remarks }}</td>

View File

@@ -68,16 +68,12 @@ frappe.query_reports["Trial Balance for Party"] = {
{
fieldname: "account",
label: __("Account"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
options: "Account",
get_query: function () {
var company = frappe.query_report.get_filter_value("company");
return {
doctype: "Account",
filters: {
company: company,
},
};
get_data: function (txt) {
return frappe.db.get_link_options("Account", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{

View File

@@ -4,8 +4,10 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt
from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
@@ -35,9 +37,14 @@ def get_data(filters, show_party_name):
filters=party_filters,
order_by="name",
)
account_filter = []
if filters.get("account"):
account_filter = get_accounts_with_children(filters.get("account"))
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
opening_balances = get_opening_balances(filters)
balances_within_period = get_balances_within_period(filters)
opening_balances = get_opening_balances(filters, account_filter)
balances_within_period = get_balances_within_period(filters, account_filter)
data = []
# total_debit, total_credit = 0, 0
@@ -89,30 +96,34 @@ def get_data(filters, show_party_name):
return data
def get_opening_balances(filters):
account_filter = ""
if filters.get("account"):
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
def get_opening_balances(filters, account_filter=None):
GL_Entry = frappe.qb.DocType("GL Entry")
gle = frappe.db.sql(
f"""
select party, sum(debit) as opening_debit, sum(credit) as opening_credit
from `tabGL Entry`
where company=%(company)s
and is_cancelled=0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
{account_filter}
group by party""",
{
"company": filters.company,
"from_date": filters.from_date,
"to_date": filters.to_date,
"party_type": filters.party_type,
},
as_dict=True,
query = (
frappe.qb.from_(GL_Entry)
.select(
GL_Entry.party,
Sum(GL_Entry.debit).as_("opening_debit"),
Sum(GL_Entry.credit).as_("opening_credit"),
)
.where(
(GL_Entry.company == filters.company)
& (GL_Entry.is_cancelled == 0)
& (GL_Entry.party_type == filters.party_type)
& (GL_Entry.party != "")
& (
(GL_Entry.posting_date < filters.from_date)
| ((GL_Entry.is_opening == "Yes") & (GL_Entry.posting_date <= filters.to_date))
)
)
.groupby(GL_Entry.party)
)
if account_filter:
query = query.where(GL_Entry.account.isin(account_filter))
gle = query.run(as_dict=True)
opening = frappe._dict()
for d in gle:
opening_debit, opening_credit = toggle_debit_credit(d.opening_debit, d.opening_credit)
@@ -121,31 +132,33 @@ def get_opening_balances(filters):
return opening
def get_balances_within_period(filters):
account_filter = ""
if filters.get("account"):
account_filter = "and account = %s" % (frappe.db.escape(filters.get("account")))
def get_balances_within_period(filters, account_filter=None):
GL_Entry = frappe.qb.DocType("GL Entry")
gle = frappe.db.sql(
f"""
select party, sum(debit) as debit, sum(credit) as credit
from `tabGL Entry`
where company=%(company)s
and is_cancelled = 0
and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != ''
and posting_date >= %(from_date)s and posting_date <= %(to_date)s
and ifnull(is_opening, 'No') = 'No'
{account_filter}
group by party""",
{
"company": filters.company,
"from_date": filters.from_date,
"to_date": filters.to_date,
"party_type": filters.party_type,
},
as_dict=True,
query = (
frappe.qb.from_(GL_Entry)
.select(
GL_Entry.party,
Sum(GL_Entry.debit).as_("debit"),
Sum(GL_Entry.credit).as_("credit"),
)
.where(
(GL_Entry.company == filters.company)
& (GL_Entry.is_cancelled == 0)
& (GL_Entry.party_type == filters.party_type)
& (GL_Entry.party != "")
& (GL_Entry.posting_date >= filters.from_date)
& (GL_Entry.posting_date <= filters.to_date)
& (GL_Entry.is_opening == "No")
)
.groupby(GL_Entry.party)
)
if account_filter:
query = query.where(GL_Entry.account.isin(account_filter))
gle = query.run(as_dict=True)
balances_within_period = frappe._dict()
for d in gle:
balances_within_period.setdefault(d.party, [d.debit, d.credit])

View File

@@ -609,9 +609,7 @@ frappe.ui.form.on("Asset", {
frm.trigger("toggle_reference_doc");
if (frm.doc.purchase_receipt) {
if (frm.doc.item_code) {
frappe.db.get_doc("Purchase Receipt", frm.doc.purchase_receipt).then((pr_doc) => {
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt", pr_doc);
});
frm.events.set_values_from_purchase_doc(frm, "Purchase Receipt");
} else {
frm.set_value("purchase_receipt", "");
frappe.msgprint({
@@ -626,9 +624,7 @@ frappe.ui.form.on("Asset", {
frm.trigger("toggle_reference_doc");
if (frm.doc.purchase_invoice) {
if (frm.doc.item_code) {
frappe.db.get_doc("Purchase Invoice", frm.doc.purchase_invoice).then((pi_doc) => {
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice", pi_doc);
});
frm.events.set_values_from_purchase_doc(frm, "Purchase Invoice");
} else {
frm.set_value("purchase_invoice", "");
frappe.msgprint({
@@ -639,45 +635,35 @@ frappe.ui.form.on("Asset", {
}
},
set_values_from_purchase_doc: function (frm, doctype, purchase_doc) {
frm.set_value("company", purchase_doc.company);
if (purchase_doc.bill_date) {
frm.set_value("purchase_date", purchase_doc.bill_date);
} else {
frm.set_value("purchase_date", purchase_doc.posting_date);
}
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
frm.set_value("available_for_use_date", frm.doc.purchase_date);
}
const item = purchase_doc.items.find((item) => item.item_code === frm.doc.item_code);
if (!item) {
let doctype_field = frappe.scrub(doctype);
frm.set_value(doctype_field, "");
frappe.msgprint({
title: __("Invalid {0}", [__(doctype)]),
message: __("The selected {0} does not contain the selected Asset Item.", [__(doctype)]),
indicator: "red",
});
}
frappe.db.get_value("Item", item.item_code, "is_grouped_asset", (r) => {
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
var purchase_amount = flt(
item.valuation_rate * asset_quantity,
precision("gross_purchase_amount")
);
set_values_from_purchase_doc: (frm, doctype) => {
frappe.call({
method: "erpnext.assets.doctype.asset.asset.get_values_from_purchase_doc",
args: {
purchase_doc_name: frm.doc.purchase_receipt || frm.doc.purchase_invoice,
item_code: frm.doc.item_code,
doctype: doctype,
},
callback: (r) => {
if (r.message) {
let data = r.message;
frm.set_value("company", data.company);
frm.set_value("purchase_date", data.purchase_date);
frm.set_value("gross_purchase_amount", data.gross_purchase_amount);
frm.set_value("purchase_amount", data.gross_purchase_amount);
frm.set_value("asset_quantity", data.asset_quantity);
frm.set_value("cost_center", data.cost_center);
frm.set_value("gross_purchase_amount", purchase_amount);
frm.set_value("purchase_amount", purchase_amount);
frm.set_value("asset_quantity", asset_quantity);
frm.set_value("cost_center", item.cost_center || purchase_doc.cost_center);
if (item.asset_location) {
frm.set_value("location", item.asset_location);
}
if (doctype === "Purchase Receipt") {
frm.set_value("purchase_receipt_item", item.name);
} else if (doctype === "Purchase Invoice") {
frm.set_value("purchase_invoice_item", item.name);
}
if (doctype === "Purchase Receipt") {
frm.set_value("purchase_receipt_item", data.purchase_receipt_item);
} else {
frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
}
let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
frm.set_df_property("asset_quantity", "read_only", is_editable);
}
},
});
},

View File

@@ -228,8 +228,7 @@
"fieldtype": "Currency",
"label": "Gross Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
"options": "Company:company:default_currency",
"read_only_depends_on": "eval:!doc.is_existing_asset"
"options": "Company:company:default_currency"
},
{
"fieldname": "available_for_use_date",
@@ -436,8 +435,7 @@
"default": "1",
"fieldname": "asset_quantity",
"fieldtype": "Int",
"label": "Asset Quantity",
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
"label": "Asset Quantity"
},
{
"fieldname": "depr_entry_posting_status",
@@ -507,17 +505,15 @@
},
{
"fieldname": "purchase_receipt_item",
"fieldtype": "Link",
"fieldtype": "Data",
"hidden": 1,
"label": "Purchase Receipt Item",
"options": "Purchase Receipt Item"
"label": "Purchase Receipt Item"
},
{
"fieldname": "purchase_invoice_item",
"fieldtype": "Link",
"fieldtype": "Data",
"hidden": 1,
"label": "Purchase Invoice Item",
"options": "Purchase Invoice Item"
"label": "Purchase Invoice Item"
},
{
"fieldname": "insurance_details_tab",
@@ -592,7 +588,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2024-12-26 14:23:20.968882",
"modified": "2025-02-11 16:01:56.140904",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -95,9 +95,9 @@ class Asset(AccountsController):
purchase_amount: DF.Currency
purchase_date: DF.Date | None
purchase_invoice: DF.Link | None
purchase_invoice_item: DF.Link | None
purchase_invoice_item: DF.Data | None
purchase_receipt: DF.Link | None
purchase_receipt_item: DF.Link | None
purchase_receipt_item: DF.Data | None
split_from: DF.Link | None
status: DF.Literal[
"Draft",
@@ -121,6 +121,7 @@ class Asset(AccountsController):
def validate(self):
self.validate_precision()
self.set_purchase_doc_row_item()
self.validate_asset_values()
self.validate_asset_and_reference()
self.validate_item()
@@ -200,6 +201,38 @@ class Asset(AccountsController):
def after_delete(self):
add_asset_activity(self.name, _("Asset deleted"))
def set_purchase_doc_row_item(self):
if self.is_existing_asset or self.is_composite_asset:
return
self.purchase_amount = self.gross_purchase_amount
purchase_doc_type = "Purchase Receipt" if self.purchase_receipt else "Purchase Invoice"
purchase_doc = self.purchase_receipt or self.purchase_invoice
if not purchase_doc:
return
linked_item = self.get_linked_item(purchase_doc_type, purchase_doc)
if linked_item:
if purchase_doc_type == "Purchase Receipt":
self.purchase_receipt_item = linked_item
else:
self.purchase_invoice_item = linked_item
def get_linked_item(self, purchase_doc_type, purchase_doc):
purchase_doc = frappe.get_doc(purchase_doc_type, purchase_doc)
for item in purchase_doc.items:
if self.asset_quantity > 1:
if item.base_net_amount == self.gross_purchase_amount and item.qty == self.asset_quantity:
return item.name
elif item.qty == self.asset_quantity:
return item.name
else:
if item.base_net_rate == self.gross_purchase_amount and item.qty == self.asset_quantity:
return item.name
def validate_asset_and_reference(self):
if self.purchase_invoice or self.purchase_receipt:
reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt"
@@ -1126,6 +1159,30 @@ def has_active_capitalization(asset):
return active_capitalizations > 0
@frappe.whitelist()
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
if not matching_items:
frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
first_item = matching_items[0]
is_multiple_items = len(matching_items) > 1
return {
"company": purchase_doc.company,
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
"gross_purchase_amount": flt(first_item.base_net_amount),
"asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
"asset_location": first_item.get("asset_location"),
"is_multiple_items": is_multiple_items,
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
}
@frappe.whitelist()
def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name)

View File

@@ -448,9 +448,9 @@ def scrap_asset(asset_name, scrap_date=None):
notes = _("This schedule was created when Asset {0} was scrapped.").format(
get_link_to_form(asset.doctype, asset.name)
)
depreciate_asset(asset, date, notes)
asset.reload()
if asset.status != "Fully Depreciated":
depreciate_asset(asset, date, notes)
asset.reload()
depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")

View File

@@ -937,6 +937,7 @@
"fieldtype": "Float",
"label": "Subcontracted Quantity",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
}
],
@@ -944,7 +945,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-12-10 12:11:18.536089",
"modified": "2025-02-18 12:35:04.432636",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -173,7 +173,7 @@ class AccountsController(TransactionBase):
self.validate_qty_is_not_zero()
if (
self.doctype in ["Sales Invoice", "Purchase Invoice"]
self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"]
and self.get("is_return")
and self.get("update_stock")
):

View File

@@ -780,8 +780,10 @@ class BuyingController(SubcontractingController):
is_plural = "s" if len(created_assets) != 1 else ""
messages.append(
_("Asset{} {assets_link} created for {}").format(
is_plural, frappe.bold(d.item_code), assets_link=assets_link
_("Asset{is_plural} {assets_link} created for {item_code}").format(
is_plural=is_plural,
assets_link=assets_link,
item_code=frappe.bold(d.item_code),
)
)
else:

View File

@@ -258,7 +258,7 @@ def get_already_returned_items(doc):
field = (
frappe.scrub(doc.doctype) + "_item"
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice"]
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Sales Invoice", "POS Invoice"]
else "dn_detail"
)
data = frappe.db.sql(
@@ -770,6 +770,7 @@ def get_return_against_item_fields(voucher_type):
"Delivery Note": "dn_detail",
"Sales Invoice": "sales_invoice_item",
"Subcontracting Receipt": "subcontracting_receipt_item",
"POS Invoice": "sales_invoice_item",
}
return return_against_item_fields[voucher_type]
@@ -1162,3 +1163,29 @@ def get_available_serial_nos(serial_nos, warehouse):
def get_payment_data(invoice):
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
return payment
@frappe.whitelist()
def get_pos_invoice_item_returned_qty(pos_invoice, customer, item_row_name):
is_return, docstatus = frappe.db.get_value("POS Invoice", pos_invoice, ["is_return", "docstatus"])
if not is_return and docstatus == 1:
return get_returned_qty_map_for_row(pos_invoice, customer, item_row_name, "POS Invoice")
@frappe.whitelist()
def is_pos_invoice_returnable(pos_invoice):
is_return, docstatus, customer = frappe.db.get_value(
"POS Invoice", pos_invoice, ["is_return", "docstatus", "customer"]
)
if is_return or docstatus == 0:
return False
invoice_item_qty = frappe.db.get_all("POS Invoice Item", {"parent": pos_invoice}, ["name", "qty"])
already_full_returned = 0
for d in invoice_item_qty:
returned_qty = get_returned_qty_map_for_row(pos_invoice, customer, d.name, "POS Invoice")
if returned_qty.qty == d.qty:
already_full_returned += 1
return len(invoice_item_qty) != already_full_returned

View File

@@ -593,12 +593,13 @@ class SellingController(StockController):
if not self.is_internal_transfer() or self.docstatus == 1
else None
)
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
if self.docstatus == 1:
if self.is_internal_transfer():
if serial_and_batch_bundle and self.docstatus == 1 and self.is_return:
serial_and_batch_bundle = self.make_package_for_transfer(
serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward"
)
else:
elif not serial_and_batch_bundle:
serial_and_batch_bundle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse},

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate
from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate, safe_div
class OverAllowanceError(frappe.ValidationError):
@@ -616,7 +616,7 @@ class StatusUpdater(Document):
)[0][0]
)
per_billed = (min(ref_doc_qty, billed_qty) / ref_doc_qty) * 100
per_billed = safe_div(min(ref_doc_qty, billed_qty), ref_doc_qty) * 100
ref_doc = frappe.get_doc(ref_dt, ref_dn)

View File

@@ -13,7 +13,7 @@ app_home = "/app/home"
add_to_apps_screen = [
{
"name": app_name,
"logo": "/assets/erpnext/images/erpnext-logo-blue.png",
"logo": "/assets/erpnext/images/erpnext-logo.svg",
"title": app_title,
"route": app_home,
"has_permission": "erpnext.check_app_permission",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -368,16 +368,8 @@ def get_children(doctype=None, parent=None, **kwargs):
"uom",
"rate",
"amount",
"workstation_type",
"operation",
"operation_time",
"is_subcontracted",
"workstation",
"source_warehouse",
"wip_warehouse",
"fg_warehouse",
"skip_material_transfer",
"backflush_from_wip_warehouse",
]
query_filters = {

View File

@@ -251,7 +251,7 @@ class JobCard(Document):
open_job_cards = []
if d.get("employee"):
open_job_cards = self.get_open_job_cards(d.get("employee"))
open_job_cards = self.get_open_job_cards(d.get("employee"), workstation=self.workstation)
data = self.get_overlap_for(d, open_job_cards=open_job_cards)
if data:
@@ -292,9 +292,13 @@ class JobCard(Document):
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
)
if args.get("employee"):
# override capacity for employee
production_capacity = 1
if self.get_open_job_cards(args.get("employee")):
frappe.throw(
_(
"Employee {0} is currently working on another workstation. Please assign another employee."
).format(args.get("employee")),
OverlapError,
)
if not self.has_overlap(production_capacity, time_logs):
return {}
@@ -401,7 +405,7 @@ class JobCard(Document):
return time_logs
def get_open_job_cards(self, employee):
def get_open_job_cards(self, employee, workstation=None):
jc = frappe.qb.DocType("Job Card")
jctl = frappe.qb.DocType("Job Card Time Log")
@@ -412,13 +416,15 @@ class JobCard(Document):
.select(jc.name)
.where(
(jctl.parent == jc.name)
& (jc.workstation == self.workstation)
& (jctl.employee == employee)
& (jc.docstatus < 1)
& (jc.name != self.name)
)
)
if workstation:
query = query.where(jc.workstation == workstation)
jobs = query.run(as_dict=True)
return [job.get("name") for job in jobs] if jobs else []

View File

@@ -400,5 +400,6 @@ erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect
erpnext.patches.v15_0.sync_auto_reconcile_config
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
erpnext.patches.v14_0.disable_add_row_in_gross_profit
erpnext.patches.v14_0.update_posting_datetime
erpnext.patches.v15_0.rename_field_from_rate_difference_to_amount_difference
erpnext.patches.v15_0.recalculate_amount_difference_field
erpnext.patches.v15_0.recalculate_amount_difference_field

View File

@@ -0,0 +1,10 @@
import frappe
def execute():
frappe.db.sql(
"""
UPDATE `tabStock Ledger Entry`
SET posting_datetime = timestamp(posting_date, posting_time)
"""
)

View File

@@ -1,73 +1,29 @@
import frappe
from frappe.utils import cstr
def execute():
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
frappe.reload_doc("assets", "doctype", "Asset Finance Book")
assets = get_details_of_draft_or_submitted_depreciable_assets()
asset_finance_books_map = get_asset_finance_books_map()
asset_depreciation_schedules_map = get_asset_depreciation_schedules_map()
for asset in assets:
if not asset_depreciation_schedules_map.get(asset.name):
for key, fb_row in asset_finance_books_map.items():
depreciation_schedules = asset_depreciation_schedules_map.get(key)
if not depreciation_schedules:
continue
depreciation_schedules = asset_depreciation_schedules_map[asset.name]
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(fb_row, fb_row)
asset_depr_schedule_doc.flags.ignore_validate = True
asset_depr_schedule_doc.insert()
for fb_row in asset_finance_books_map[asset.name]:
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
if fb_row.docstatus == 1:
frappe.db.set_value(
"Asset Depreciation Schedule",
asset_depr_schedule_doc.name,
{"docstatus": 1, "status": "Active"},
)
asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row)
asset_depr_schedule_doc.insert()
if asset.docstatus == 1:
asset_depr_schedule_doc.submit()
depreciation_schedules_of_fb_row = [
ds for ds in depreciation_schedules if ds["finance_book_id"] == str(fb_row.idx)
]
update_depreciation_schedules(depreciation_schedules_of_fb_row, asset_depr_schedule_doc.name)
def get_details_of_draft_or_submitted_depreciable_assets():
asset = frappe.qb.DocType("Asset")
records = (
frappe.qb.from_(asset)
.select(
asset.name,
asset.opening_accumulated_depreciation,
asset.gross_purchase_amount,
asset.opening_number_of_booked_depreciations,
asset.docstatus,
)
.where(asset.calculate_depreciation == 1)
.where(asset.docstatus < 2)
).run(as_dict=True)
return records
def group_records_by_asset_name(records):
grouped_dict = {}
for item in records:
key = next(iter(item.keys()))
value = item[key]
if value not in grouped_dict:
grouped_dict[value] = []
del item["asset_name"]
grouped_dict[value].append(item)
return grouped_dict
update_depreciation_schedules(depreciation_schedules, asset_depr_schedule_doc.name)
def get_asset_finance_books_map():
@@ -89,12 +45,20 @@ def get_asset_finance_books_map():
afb.expected_value_after_useful_life,
afb.daily_prorata_based,
afb.shift_based,
asset.docstatus,
asset.name,
asset.opening_accumulated_depreciation,
asset.gross_purchase_amount,
asset.opening_number_of_booked_depreciations,
)
.where(asset.docstatus < 2)
.where(asset.calculate_depreciation == 1)
.orderby(afb.idx)
).run(as_dict=True)
asset_finance_books_map = group_records_by_asset_name(records)
asset_finance_books_map = frappe._dict()
for d in records:
asset_finance_books_map.setdefault((d.asset_name, cstr(d.finance_book)), d)
return asset_finance_books_map
@@ -110,13 +74,17 @@ def get_asset_depreciation_schedules_map():
.select(
asset.name.as_("asset_name"),
ds.name,
ds.finance_book,
ds.finance_book_id,
)
.where(asset.docstatus < 2)
.where(asset.calculate_depreciation == 1)
.orderby(ds.idx)
).run(as_dict=True)
asset_depreciation_schedules_map = group_records_by_asset_name(records)
asset_depreciation_schedules_map = frappe._dict()
for d in records:
asset_depreciation_schedules_map.setdefault((d.asset_name, cstr(d.finance_book)), []).append(d)
return asset_depreciation_schedules_map

View File

@@ -526,8 +526,7 @@
> .checkout-btn {
@extend .primary-action;
background-color: var(--blue-200);
color: white;
background-color: var(--control-bg);
}
> .edit-cart-btn {
@@ -769,8 +768,8 @@
.submit-order-btn {
@extend .primary-action;
background-color: var(--primary-color);
color: white;
background-color: var(--btn-primary);
color: var(--neutral);
}
.section-label {
@@ -1087,34 +1086,49 @@
> .item-row-wrapper {
display: flex;
align-items: center;
gap: 2px;
flex-direction: column;
padding: var(--padding-sm) var(--padding-md);
border: 1px solid lightgray;
border-radius: 10px;
background: var(--bg-light-gray);
> .item-name {
@extend .nowrap;
font-weight: 500;
margin-right: var(--margin-md);
}
> .item-qty {
font-weight: 500;
margin-left: auto;
}
> .item-rate-disc {
> .item-row-data {
display: flex;
text-align: right;
margin-left: var(--margin-md);
justify-content: flex-end;
align-items: center;
> .item-disc {
color: var(--dark-green-500);
}
> .item-rate {
> .item-name {
@extend .nowrap;
font-weight: 500;
margin-left: var(--margin-md);
margin-right: var(--margin-md);
}
> .item-qty {
font-weight: 500;
margin-left: auto;
font-size: small;
}
> .item-rate-disc {
display: flex;
text-align: right;
margin-left: var(--margin-md);
justify-content: flex-end;
font-size: small;
> .item-disc {
color: var(--dark-green-500);
}
> .item-rate {
font-weight: 500;
margin-left: var(--margin-md);
}
}
}
> .item-row-refund {
font-size: x-small;
}
}
@@ -1127,6 +1141,12 @@
}
}
> .order-summary-container {
display: flex;
background: white;
gap: 8px;
}
> .summary-btns {
display: flex;
justify-content: space-between;

View File

@@ -353,6 +353,26 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
);
dialog.show();
}
currency() {
super.currency();
let me = this;
const company_currency = this.get_company_currency();
if (this.frm.doc.currency && this.frm.doc.currency !== company_currency) {
this.get_exchange_rate(
this.frm.doc.transaction_date,
this.frm.doc.currency,
company_currency,
function (exchange_rate) {
if (exchange_rate != me.frm.doc.conversion_rate) {
me.set_margin_amount_based_on_currency(exchange_rate);
me.set_actual_charges_based_on_currency(exchange_rate);
me.frm.set_value("conversion_rate", exchange_rate);
}
}
);
}
}
};
cur_frm.script_manager.make(erpnext.selling.QuotationController);

View File

@@ -107,7 +107,11 @@
"purchase_order",
"column_break_89",
"material_request_item",
"purchase_order_item"
"purchase_order_item",
"accounting_dimensions_section",
"cost_center",
"column_break_ihdh",
"project"
],
"fields": [
{
@@ -932,12 +936,42 @@
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"allow_on_submit": 1,
"default": ":Company",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1,
"print_width": "120px",
"reqd": 1,
"width": "120px"
},
{
"fieldname": "column_break_ihdh",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project",
"search_index": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-01-09 15:34:47.768457",
"modified": "2025-02-06 13:29:24.619850",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -32,6 +32,7 @@ class SalesOrderItem(Document):
brand: DF.Link | None
company_total_stock: DF.Float
conversion_factor: DF.Float
cost_center: DF.Link
customer_item_code: DF.Data | None
delivered_by_supplier: DF.Check
delivered_qty: DF.Float
@@ -69,6 +70,7 @@ class SalesOrderItem(Document):
pricing_rules: DF.SmallText | None
produced_qty: DF.Float
production_plan_qty: DF.Float
project: DF.Link | None
projected_qty: DF.Float
purchase_order: DF.Link | None
purchase_order_item: DF.Data | None

View File

@@ -459,6 +459,8 @@ erpnext.PointOfSale.Controller = class {
() => this.make_return_invoice(doc),
() => this.cart.load_invoice(),
() => this.item_selector.toggle_component(true),
() => this.item_selector.resize_selector(false),
() => this.item_details.toggle_component(false),
]);
});
},
@@ -469,6 +471,8 @@ erpnext.PointOfSale.Controller = class {
() => this.frm.call("reset_mode_of_payments"),
() => this.cart.load_invoice(),
() => this.item_selector.toggle_component(true),
() => this.item_selector.resize_selector(false),
() => this.item_details.toggle_component(false),
]);
},
delete_order: (name) => {

View File

@@ -184,7 +184,7 @@ erpnext.PointOfSale.ItemCart = class {
});
this.$component.on("click", ".checkout-btn", async function () {
if ($(this).attr("style").indexOf("--primary-color") == -1) return;
if ($(this).attr("style").indexOf("--btn-primary") == -1) return;
await me.events.checkout();
me.toggle_checkout_btn(false);
@@ -702,12 +702,14 @@ erpnext.PointOfSale.ItemCart = class {
if (toggle) {
this.$add_discount_elem.css("display", "flex");
this.$cart_container.find(".checkout-btn").css({
"background-color": "var(--primary-color)",
"background-color": "var(--btn-primary)",
color: "var(--neutral)",
});
} else {
this.$add_discount_elem.css("display", "none");
this.$cart_container.find(".checkout-btn").css({
"background-color": "var(--gray-200)",
"background-color": "var(--control-bg)",
color: "",
});
}
}

View File

@@ -24,7 +24,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
<div class="abs-container">
<div class="upper-section"></div>
<div class="label">${__("Items")}</div>
<div class="items-container summary-container"></div>
<div class="items-container summary-container order-summary-container"></div>
<div class="label">${__("Totals")}</div>
<div class="totals-container summary-container"></div>
<div class="label">${__("Payments")}</div>
@@ -90,12 +90,18 @@ erpnext.PointOfSale.PastOrderSummary = class {
</div>`;
}
get_item_html(doc, item_data) {
async get_item_html(doc, item_data) {
const item_refund_data = doc.is_return || doc.docstatus === 0 ? "" : await get_returned_qty();
return `<div class="item-row-wrapper">
<div class="item-row-data">
<div class="item-name">${item_data.item_name}</div>
<div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
<div class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`;
</div>
${item_refund_data}
</div>`;
function get_rate_discount_html() {
if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) {
@@ -108,6 +114,25 @@ erpnext.PointOfSale.PastOrderSummary = class {
)}</div>`;
}
}
async function get_returned_qty() {
const r = await frappe.call({
method: "erpnext.controllers.sales_and_purchase_return.get_pos_invoice_item_returned_qty",
args: {
pos_invoice: doc.name,
customer: doc.customer,
item_row_name: item_data.name,
},
});
if (!r.message.qty) {
return "";
}
return `<div class="item-row-refund">
<strong>${r.message.qty}</strong> ${__("Returned")}
</div>`;
}
}
get_discount_html(doc) {
@@ -166,7 +191,16 @@ erpnext.PointOfSale.PastOrderSummary = class {
}
bind_events() {
this.$summary_container.on("click", ".return-btn", () => {
this.$summary_container.on("click", ".return-btn", async () => {
const r = await this.is_pos_invoice_returnable(this.doc.name);
if (!r) {
frappe.msgprint({
title: __("Invalid Return"),
indicator: "orange",
message: __("All the items have been already returned."),
});
return;
}
this.events.process_return(this.doc.name);
this.toggle_component(false);
this.$component.find(".no-summary-placeholder").css("display", "flex");
@@ -370,13 +404,13 @@ erpnext.PointOfSale.PastOrderSummary = class {
});
}
attach_items_info(doc) {
async attach_items_info(doc) {
this.$items_container.html("");
doc.items.forEach((item) => {
const item_dom = this.get_item_html(doc, item);
for (const item of doc.items) {
const item_dom = await this.get_item_html(doc, item);
this.$items_container.append(item_dom);
this.set_dynamic_rate_header_width();
});
}
}
set_dynamic_rate_header_width() {
@@ -438,4 +472,14 @@ erpnext.PointOfSale.PastOrderSummary = class {
this.print_receipt();
}
}
async is_pos_invoice_returnable(invoice) {
const r = await frappe.call({
method: "erpnext.controllers.sales_and_purchase_return.is_pos_invoice_returnable",
args: {
pos_invoice: invoice,
},
});
return r.message;
}
};

View File

@@ -7,7 +7,7 @@
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "getting-started",
"icon": "home",
"idx": 0,
"is_hidden": 0,
"label": "Home",
@@ -232,7 +232,7 @@
"type": "Link"
}
],
"modified": "2023-05-24 14:47:18.765388",
"modified": "2025-02-17 10:55:02.213683",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",

View File

@@ -790,6 +790,48 @@ class TestDeliveryNote(IntegrationTestCase):
{"warehouse": "_Test Warehouse - _TC"},
)
def test_delivery_note_internal_transfer_serial_no_status(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
item = make_item(
"_Test Item for Internal Transfer With Serial No Status",
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "INT-SN-.####"},
).name
warehouse = "_Test Warehouse - _TC"
target = "Stores - _TC"
company = "_Test Company"
customer = create_internal_customer(represents_company=company)
rate = 42
se = make_stock_entry(target=warehouse, qty=5, basic_rate=rate, item_code=item)
serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle)
dn = create_delivery_note(
item_code=item,
company=company,
customer=customer,
qty=5,
rate=500,
warehouse=warehouse,
target_warehouse=target,
ignore_pricing_rule=0,
use_serial_batch_fields=1,
serial_no="\n".join(serial_nos),
)
for serial_no in serial_nos:
sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1)
self.assertEqual(sn.status, "Active")
self.assertEqual(sn.warehouse, target)
dn.cancel()
for serial_no in serial_nos:
sn = frappe.db.get_value("Serial No", serial_no, ["status", "warehouse"], as_dict=1)
self.assertEqual(sn.status, "Active")
self.assertEqual(sn.warehouse, warehouse)
def test_delivery_of_bundled_items_to_target_warehouse(self):
from erpnext.selling.doctype.customer.test_customer import create_internal_customer

View File

@@ -1,6 +1,5 @@
{
"actions": [],
"allow_guest_to_view": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:item_code",
@@ -888,10 +887,9 @@
"icon": "fa fa-tag",
"idx": 2,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2024-04-30 13:46:39.098753",
"modified": "2025-02-03 23:43:57.253667",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -202,7 +202,8 @@
"oldfieldname": "parent_detail_docname",
"oldfieldtype": "Data",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"depends_on": "batch_no",
@@ -295,7 +296,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:07.966447",
"modified": "2025-02-18 13:06:02.789654",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",

View File

@@ -1251,6 +1251,7 @@ class TestPickList(IntegrationTestCase):
"is_recursive": 1,
"recurse_for": 2,
"free_qty": 1,
"dont_enforce_free_item_qty": 0,
"company": "_Test Company",
"customer": "_Test Customer",
}

View File

@@ -34,7 +34,9 @@
"verified_by",
"column_break_17",
"remarks",
"amended_from"
"amended_from",
"print_settings_section",
"letter_head"
],
"fields": [
{
@@ -255,6 +257,20 @@
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "print_settings_section",
"fieldtype": "Section Break",
"label": "Print Settings"
},
{
"allow_on_submit": 1,
"fetch_from": "company.default_letter_head",
"fetch_if_empty": 1,
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
"options": "Letter Head"
}
],
"icon": "fa fa-search",
@@ -262,7 +278,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-01-16 17:00:48.774532",
"modified": "2025-02-17 13:20:17.583094",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",

View File

@@ -37,6 +37,7 @@ class QualityInspection(Document):
item_code: DF.Link
item_name: DF.Data | None
item_serial_no: DF.Link | None
letter_head: DF.Link | None
manual_inspection: DF.Check
naming_series: DF.Literal["MAT-QA-.YYYY.-"]
quality_inspection_template: DF.Link | None

View File

@@ -134,6 +134,13 @@ frappe.ui.form.on("Serial and Batch Bundle", {
},
toggle_fields(frm) {
let show_naming_series_field =
frappe.user_defaults.set_serial_and_batch_bundle_naming_based_on_naming_series;
frm.toggle_display("naming_series", cint(show_naming_series_field));
frm.toggle_reqd("naming_series", cint(show_naming_series_field));
frm.toggle_display("naming_series", frm.doc.__islocal ? true : false);
if (frm.doc.has_serial_no) {
frm.doc.entries.forEach((row) => {
if (Math.abs(row.qty) !== 1) {

View File

@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"item_details_tab",
"naming_series",
"company",
"item_name",
"has_serial_no",
@@ -242,12 +243,20 @@
"fieldtype": "Data",
"label": "Returned Against",
"read_only": 1
},
{
"default": "SABB-.########",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "\nSABB-.########",
"set_only_once": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-02-12 09:53:32.090309",
"modified": "2025-02-17 16:22:36.056205",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",

View File

@@ -8,6 +8,7 @@ from collections import Counter, defaultdict
import frappe
from frappe import _, _dict, bold
from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import (
add_days,
@@ -68,6 +69,7 @@ class SerialandBatchBundle(Document):
item_code: DF.Link
item_group: DF.Link | None
item_name: DF.Data | None
naming_series: DF.Literal["", "SABB-.########"]
posting_date: DF.Date | None
posting_time: DF.Time | None
returned_against: DF.Data | None
@@ -80,6 +82,24 @@ class SerialandBatchBundle(Document):
warehouse: DF.Link | None
# end: auto-generated types
def autoname(self):
if frappe.db.get_single_value(
"Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series"
):
if not self.naming_series:
frappe.throw(_("Naming Series is mandatory"))
naming_series = self.naming_series
if "#" not in naming_series:
naming_series += ".#####"
self.name = make_autoname(self.naming_series)
else:
try:
self.name = frappe.generate_hash(length=20)
except frappe.DuplicateEntryError:
self.autoname()
def validate(self):
if self.docstatus == 1 and self.voucher_detail_no:
self.validate_voucher_detail_no()

View File

@@ -26,6 +26,67 @@ class UnitTestSerialAndBatchBundle(UnitTestCase):
class TestSerialandBatchBundle(IntegrationTestCase):
def test_naming_for_sabb(self):
frappe.db.set_single_value(
"Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 1
)
serial_item_code = "New Serial No Valuation 11"
make_item(
serial_item_code,
{
"has_serial_no": 1,
"serial_no_series": "TEST-A-SER-VAL-.#####",
"is_stock_item": 1,
},
)
for sn in ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"]:
if not frappe.db.exists("Serial No", sn):
frappe.get_doc(
{
"doctype": "Serial No",
"serial_no": sn,
"item_code": serial_item_code,
}
).insert(ignore_permissions=True)
bundle_doc = make_serial_batch_bundle(
{
"item_code": serial_item_code,
"warehouse": "_Test Warehouse - _TC",
"voucher_type": "Stock Entry",
"posting_date": today(),
"posting_time": nowtime(),
"qty": 10,
"serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"],
"type_of_transaction": "Inward",
"do_not_submit": True,
}
)
self.assertTrue(bundle_doc.name.startswith("SABB-"))
frappe.db.set_single_value(
"Stock Settings", "set_serial_and_batch_bundle_naming_based_on_naming_series", 0
)
bundle_doc = make_serial_batch_bundle(
{
"item_code": serial_item_code,
"warehouse": "_Test Warehouse - _TC",
"voucher_type": "Stock Entry",
"posting_date": today(),
"posting_time": nowtime(),
"qty": 10,
"serial_nos": ["TEST-A-SER-VAL-00001", "TEST-A-SER-VAL-00002"],
"type_of_transaction": "Inward",
"do_not_submit": True,
}
)
self.assertFalse(bundle_doc.name.startswith("SABB-"))
def test_inward_outward_serial_valuation(self):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt

View File

@@ -57,6 +57,8 @@
"use_serial_batch_fields",
"do_not_update_serial_batch_on_creation_of_auto_bundle",
"allow_existing_serial_no",
"serial_and_batch_bundle_section",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
"stock_planning_tab",
"auto_material_request",
"auto_indent",
@@ -475,6 +477,17 @@
"fieldname": "auto_reserve_stock",
"fieldtype": "Check",
"label": "Auto Reserve Stock"
},
{
"fieldname": "serial_and_batch_bundle_section",
"fieldtype": "Section Break",
"label": "Serial and Batch Bundle"
},
{
"default": "0",
"fieldname": "set_serial_and_batch_bundle_naming_based_on_naming_series",
"fieldtype": "Check",
"label": "Set Serial and Batch Bundle Naming Based on Naming Series"
}
],
"icon": "icon-cog",
@@ -482,7 +495,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-12-10 17:52:36.030456",
"modified": "2025-02-17 13:36:36.177743",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -56,6 +56,7 @@ class StockSettings(Document):
role_allowed_to_create_edit_back_dated_transactions: DF.Link | None
role_allowed_to_over_deliver_receive: DF.Link | None
sample_retention_warehouse: DF.Link | None
set_serial_and_batch_bundle_naming_based_on_naming_series: DF.Check
show_barcode_field: DF.Check
stock_auth_role: DF.Link | None
stock_frozen_upto: DF.Date | None
@@ -76,6 +77,7 @@ class StockSettings(Document):
"default_warehouse",
"set_qty_in_transactions_based_on_serial_no_input",
"use_serial_batch_fields",
"set_serial_and_batch_bundle_naming_based_on_naming_series",
]:
frappe.db.set_default(key, self.get(key, ""))

View File

@@ -663,7 +663,7 @@ def get_combine_datetime(posting_date, posting_time):
if isinstance(posting_time, datetime.timedelta):
posting_time = (datetime.datetime.min + posting_time).time()
return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0)
return datetime.datetime.combine(posting_date, posting_time)
@frappe.request_cache