mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 12:38:27 +00:00
Merge branch 'develop' into st31369
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
2763
erpnext/locale/bs.po
2763
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
1112
erpnext/locale/de.po
1112
erpnext/locale/de.po
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
@@ -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 = {
|
||||
|
||||
@@ -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 []
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
10
erpnext/patches/v14_0/update_posting_datetime.py
Normal file
10
erpnext/patches/v14_0/update_posting_datetime.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.sql(
|
||||
"""
|
||||
UPDATE `tabStock Ledger Entry`
|
||||
SET posting_datetime = timestamp(posting_date, posting_time)
|
||||
"""
|
||||
)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, ""))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user