Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4723fbfd57 | ||
|
|
ab49ee2e05 | ||
|
|
d1295d1c79 | ||
|
|
46a8500361 | ||
|
|
88ba24f1cf | ||
|
|
87bb403159 | ||
|
|
43f59da0a9 | ||
|
|
2c865bcd49 | ||
|
|
fc783f5acf | ||
|
|
24126b03dd | ||
|
|
5fa4fd8825 | ||
|
|
23fb4f348f | ||
|
|
227c912ece | ||
|
|
5bfbdb805c | ||
|
|
ec40131d4d | ||
|
|
859672c419 | ||
|
|
e8c3617628 | ||
|
|
09b21b8cb4 | ||
|
|
356da69179 | ||
|
|
bd0f11ef4f | ||
|
|
9f3dfb3d18 | ||
|
|
24b5b3c8e0 | ||
|
|
a7de8c1143 | ||
|
|
2fcab327aa | ||
|
|
f3cbbef346 | ||
|
|
339beff023 | ||
|
|
85d398efcc | ||
|
|
e09f101336 | ||
|
|
f9b52b292e | ||
|
|
8a94d7bea8 | ||
|
|
f871f08f47 | ||
|
|
313ea3983f | ||
|
|
c3e2ff2fa5 | ||
|
|
33e835c4d3 | ||
|
|
c9fa4af4fe | ||
|
|
36898f6797 | ||
|
|
b9a0f4fed8 | ||
|
|
1019d98c5a | ||
|
|
cc9b22ce9f | ||
|
|
f5135cd4a4 | ||
|
|
137ef78d96 | ||
|
|
5452f8ac4a | ||
|
|
f03e301250 | ||
|
|
c23868a14d |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.76.0"
|
||||
__version__ = "14.77.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -52,6 +52,21 @@ class ExchangeRateRevaluation(Document):
|
||||
if not (self.company and self.posting_date):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
def before_submit(self):
|
||||
self.remove_accounts_without_gain_loss()
|
||||
|
||||
def remove_accounts_without_gain_loss(self):
|
||||
self.accounts = [account for account in self.accounts if account.gain_loss]
|
||||
|
||||
if not self.accounts:
|
||||
frappe.throw(_("At least one account with exchange gain or loss is required"))
|
||||
|
||||
frappe.msgprint(
|
||||
_("Removing rows without exchange gain or loss"),
|
||||
alert=True,
|
||||
indicator="yellow",
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "GL Entry"
|
||||
|
||||
@@ -226,23 +241,23 @@ class ExchangeRateRevaluation(Document):
|
||||
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
@@ -266,23 +281,22 @@ class ExchangeRateRevaluation(Document):
|
||||
current_exchange_rate * d.balance_in_account_currency
|
||||
)
|
||||
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
},
|
||||
|
||||
setup: function(frm) {
|
||||
@@ -320,6 +324,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
payment_type: function(frm) {
|
||||
set_default_party_type(frm);
|
||||
if(frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(["party", "party_balance", "paid_from", "paid_to",
|
||||
"references", "total_allocated_amount"], function(i, field) {
|
||||
@@ -1511,3 +1516,16 @@ frappe.ui.form.on('Payment Entry Deduction', {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
});
|
||||
|
||||
function set_default_party_type(frm) {
|
||||
if (frm.doc.party) return;
|
||||
|
||||
let party_type;
|
||||
if (frm.doc.payment_type == "Receive") {
|
||||
party_type = "Customer";
|
||||
} else if (frm.doc.payment_type == "Pay") {
|
||||
party_type = "Supplier";
|
||||
}
|
||||
|
||||
if (party_type) frm.set_value("party_type", party_type);
|
||||
}
|
||||
|
||||
@@ -144,12 +144,14 @@ class PaymentReconciliation(Document):
|
||||
if self.get("cost_center"):
|
||||
conditions.append(jea.cost_center == self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
account_type = erpnext.get_party_account_type(self.party_type)
|
||||
|
||||
if account_type == "Receivable":
|
||||
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
|
||||
elif account_type == "Payable":
|
||||
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
|
||||
|
||||
conditions.append(dr_or_cr.gt(0))
|
||||
|
||||
if self.bank_cash_account:
|
||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||
@@ -164,7 +166,7 @@ class PaymentReconciliation(Document):
|
||||
je.posting_date,
|
||||
je.remark.as_("remarks"),
|
||||
jea.name.as_("reference_row"),
|
||||
jea[dr_or_cr].as_("amount"),
|
||||
dr_or_cr.as_("amount"),
|
||||
jea.is_advance,
|
||||
jea.exchange_rate,
|
||||
jea.account_currency.as_("currency"),
|
||||
@@ -299,6 +301,10 @@ class PaymentReconciliation(Document):
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
non_reconciled_invoices = sorted(
|
||||
non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
|
||||
)
|
||||
|
||||
self.add_invoice_entries(non_reconciled_invoices)
|
||||
|
||||
def add_invoice_entries(self, non_reconciled_invoices):
|
||||
|
||||
@@ -615,6 +615,42 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_negative_debit_or_credit_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# credit debtors account to record a payment
|
||||
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = self.customer
|
||||
je.accounts[1].credit_in_account_currency = 0
|
||||
je.accounts[1].debit_in_account_currency = -1 * amount
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
sales = "Sales - _PR"
|
||||
@@ -937,6 +973,100 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer4
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debtors_eur
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Journal Entry
|
||||
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].credit_in_account_currency = -8000
|
||||
je1.accounts[0].credit = -8000
|
||||
je1.accounts[0].debit_in_account_currency = 0
|
||||
je1.accounts[0].debit = 0
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = self.customer4
|
||||
je1.accounts[1].exchange_rate = 80
|
||||
je1.accounts[1].credit_in_account_currency = 100
|
||||
je1.accounts[1].credit = 8000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].credit_in_account_currency = -16000
|
||||
je2.accounts[0].credit = -16000
|
||||
je2.accounts[0].debit_in_account_currency = 0
|
||||
je2.accounts[0].debit = 0
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer4
|
||||
je2.accounts[1].exchange_rate = 80
|
||||
je2.accounts[1].credit_in_account_currency = 200
|
||||
je1.accounts[1].credit = 16000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer4
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
# Test exact payment allocation
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Test partial payment allocation (with excess payment entry)
|
||||
pr.set("allocation", [])
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[1].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_credit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(credit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
# total credit includes the exchange gain/loss amount
|
||||
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
|
||||
@@ -838,17 +838,18 @@ def validate_payment(doc, method=None):
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
reference_name = filters.get("reference_doctype")
|
||||
filters = frappe._dict(filters)
|
||||
|
||||
if not reference_doctype or not reference_name:
|
||||
if not filters.reference_doctype or not filters.reference_name:
|
||||
return []
|
||||
|
||||
if txt:
|
||||
filters.name = ["like", f"%{txt}%"]
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters={
|
||||
"reference_doctype": filters["reference_doctype"],
|
||||
"reference_name": filters["reference_name"],
|
||||
**filters,
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
|
||||
@@ -359,7 +359,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
if isinstance(pricing_rule, str):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
||||
update_pricing_rule_uom(pricing_rule, args)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
|
||||
fetch_other_item = True if pricing_rule.apply_rule_on_other else False
|
||||
pricing_rule.apply_rule_on_other_items = (
|
||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if not args.coupon_code:
|
||||
return item_details
|
||||
|
||||
coupon_code = frappe.db.get_value(
|
||||
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||
)
|
||||
if args.coupon_code != coupon_code:
|
||||
continue
|
||||
|
||||
if pricing_rule.get("suggestion"):
|
||||
continue
|
||||
@@ -386,9 +399,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
pricing_rule.apply_rule_on_other_items
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
|
||||
return item_details
|
||||
|
||||
if not pricing_rule.validate_applied_rule:
|
||||
if pricing_rule.price_or_product_discount == "Price":
|
||||
apply_price_discount_rule(pricing_rule, item_details, args)
|
||||
|
||||
@@ -536,7 +536,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
|
||||
@@ -161,6 +161,45 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_with_tax_on_excess_amount(self):
|
||||
invoices = []
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
|
||||
|
||||
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
|
||||
pi1.apply_tds = 1
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 10,000
|
||||
# Threshold calculation should be only on the third invoice
|
||||
self.assertTrue(len(pi1.taxes) > 0)
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
|
||||
@@ -579,6 +579,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
|
||||
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
|
||||
|
||||
rev_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d["dr_or_cr"] == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
if jv_detail.get(rev_dr_or_cr):
|
||||
d["dr_or_cr"] = rev_dr_or_cr
|
||||
d["allocated_amount"] = d["allocated_amount"] * -1
|
||||
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
|
||||
|
||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||
# adjust the unreconciled balance
|
||||
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
|
||||
|
||||
@@ -167,6 +167,9 @@ class SellingController(StockController):
|
||||
|
||||
total = 0.0
|
||||
sales_team = self.get("sales_team")
|
||||
|
||||
self.validate_sales_team(sales_team)
|
||||
|
||||
for sales_person in sales_team:
|
||||
self.round_floats_in(sales_person)
|
||||
|
||||
@@ -186,6 +189,20 @@ class SellingController(StockController):
|
||||
if sales_team and total != 100.0:
|
||||
throw(_("Total allocated percentage for sales team should be 100"))
|
||||
|
||||
def validate_sales_team(self, sales_team):
|
||||
sales_persons = [d.sales_person for d in sales_team]
|
||||
|
||||
if not sales_persons:
|
||||
return
|
||||
|
||||
sales_person_status = frappe.db.get_all(
|
||||
"Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"]
|
||||
)
|
||||
|
||||
for row in sales_person_status:
|
||||
if not row.enabled:
|
||||
frappe.throw(_("Sales Person <b>{0}</b> is disabled.").format(row.name))
|
||||
|
||||
def validate_max_discount(self):
|
||||
for d in self.get("items"):
|
||||
if d.item_code:
|
||||
|
||||
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Code List", {
|
||||
refresh: (frm) => {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.add_custom_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(frm);
|
||||
});
|
||||
}
|
||||
},
|
||||
setup: (frm) => {
|
||||
frm.savetrash = () => {
|
||||
frm.validate_form_action("Delete");
|
||||
frappe.confirm(
|
||||
__(
|
||||
"Are you sure you want to delete {0}?<p>This action will also delete all associated Common Code documents.</p>",
|
||||
[frm.docname.bold()]
|
||||
),
|
||||
function () {
|
||||
return frappe.call({
|
||||
method: "frappe.client.delete",
|
||||
args: {
|
||||
doctype: frm.doctype,
|
||||
name: frm.docname,
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Deleting {0} and all associated Common Code documents...", [
|
||||
frm.docname,
|
||||
]),
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.utils.play_sound("delete");
|
||||
frappe.model.clear_doc(frm.doctype, frm.docname);
|
||||
window.history.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
frm.set_query("default_common_code", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
code_list: doc.name,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "prompt",
|
||||
"creation": "2024-09-29 06:55:03.920375",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"canonical_uri",
|
||||
"url",
|
||||
"default_common_code",
|
||||
"column_break_nkls",
|
||||
"version",
|
||||
"publisher",
|
||||
"publisher_id",
|
||||
"section_break_npxp",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "publisher",
|
||||
"fieldtype": "Data",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Publisher"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "version",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Version"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "canonical_uri",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Canonical URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_nkls",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_npxp",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "publisher_id",
|
||||
"fieldtype": "Data",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Publisher ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"label": "URL",
|
||||
"options": "URL"
|
||||
},
|
||||
{
|
||||
"description": "This value shall be used when no matching Common Code for a record is found.",
|
||||
"fieldname": "default_common_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Common Code",
|
||||
"options": "Common Code"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Common Code",
|
||||
"link_fieldname": "code_list"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-16 17:01:40.260293",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Code List",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lxml.etree import Element
|
||||
|
||||
|
||||
class CodeList(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
canonical_uri: DF.Data | None
|
||||
default_common_code: DF.Link | None
|
||||
description: DF.SmallText | None
|
||||
publisher: DF.Data | None
|
||||
publisher_id: DF.Data | None
|
||||
title: DF.Data | None
|
||||
url: DF.Data | None
|
||||
version: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def on_trash(self):
|
||||
if not frappe.flags.in_bulk_delete:
|
||||
self.__delete_linked_docs()
|
||||
|
||||
def __delete_linked_docs(self):
|
||||
self.db_set("default_common_code", None)
|
||||
|
||||
linked_docs = frappe.get_all(
|
||||
"Common Code",
|
||||
filters={"code_list": self.name},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
for doc in linked_docs:
|
||||
frappe.delete_doc("Common Code", doc.name)
|
||||
|
||||
def get_codes_for(self, doctype: str, name: str) -> tuple[str]:
|
||||
"""Get the applicable codes for a doctype and name"""
|
||||
return get_codes_for(self.name, doctype, name)
|
||||
|
||||
def get_docnames_for(self, doctype: str, code: str) -> tuple[str]:
|
||||
"""Get the mapped docnames for a doctype and code"""
|
||||
return get_docnames_for(self.name, doctype, code)
|
||||
|
||||
def get_default_code(self) -> str | None:
|
||||
"""Get the default common code for this code list"""
|
||||
return (
|
||||
frappe.db.get_value("Common Code", self.default_common_code, "common_code")
|
||||
if self.default_common_code
|
||||
else None
|
||||
)
|
||||
|
||||
def from_genericode(self, root: "Element"):
|
||||
"""Extract Code List details from genericode XML"""
|
||||
self.title = root.find(".//Identification/ShortName").text
|
||||
self.version = root.find(".//Identification/Version").text
|
||||
self.canonical_uri = root.find(".//CanonicalUri").text
|
||||
# optionals
|
||||
self.description = getattr(root.find(".//Identification/LongName"), "text", None)
|
||||
self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None)
|
||||
if not self.publisher:
|
||||
self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None)
|
||||
self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None)
|
||||
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
|
||||
|
||||
|
||||
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
|
||||
"""Return the common code for a given record"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
codes = (
|
||||
frappe.qb.from_(CommonCode)
|
||||
.join(DynamicLink)
|
||||
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||
.select(CommonCode.common_code)
|
||||
.where(
|
||||
(DynamicLink.link_doctype == doctype)
|
||||
& (DynamicLink.link_name == name)
|
||||
& (CommonCode.code_list == code_list)
|
||||
)
|
||||
.distinct()
|
||||
.orderby(CommonCode.common_code)
|
||||
).run()
|
||||
|
||||
return tuple(c[0] for c in codes) if codes else ()
|
||||
|
||||
|
||||
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
|
||||
"""Return the record name for a given common code"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
docnames = (
|
||||
frappe.qb.from_(CommonCode)
|
||||
.join(DynamicLink)
|
||||
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||
.select(DynamicLink.link_name)
|
||||
.where(
|
||||
(DynamicLink.link_doctype == doctype)
|
||||
& (CommonCode.common_code == code)
|
||||
& (CommonCode.code_list == code_list)
|
||||
)
|
||||
.distinct()
|
||||
.orderby(DynamicLink.idx)
|
||||
).run()
|
||||
|
||||
return tuple(d[0] for d in docnames) if docnames else ()
|
||||
|
||||
|
||||
def get_default_code(code_list: str) -> str | None:
|
||||
"""Return the default common code for a given code list"""
|
||||
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
|
||||
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None
|
||||
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
@@ -0,0 +1,218 @@
|
||||
frappe.provide("erpnext.edi");
|
||||
|
||||
erpnext.edi.import_genericode = function (listview_or_form) {
|
||||
let doctype = "Code List";
|
||||
let docname = undefined;
|
||||
if (listview_or_form.doc !== undefined) {
|
||||
docname = listview_or_form.doc.name;
|
||||
}
|
||||
new frappe.ui.FileUploader({
|
||||
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
allow_toggle_private: false,
|
||||
allow_take_photo: false,
|
||||
on_success: function (_file_doc, r) {
|
||||
listview_or_form.refresh();
|
||||
show_column_selection_dialog(r.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function show_column_selection_dialog(context) {
|
||||
let title_description = __("If there is no title column, use the code column for the title.");
|
||||
let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]);
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "code_list_info",
|
||||
options: `<div class="text-muted">${__(
|
||||
"You are importing data for the code list:"
|
||||
)} ${frappe.utils.get_form_link(
|
||||
"Code List",
|
||||
context.code_list,
|
||||
true,
|
||||
context.code_list_title
|
||||
)}</div>`,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "import_column",
|
||||
label: __("Import"),
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldname: "title_column",
|
||||
label: __("as Title"),
|
||||
fieldtype: "Select",
|
||||
reqd: 1,
|
||||
options: context.columns,
|
||||
default: default_title,
|
||||
description: default_title ? null : title_description,
|
||||
},
|
||||
{
|
||||
fieldname: "code_column",
|
||||
label: __("as Code"),
|
||||
fieldtype: "Select",
|
||||
options: context.columns,
|
||||
reqd: 1,
|
||||
default: get_default(context.columns, ["code", "Code", "value"]),
|
||||
},
|
||||
{
|
||||
fieldname: "filters_column",
|
||||
label: __("Filter"),
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
];
|
||||
|
||||
if (context.columns.length > 2) {
|
||||
fields.splice(5, 0, {
|
||||
fieldname: "description_column",
|
||||
label: __("as Description"),
|
||||
fieldtype: "Select",
|
||||
options: [null].concat(context.columns),
|
||||
default: get_default(context.columns, [
|
||||
"description",
|
||||
"Description",
|
||||
"remark",
|
||||
__("description"),
|
||||
__("Description"),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
// Add filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
fields.push({
|
||||
fieldname: `filter_${column}`,
|
||||
label: __("by {}", [column]),
|
||||
fieldtype: "Select",
|
||||
options: [null].concat(context.filterable_columns[column]),
|
||||
});
|
||||
}
|
||||
|
||||
fields.push(
|
||||
{
|
||||
fieldname: "preview_section",
|
||||
label: __("Preview"),
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "preview_html",
|
||||
fieldtype: "HTML",
|
||||
}
|
||||
);
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Select Columns and Filters"),
|
||||
fields: fields,
|
||||
primary_action_label: __("Import"),
|
||||
size: "large", // This will make the modal wider
|
||||
primary_action(values) {
|
||||
let filters = {};
|
||||
for (let field in values) {
|
||||
if (field.startsWith("filter_") && values[field]) {
|
||||
filters[field.replace("filter_", "")] = values[field];
|
||||
}
|
||||
}
|
||||
frappe
|
||||
.xcall("erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", {
|
||||
code_list_name: context.code_list,
|
||||
file_name: context.file,
|
||||
code_column: values.code_column,
|
||||
title_column: values.title_column,
|
||||
description_column: values.description_column,
|
||||
filters: filters,
|
||||
})
|
||||
.then((count) => {
|
||||
frappe.msgprint(__("Import completed. {0} common codes created.", [count]));
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
|
||||
d.fields_dict.code_column.df.onchange = () => update_preview(d, context);
|
||||
d.fields_dict.title_column.df.onchange = (e) => {
|
||||
let field = d.fields_dict.title_column;
|
||||
if (!e.target.value) {
|
||||
field.df.description = title_description;
|
||||
field.refresh();
|
||||
} else {
|
||||
field.df.description = null;
|
||||
field.refresh();
|
||||
}
|
||||
update_preview(d, context);
|
||||
};
|
||||
|
||||
// Add onchange events for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
d.fields_dict[`filter_${column}`].df.onchange = () => update_preview(d, context);
|
||||
}
|
||||
|
||||
d.show();
|
||||
update_preview(d, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first key from the keys array that is found in the columns array.
|
||||
*/
|
||||
function get_default(columns, keys) {
|
||||
return keys.find((key) => columns.includes(key));
|
||||
}
|
||||
|
||||
function update_preview(dialog, context) {
|
||||
let code_column = dialog.get_value("code_column");
|
||||
let title_column = dialog.get_value("title_column");
|
||||
let description_column = dialog.get_value("description_column");
|
||||
|
||||
let html = '<table class="table table-bordered"><thead><tr>';
|
||||
if (title_column) html += `<th>${__("Title")}</th>`;
|
||||
if (code_column) html += `<th>${__("Code")}</th>`;
|
||||
if (description_column) html += `<th>${__("Description")}</th>`;
|
||||
|
||||
// Add headers for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
if (dialog.get_value(`filter_${column}`)) {
|
||||
html += `<th>${__(column)}</th>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += "</tr></thead><tbody>";
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
html += "<tr>";
|
||||
if (title_column) {
|
||||
let title = context.example_values[title_column][i] || "";
|
||||
html += `<td title="${title}">${truncate(title)}</td>`;
|
||||
}
|
||||
if (code_column) {
|
||||
let code = context.example_values[code_column][i] || "";
|
||||
html += `<td title="${code}">${truncate(code)}</td>`;
|
||||
}
|
||||
if (description_column) {
|
||||
let description = context.example_values[description_column][i] || "";
|
||||
html += `<td title="${description}">${truncate(description)}</td>`;
|
||||
}
|
||||
|
||||
// Add values for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
if (dialog.get_value(`filter_${column}`)) {
|
||||
let value = context.example_values[column][i] || "";
|
||||
html += `<td title="${value}">${truncate(value)}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += "</tr>";
|
||||
}
|
||||
|
||||
html += "</tbody></table>";
|
||||
|
||||
dialog.fields_dict.preview_html.$wrapper.html(html);
|
||||
}
|
||||
|
||||
function truncate(value, maxLength = 40) {
|
||||
if (typeof value !== "string") return "";
|
||||
return value.length > maxLength ? value.substring(0, maxLength - 3) + "..." : value;
|
||||
}
|
||||
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from lxml import etree
|
||||
|
||||
URL_PREFIXES = ("http://", "https://")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_genericode():
|
||||
doctype = "Code List"
|
||||
docname = frappe.form_dict.docname
|
||||
content = frappe.local.uploaded_file
|
||||
|
||||
# recover the content, if it's a link
|
||||
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
|
||||
try:
|
||||
# If it's a URL, fetch the content and make it a local file (for durable audit)
|
||||
response = requests.get(frappe.local.uploaded_file_url)
|
||||
response.raise_for_status()
|
||||
frappe.local.uploaded_file = content = response.content
|
||||
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
|
||||
frappe.local.uploaded_file_url = None
|
||||
except Exception as e:
|
||||
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
|
||||
|
||||
if file_url := frappe.local.uploaded_file_url:
|
||||
file_path = frappe.utils.file_manager.get_file_path(file_url)
|
||||
with open(file_path.encode(), mode="rb") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse the xml content
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
try:
|
||||
root = etree.fromstring(content, parser=parser)
|
||||
except Exception as e:
|
||||
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
|
||||
|
||||
# Extract the name (CanonicalVersionUri) from the parsed XML
|
||||
name = root.find(".//CanonicalVersionUri").text
|
||||
docname = docname or name
|
||||
|
||||
if frappe.db.exists(doctype, docname):
|
||||
code_list = frappe.get_doc(doctype, docname)
|
||||
if code_list.name != name:
|
||||
frappe.throw(_("The uploaded file does not match the selected Code List."))
|
||||
else:
|
||||
# Create a new Code List document with the extracted name
|
||||
code_list = frappe.new_doc(doctype)
|
||||
code_list.name = name
|
||||
|
||||
code_list.from_genericode(root)
|
||||
code_list.save()
|
||||
|
||||
# Attach the file and provide a recoverable identifier
|
||||
file_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"attached_to_doctype": "Code List",
|
||||
"attached_to_name": code_list.name,
|
||||
"folder": "Home/Attachments",
|
||||
"file_name": frappe.local.uploaded_filename,
|
||||
"file_url": frappe.local.uploaded_file_url,
|
||||
"is_private": 1,
|
||||
"content": content,
|
||||
}
|
||||
).save()
|
||||
|
||||
# Get available columns and example values
|
||||
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
|
||||
|
||||
return {
|
||||
"code_list": code_list.name,
|
||||
"code_list_title": code_list.title,
|
||||
"file": file_doc.name,
|
||||
"columns": columns,
|
||||
"example_values": example_values,
|
||||
"filterable_columns": filterable_columns,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def process_genericode_import(
|
||||
code_list_name: str,
|
||||
file_name: str,
|
||||
code_column: str,
|
||||
title_column: str | None = None,
|
||||
description_column: str | None = None,
|
||||
filters: str | None = None,
|
||||
):
|
||||
from erpnext.edi.doctype.common_code.common_code import import_genericode
|
||||
|
||||
column_map = {"code": code_column, "title": title_column, "description": description_column}
|
||||
|
||||
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
|
||||
|
||||
|
||||
def get_genericode_columns_and_examples(root):
|
||||
columns = []
|
||||
example_values = {}
|
||||
filterable_columns = {}
|
||||
|
||||
# Get column names
|
||||
for column in root.findall(".//Column"):
|
||||
column_id = column.get("Id")
|
||||
columns.append(column_id)
|
||||
example_values[column_id] = []
|
||||
filterable_columns[column_id] = set()
|
||||
|
||||
# Get all values and count unique occurrences
|
||||
for row in root.findall(".//SimpleCodeList/Row"):
|
||||
for value in row.findall("Value"):
|
||||
column_id = value.get("ColumnRef")
|
||||
if column_id not in columns:
|
||||
# Handle undeclared column
|
||||
columns.append(column_id)
|
||||
example_values[column_id] = []
|
||||
filterable_columns[column_id] = set()
|
||||
|
||||
simple_value = value.find("./SimpleValue")
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
filterable_columns[column_id].add(simple_value.text)
|
||||
|
||||
# Get example values (up to 3) and filter columns with cardinality <= 5
|
||||
for row in root.findall(".//SimpleCodeList/Row")[:3]:
|
||||
for value in row.findall("Value"):
|
||||
column_id = value.get("ColumnRef")
|
||||
simple_value = value.find("./SimpleValue")
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
example_values[column_id].append(simple_value.text)
|
||||
|
||||
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}
|
||||
|
||||
return columns, example_values, filterable_columns
|
||||
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
@@ -0,0 +1,8 @@
|
||||
frappe.listview_settings["Code List"] = {
|
||||
onload: function (listview) {
|
||||
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(listview);
|
||||
});
|
||||
},
|
||||
hide_name_column: true,
|
||||
};
|
||||
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCodeList(FrappeTestCase):
|
||||
pass
|
||||
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Common Code", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2024-09-29 07:01:18.133067",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code_list",
|
||||
"title",
|
||||
"common_code",
|
||||
"description",
|
||||
"column_break_wxsw",
|
||||
"additional_data",
|
||||
"section_break_rhgh",
|
||||
"applies_to"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "code_list",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Code List",
|
||||
"options": "Code List",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Title",
|
||||
"length": 300,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wxsw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_rhgh",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "applies_to",
|
||||
"fieldtype": "Table",
|
||||
"label": "Applies To",
|
||||
"options": "Dynamic Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "common_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Common Code",
|
||||
"length": 300,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_data",
|
||||
"fieldtype": "Code",
|
||||
"label": "Additional Data",
|
||||
"max_height": "190px",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"max_height": "60px"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-11-06 07:46:17.175687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Common Code",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "common_code,description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import hashlib
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import get_link_to_form
|
||||
from lxml import etree
|
||||
|
||||
|
||||
class CommonCode(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink
|
||||
from frappe.types import DF
|
||||
|
||||
additional_data: DF.Code | None
|
||||
applies_to: DF.Table[DynamicLink]
|
||||
code_list: DF.Link
|
||||
common_code: DF.Data
|
||||
description: DF.SmallText | None
|
||||
title: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_distinct_references()
|
||||
|
||||
def validate_distinct_references(self):
|
||||
"""Ensure no two Common Codes of the same Code List are linked to the same document."""
|
||||
for link in self.applies_to:
|
||||
existing_links = frappe.get_all(
|
||||
"Common Code",
|
||||
filters=[
|
||||
["name", "!=", self.name],
|
||||
["code_list", "=", self.code_list],
|
||||
["Dynamic Link", "link_doctype", "=", link.link_doctype],
|
||||
["Dynamic Link", "link_name", "=", link.link_name],
|
||||
],
|
||||
fields=["name", "common_code"],
|
||||
)
|
||||
|
||||
if existing_links:
|
||||
existing_link = existing_links[0]
|
||||
frappe.throw(
|
||||
_("{0} {1} is already linked to Common Code {2}.").format(
|
||||
link.link_doctype,
|
||||
link.link_name,
|
||||
get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]),
|
||||
)
|
||||
)
|
||||
|
||||
def from_genericode(self, column_map: dict, xml_element: "etree.Element"):
|
||||
"""Populate the Common Code document from a genericode XML element
|
||||
|
||||
Args:
|
||||
column_map (dict): A mapping of column names to XML column references. Keys: code, title, description
|
||||
code (etree.Element): The XML element representing a code in the genericode file
|
||||
"""
|
||||
title_column = column_map.get("title")
|
||||
code_column = column_map["code"]
|
||||
description_column = column_map.get("description")
|
||||
|
||||
self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text
|
||||
|
||||
if title_column:
|
||||
simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue")
|
||||
self.title = simple_value_title.text if simple_value_title is not None else self.common_code
|
||||
|
||||
if description_column:
|
||||
simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue")
|
||||
self.description = simple_value_descr.text if simple_value_descr is not None else None
|
||||
|
||||
self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True)
|
||||
|
||||
|
||||
def simple_hash(input_string, length=6):
|
||||
return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest()
|
||||
|
||||
|
||||
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
|
||||
"""Import genericode file and create Common Code entries"""
|
||||
file_path = frappe.utils.file_manager.get_file_path(file_name)
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser=parser)
|
||||
root = tree.getroot()
|
||||
|
||||
# Construct the XPath expression
|
||||
xpath_expr = ".//SimpleCodeList/Row"
|
||||
filter_conditions = [
|
||||
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
|
||||
]
|
||||
if filter_conditions:
|
||||
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
|
||||
|
||||
elements = root.xpath(xpath_expr)
|
||||
total_elements = len(elements)
|
||||
for i, xml_element in enumerate(elements, start=1):
|
||||
common_code: "CommonCode" = frappe.new_doc("Common Code")
|
||||
common_code.code_list = code_list
|
||||
common_code.from_genericode(column_map, xml_element)
|
||||
common_code.save()
|
||||
frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes"))
|
||||
|
||||
return total_elements
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Common Code", ["code_list", "common_code"])
|
||||
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
@@ -0,0 +1,8 @@
|
||||
frappe.listview_settings["Common Code"] = {
|
||||
onload: function (listview) {
|
||||
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(listview);
|
||||
});
|
||||
},
|
||||
hide_name_column: true,
|
||||
};
|
||||
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCommonCode(FrappeTestCase):
|
||||
pass
|
||||
@@ -25,6 +25,14 @@ doctype_js = {
|
||||
"Newsletter": "public/js/newsletter.js",
|
||||
"Contact": "public/js/contact.js",
|
||||
}
|
||||
doctype_list_js = {
|
||||
"Code List": [
|
||||
"edi/doctype/code_list/code_list_import.js",
|
||||
],
|
||||
"Common Code": [
|
||||
"edi/doctype/code_list/code_list_import.js",
|
||||
],
|
||||
}
|
||||
|
||||
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Completed Qty",
|
||||
"reqd": 1
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
@@ -74,4 +74,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ Loan Management
|
||||
Telephony
|
||||
Bulk Transaction
|
||||
E-commerce
|
||||
Subcontracting
|
||||
Subcontracting
|
||||
EDI
|
||||
|
||||
@@ -38,7 +38,7 @@ def execute():
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company
|
||||
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company, creation
|
||||
FROM
|
||||
`tabStock Ledger Entry`
|
||||
WHERE
|
||||
@@ -67,6 +67,7 @@ def execute():
|
||||
"voucher_type": d.voucher_type,
|
||||
"voucher_no": d.voucher_no,
|
||||
"sle_id": d.name,
|
||||
"creation": d.creation,
|
||||
},
|
||||
allow_negative_stock=True,
|
||||
)
|
||||
|
||||
@@ -1104,6 +1104,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
|
||||
me.apply_price_list(item, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,6 +100,7 @@ erpnext.accounts.unreconcile_payment = {
|
||||
fieldtype: "Table",
|
||||
read_only: 1,
|
||||
fields: child_table_fields,
|
||||
cannot_add_rows: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -123,7 +124,6 @@ erpnext.accounts.unreconcile_payment = {
|
||||
title: "UnReconcile Allocations",
|
||||
fields: unreconcile_dialog_fields,
|
||||
size: "large",
|
||||
cannot_add_rows: true,
|
||||
primary_action_label: "UnReconcile",
|
||||
primary_action(values) {
|
||||
let selected_allocations = values.allocations.filter((x) => x.__checked);
|
||||
|
||||
@@ -918,7 +918,10 @@ def get_events(start, end, filters=None):
|
||||
""",
|
||||
{"start": start, "end": end},
|
||||
as_dict=True,
|
||||
update={"allDay": 0},
|
||||
update={
|
||||
"allDay": 0,
|
||||
"convertToUserTz": 0,
|
||||
},
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ frappe.views.calendar["Sales Order"] = {
|
||||
id: "name",
|
||||
title: "customer_name",
|
||||
allDay: "allDay",
|
||||
convertToUserTz: "convertToUserTz",
|
||||
},
|
||||
gantt: true,
|
||||
filters: [
|
||||
|
||||
@@ -277,7 +277,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
};
|
||||
this.warehouse_control.df.get_query = () => {
|
||||
return {
|
||||
filters: { company: this.events.get_frm().doc.company },
|
||||
filters: { company: this.events.get_frm().doc.company, is_group: 0 },
|
||||
};
|
||||
};
|
||||
this.warehouse_control.refresh();
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
extend_cscript(cur_frm.cscript, {
|
||||
onload: function () {
|
||||
if (cur_frm.doc.__islocal) {
|
||||
cur_frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
|
||||
frappe.ui.form.on("Currency Exchange", {
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
cur_frm.cscript.set_exchange_rate_label();
|
||||
refresh: function (frm) {
|
||||
// Don't trigger on Quick Entry form
|
||||
if (typeof frm.is_dialog === "undefined") {
|
||||
frm.trigger("set_exchange_rate_label");
|
||||
}
|
||||
},
|
||||
|
||||
from_currency: function () {
|
||||
cur_frm.cscript.set_exchange_rate_label();
|
||||
from_currency: function (frm) {
|
||||
frm.trigger("set_exchange_rate_label");
|
||||
},
|
||||
|
||||
to_currency: function () {
|
||||
cur_frm.cscript.set_exchange_rate_label();
|
||||
to_currency: function (frm) {
|
||||
frm.trigger("set_exchange_rate_label");
|
||||
},
|
||||
|
||||
set_exchange_rate_label: function () {
|
||||
if (cur_frm.doc.from_currency && cur_frm.doc.to_currency) {
|
||||
var default_label = __(frappe.meta.docfield_map[cur_frm.doctype]["exchange_rate"].label);
|
||||
cur_frm.fields_dict.exchange_rate.set_label(
|
||||
default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", cur_frm.doc)
|
||||
set_exchange_rate_label: function (frm) {
|
||||
if (frm.doc.from_currency && frm.doc.to_currency) {
|
||||
var default_label = __(frappe.meta.docfield_map[frm.doctype]["exchange_rate"].label);
|
||||
frm.fields_dict.exchange_rate.set_label(
|
||||
default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", frm.doc)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -47,7 +47,23 @@ frappe.query_reports["Stock Ledger Variance"] = {
|
||||
fieldname: "difference_in",
|
||||
fieldtype: "Select",
|
||||
label: __("Difference In"),
|
||||
options: ["", "Qty", "Value", "Valuation"],
|
||||
options: [
|
||||
{
|
||||
// Check "Stock Ledger Invariant Check" report with A - B column
|
||||
label: __("Quantity (A - B)"),
|
||||
value: "Qty",
|
||||
},
|
||||
{
|
||||
// Check "Stock Ledger Invariant Check" report with G - D column
|
||||
label: __("Value (G - D)"),
|
||||
value: "Value",
|
||||
},
|
||||
{
|
||||
// Check "Stock Ledger Invariant Check" report with I - K column
|
||||
label: __("Valuation (I - K)"),
|
||||
value: "Valuation",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldname: "include_disabled",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
@@ -270,12 +272,16 @@ def has_difference(row, precision, difference_in, valuation_method):
|
||||
value_diff = flt(row.diff_value_diff, precision)
|
||||
valuation_diff = flt(row.valuation_diff, precision)
|
||||
else:
|
||||
qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
|
||||
value_diff = (
|
||||
flt(row.diff_value_diff, precision)
|
||||
or flt(row.fifo_value_diff, precision)
|
||||
or flt(row.fifo_difference_diff, precision)
|
||||
)
|
||||
qty_diff = flt(row.difference_in_qty, precision)
|
||||
value_diff = flt(row.diff_value_diff, precision)
|
||||
|
||||
if row.stock_queue and json.loads(row.stock_queue):
|
||||
value_diff = value_diff or (
|
||||
flt(row.fifo_value_diff, precision) or flt(row.fifo_difference_diff, precision)
|
||||
)
|
||||
|
||||
qty_diff = qty_diff or flt(row.fifo_qty_diff, precision)
|
||||
|
||||
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
|
||||
|
||||
if difference_in == "Qty" and qty_diff:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user