mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 04:28:27 +00:00
Merge pull request #54584 from frappe/version-15-hotfix
This commit is contained in:
@@ -2306,22 +2306,20 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
# Get positive outstanding sales /purchase invoices
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
condition = " and voucher_type={} and voucher_no={}".format(
|
||||
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
|
||||
)
|
||||
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
|
||||
common_filter.append(ple.voucher_type == args["voucher_type"])
|
||||
common_filter.append(ple.voucher_no == args["voucher_no"])
|
||||
|
||||
# Add cost center condition
|
||||
if args.get("cost_center"):
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
|
||||
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if args.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
|
||||
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
@@ -2331,17 +2329,15 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
|
||||
for fieldname, date_fields in date_fields_dict.items():
|
||||
if args.get(date_fields[0]) and args.get(date_fields[1]):
|
||||
condition += " and {} between '{}' and '{}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
condition += f" and {fieldname} between {frappe.db.escape(args.get(date_fields[0]))} and {frappe.db.escape(args.get(date_fields[1]))}"
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
elif args.get(date_fields[0]):
|
||||
# if only from date is supplied
|
||||
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
|
||||
condition += f" and {fieldname} >= {frappe.db.escape(args.get(date_fields[0]))}"
|
||||
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||
elif args.get(date_fields[1]):
|
||||
# if only to date is supplied
|
||||
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
|
||||
condition += f" and {fieldname} <= {frappe.db.escape(args.get(date_fields[1]))}"
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
@@ -2561,7 +2557,7 @@ def get_orders_to_be_billed(
|
||||
active_dimensions = get_dimensions(True)[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
|
||||
@@ -200,6 +200,30 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||
self.assertEqual(outstanding_amount, 100)
|
||||
|
||||
def test_reference_outstanding_amount_on_advance_pull(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
so = make_sales_order(qty=1, rate=1000)
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
pe.paid_amount = pe.received_amount = 500
|
||||
pe.references[0].allocated_amount = 500
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.advance_paid, 500)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.allocate_advances_automatically = 1
|
||||
si.save()
|
||||
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
|
||||
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
|
||||
si.submit()
|
||||
|
||||
pe.load_from_db()
|
||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
|
||||
|
||||
def test_payment_entry_against_pi(self):
|
||||
pi = make_purchase_invoice(
|
||||
supplier="_Test Supplier USD",
|
||||
@@ -1937,6 +1961,37 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
|
||||
|
||||
def test_project_name_in_exchange_gain_loss_entry(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
do_not_submit=True,
|
||||
)
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
|
||||
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
|
||||
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
|
||||
pe.source_exchange_rate = 100
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
rows = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
|
||||
fields=["project"],
|
||||
)
|
||||
self.assertEqual(len(rows), 2)
|
||||
|
||||
self.assertEqual(rows[0].project, si.project)
|
||||
self.assertEqual(rows[1].project, si.project)
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
from_date: frm.doc.period_start_date,
|
||||
to_date: frm.doc.period_end_date,
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
|
||||
@@ -34,6 +34,17 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_account",
|
||||
label: __("Payable Account"),
|
||||
|
||||
@@ -120,3 +120,49 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
def test_project_filter(self):
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
|
||||
).insert()
|
||||
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.project = project.name
|
||||
pi.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"project": [project.name],
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
row = report[0]
|
||||
self.assertEqual(row.project, project.name)
|
||||
self.assertEqual(row.invoiced, 300.0)
|
||||
|
||||
def test_project_on_report_output(self):
|
||||
"""
|
||||
Report row must carry the invoice's project.
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
|
||||
).insert()
|
||||
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.project = project.name
|
||||
pi.save().submit()
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])
|
||||
|
||||
@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
|
||||
@@ -36,6 +36,17 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
|
||||
@@ -194,6 +194,7 @@ class ReceivablePayableReport:
|
||||
and ple.against_voucher_type in self.advance_payment_doctypes
|
||||
):
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
self.voucher_balance[key].project = ple.project
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -360,6 +361,7 @@ class ReceivablePayableReport:
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
project,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
@@ -388,6 +390,7 @@ class ReceivablePayableReport:
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
"project",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
@@ -925,6 +928,7 @@ class ReceivablePayableReport:
|
||||
ple.against_voucher_no,
|
||||
ple.party_type,
|
||||
ple.cost_center,
|
||||
ple.project,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
@@ -992,6 +996,9 @@ class ReceivablePayableReport:
|
||||
if self.filters.cost_center:
|
||||
self.get_cost_center_conditions()
|
||||
|
||||
if self.filters.project:
|
||||
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
|
||||
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def get_cost_center_conditions(self):
|
||||
@@ -1231,6 +1238,7 @@ class ReceivablePayableReport:
|
||||
)
|
||||
|
||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
|
||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||
self.add_column(
|
||||
label=_("Voucher No"),
|
||||
@@ -1403,6 +1411,7 @@ class InitSQLProceduresForAR:
|
||||
posting_date date,
|
||||
account_currency {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
project {_varchar_type},
|
||||
invoiced {_currency_type},
|
||||
paid {_currency_type},
|
||||
credit_note {_currency_type},
|
||||
@@ -1422,6 +1431,7 @@ class InitSQLProceduresForAR:
|
||||
against_voucher_no {_varchar_type},
|
||||
party_type {_varchar_type},
|
||||
cost_center {_varchar_type},
|
||||
project {_varchar_type},
|
||||
party {_varchar_type},
|
||||
posting_date date,
|
||||
due_date date,
|
||||
@@ -1450,7 +1460,7 @@ class InitSQLProceduresForAR:
|
||||
begin
|
||||
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
|
||||
then
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
@@ -1492,7 +1502,7 @@ class InitSQLProceduresForAR:
|
||||
|
||||
end if;
|
||||
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
|
||||
end;
|
||||
"""
|
||||
|
||||
|
||||
@@ -1204,3 +1204,52 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
def test_project_filter(self):
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
|
||||
).insert()
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.project = project.name
|
||||
si.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"project": [project.name],
|
||||
}
|
||||
|
||||
report = execute(filters)[1]
|
||||
self.assertEqual(len(report), 1)
|
||||
row = report[0]
|
||||
self.assertEqual(row.project, project.name)
|
||||
self.assertEqual(row.invoiced, 100.0)
|
||||
|
||||
def test_project_on_report_output(self):
|
||||
"""
|
||||
Report row must carry the invoice's project even when the payment entry
|
||||
has no project set.
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
}
|
||||
|
||||
project = frappe.get_doc(
|
||||
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
|
||||
).insert()
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.project = project.name
|
||||
si.save().submit()
|
||||
|
||||
# payment has no project — report row must still show the invoice's project
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])
|
||||
|
||||
@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Project",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
|
||||
@@ -501,7 +501,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
|
||||
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
|
||||
from `tabPurchase Taxes and Charges`
|
||||
where parent in (%s) and category in ('Total', 'Valuation and Total')
|
||||
and base_tax_amount_after_discount_amount != 0
|
||||
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
|
||||
group by parent, account_head, add_deduct_tax
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
|
||||
@@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext.accounts.report.purchase_register.purchase_register import execute
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
|
||||
class TestPurchaseRegister(FrappeTestCase):
|
||||
@@ -26,6 +27,52 @@ class TestPurchaseRegister(FrappeTestCase):
|
||||
self.assertEqual(first_row.total_tax, 100)
|
||||
self.assertEqual(first_row.grand_total, 1100)
|
||||
|
||||
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
|
||||
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
|
||||
|
||||
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
|
||||
|
||||
pi = make_purchase_invoice()
|
||||
|
||||
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company 6",
|
||||
supplier="_Test Supplier",
|
||||
item="_Test Item",
|
||||
warehouse="_Test Warehouse - _TC6",
|
||||
cost_center="_Test Cost Center - _TC6",
|
||||
do_not_save=1,
|
||||
do_not_submit=1,
|
||||
qty=1,
|
||||
rate=1000,
|
||||
)
|
||||
pr.append(
|
||||
"taxes",
|
||||
{
|
||||
"account_head": "GST - _TC6",
|
||||
"cost_center": "_Test Cost Center - _TC6",
|
||||
"add_deduct_tax": "Add",
|
||||
"category": "Valuation and Total",
|
||||
"charge_type": "Actual",
|
||||
"description": "PR Tax",
|
||||
"tax_amount": 100.0,
|
||||
"rate": 100,
|
||||
},
|
||||
)
|
||||
pr.insert()
|
||||
pr.submit()
|
||||
|
||||
# Mimic custom naming collision across doctypes (same parent value in shared child table).
|
||||
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
|
||||
|
||||
report_results = execute(filters)
|
||||
first_row = frappe._dict(report_results[1][0])
|
||||
|
||||
self.assertEqual(first_row.voucher_no, pi.name)
|
||||
self.assertEqual(first_row.total_tax, 100)
|
||||
self.assertEqual(first_row.grand_total, 1100)
|
||||
|
||||
def test_purchase_register_ledger_view(self):
|
||||
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
|
||||
|
||||
@@ -5,6 +5,7 @@ from frappe.utils import getdate, today
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.sales_register.sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -75,6 +76,43 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
report_output = {k: v for k, v in res[0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
|
||||
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
|
||||
si = self.create_sales_invoice(rate=98)
|
||||
|
||||
# Real workflow setup: create a Sales Order with taxes in the shared child table.
|
||||
so = make_sales_order(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
rate=77,
|
||||
do_not_save=1,
|
||||
do_not_submit=1,
|
||||
)
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "Actual",
|
||||
"account_head": self.income_account,
|
||||
"description": "SO Tax",
|
||||
"tax_amount": 55.0,
|
||||
},
|
||||
)
|
||||
so.insert()
|
||||
so.submit()
|
||||
|
||||
# Mimic custom naming collision across doctypes (same parent value in shared child table).
|
||||
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
res = [x for x in report[1] if x.get("voucher_no") == si.name]
|
||||
self.assertEqual(len(res), 1)
|
||||
result = frappe._dict(res[0])
|
||||
self.assertEqual(result.net_total, 98.0)
|
||||
self.assertEqual(result.tax_total, 0)
|
||||
self.assertEqual(result.grand_total, 98.0)
|
||||
|
||||
def test_journal_with_cost_center_filter(self):
|
||||
je1 = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -500,7 +500,7 @@ def reconcile_against_document(
|
||||
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
|
||||
dimensions_dict=dimensions_dict,
|
||||
)
|
||||
if referenced_row.get("outstanding_amount"):
|
||||
if referenced_row.get("outstanding_amount") and entry.get("outstanding_amount") is None:
|
||||
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
|
||||
|
||||
reposting_rows.append(referenced_row)
|
||||
@@ -2320,6 +2320,7 @@ def create_gain_loss_journal(
|
||||
ref2_detail_no,
|
||||
cost_center,
|
||||
dimensions,
|
||||
project=None,
|
||||
) -> str:
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
@@ -2346,6 +2347,7 @@ def create_gain_loss_journal(
|
||||
"account_currency": party_account_currency,
|
||||
"exchange_rate": 0,
|
||||
"cost_center": cost_center or erpnext.get_default_cost_center(company),
|
||||
"project": project,
|
||||
"reference_type": ref1_dt,
|
||||
"reference_name": ref1_dn,
|
||||
"reference_detail_no": ref1_detail_no,
|
||||
@@ -2363,6 +2365,7 @@ def create_gain_loss_journal(
|
||||
"account_currency": gain_loss_account_currency,
|
||||
"exchange_rate": 1,
|
||||
"cost_center": cost_center or erpnext.get_default_cost_center(company),
|
||||
"project": project,
|
||||
"reference_type": ref2_dt,
|
||||
"reference_name": ref2_dn,
|
||||
"reference_detail_no": ref2_detail_no,
|
||||
|
||||
@@ -1751,6 +1751,7 @@ class AccountsController(TransactionBase):
|
||||
arg.get("referenced_row"),
|
||||
arg.get("cost_center"),
|
||||
dimensions_dict,
|
||||
arg.get("project"),
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@@ -1835,6 +1836,7 @@ class AccountsController(TransactionBase):
|
||||
d.idx,
|
||||
self.cost_center,
|
||||
dimensions_dict,
|
||||
self.project,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
|
||||
@@ -364,7 +364,17 @@ class BuyingController(SubcontractingController):
|
||||
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
||||
)
|
||||
|
||||
net_rate = item.qty * item.base_net_rate
|
||||
net_rate = (
|
||||
flt(
|
||||
(item.base_net_amount / item.received_qty) * item.qty,
|
||||
item.precision("base_net_amount"),
|
||||
)
|
||||
if item.received_qty
|
||||
and frappe.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
else item.base_net_amount
|
||||
)
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ erpnext.edi.import_genericode = function (listview_or_form) {
|
||||
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
allow_web_link: false,
|
||||
allow_toggle_private: false,
|
||||
allow_take_photo: false,
|
||||
on_success: function (_file_doc, r) {
|
||||
|
||||
@@ -1,48 +1,118 @@
|
||||
import json
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from frappe.utils import escape_html
|
||||
from frappe.utils.file_manager import save_file
|
||||
from lxml import etree
|
||||
|
||||
URL_PREFIXES = ("http://", "https://")
|
||||
GENERICODE_FETCH_TIMEOUT = 15
|
||||
LOCAL_FILE_PREFIXES = ("/files/", "/private/files/")
|
||||
|
||||
|
||||
class RemoteGenericodeUrlNotAllowedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CodeListSelectionMismatchError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@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,
|
||||
resolve_entities=False,
|
||||
load_dtd=False,
|
||||
no_network=True,
|
||||
)
|
||||
try:
|
||||
root = etree.fromstring(content, parser=parser)
|
||||
except Exception as e:
|
||||
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
|
||||
content, file_name = get_uploaded_genericode_file()
|
||||
|
||||
return import_genericode_content(
|
||||
doctype="Code List",
|
||||
docname=frappe.form_dict.docname,
|
||||
content=content,
|
||||
file_name=file_name,
|
||||
)
|
||||
except RemoteGenericodeUrlNotAllowedError:
|
||||
frappe.throw(
|
||||
_("Importing Code Lists from remote URLs is not allowed."),
|
||||
title=_("Invalid Upload"),
|
||||
)
|
||||
except CodeListSelectionMismatchError:
|
||||
frappe.throw(_("The uploaded file does not match the selected Code List."))
|
||||
except etree.XMLSyntaxError:
|
||||
frappe.throw(
|
||||
_("The uploaded file could not be parsed as a genericode XML document."),
|
||||
title=_("Parsing Error"),
|
||||
)
|
||||
|
||||
|
||||
def import_genericode_from_url(
|
||||
url: str,
|
||||
doctype: str = "Code List",
|
||||
docname: str | None = None,
|
||||
):
|
||||
"""Import a Code List from a trusted backend URL."""
|
||||
content = fetch_genericode_from_url(url)
|
||||
file_name = urlsplit(url).path.rsplit("/", 1)[-1] or "genericode.xml"
|
||||
|
||||
return import_genericode_content(
|
||||
doctype=doctype,
|
||||
docname=docname,
|
||||
content=content,
|
||||
file_name=file_name,
|
||||
)
|
||||
|
||||
|
||||
def get_uploaded_genericode_file() -> tuple[bytes, str | None]:
|
||||
uploaded_data = frappe.local.uploaded_file
|
||||
file_name = frappe.local.uploaded_filename
|
||||
if uploaded_data and file_name:
|
||||
return uploaded_data, file_name
|
||||
|
||||
file_url = frappe.local.uploaded_file_url
|
||||
if not file_url:
|
||||
raise frappe.ValidationError(_("No file uploaded or URL provided."))
|
||||
|
||||
if not is_local_file_url(file_url):
|
||||
raise RemoteGenericodeUrlNotAllowedError
|
||||
|
||||
file_doc = frappe.get_doc("File", {"file_url": file_url})
|
||||
file_doc.check_permission("read")
|
||||
return read_file_bytes(file_doc), file_name
|
||||
|
||||
|
||||
def read_file_bytes(file_doc) -> bytes:
|
||||
"""Return the raw bytes of a File document.
|
||||
|
||||
v15's `File.get_content` eagerly decodes to utf-8 and returns `str` for text
|
||||
files, but `lxml.etree.fromstring` needs bytes when the XML declares an encoding.
|
||||
"""
|
||||
content = file_doc.get_content()
|
||||
if isinstance(content, str):
|
||||
content = content.encode("utf-8")
|
||||
return content
|
||||
|
||||
|
||||
def is_local_file_url(file_url: str | None) -> bool:
|
||||
if not file_url:
|
||||
return False
|
||||
|
||||
parsed = urlsplit(file_url.strip())
|
||||
return not parsed.scheme and not parsed.netloc and parsed.path.startswith(LOCAL_FILE_PREFIXES)
|
||||
|
||||
|
||||
def fetch_genericode_from_url(url: str) -> bytes:
|
||||
response = requests.get(url, timeout=GENERICODE_FETCH_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
|
||||
def import_genericode_content(
|
||||
doctype: str,
|
||||
docname: str | None,
|
||||
content: bytes,
|
||||
file_name: str | None,
|
||||
):
|
||||
root = parse_genericode_content(content)
|
||||
|
||||
# Extract the name (CanonicalVersionUri) from the parsed XML
|
||||
name = root.find(".//CanonicalVersionUri").text
|
||||
@@ -51,7 +121,7 @@ def import_genericode():
|
||||
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."))
|
||||
raise CodeListSelectionMismatchError
|
||||
else:
|
||||
# Create a new Code List document with the extracted name
|
||||
code_list = frappe.new_doc(doctype)
|
||||
@@ -60,19 +130,13 @@ def import_genericode():
|
||||
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": frappe.db.get_value("File", {"is_attachments_folder": 1}),
|
||||
"file_name": frappe.local.uploaded_filename,
|
||||
"file_url": frappe.local.uploaded_file_url,
|
||||
"is_private": 1,
|
||||
"content": content,
|
||||
}
|
||||
).save()
|
||||
file_doc = save_file(
|
||||
fname=file_name,
|
||||
content=content,
|
||||
dt=doctype,
|
||||
dn=code_list.name,
|
||||
is_private=1,
|
||||
)
|
||||
|
||||
# Get available columns and example values
|
||||
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
|
||||
@@ -87,6 +151,16 @@ def import_genericode():
|
||||
}
|
||||
|
||||
|
||||
def parse_genericode_content(content: bytes):
|
||||
parser = etree.XMLParser(
|
||||
remove_blank_text=True,
|
||||
resolve_entities=False,
|
||||
load_dtd=False,
|
||||
no_network=True,
|
||||
)
|
||||
return etree.fromstring(content, parser=parser)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def process_genericode_import(
|
||||
code_list_name: str,
|
||||
|
||||
200
erpnext/edi/doctype/code_list/test_code_list_import.py
Normal file
200
erpnext/edi/doctype/code_list/test_code_list_import.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.edi.doctype.code_list import code_list_import
|
||||
|
||||
SAMPLE_GENERICODE = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<CodeList>
|
||||
<Identification>
|
||||
<ShortName>Test Code List</ShortName>
|
||||
<Version>1.0</Version>
|
||||
<CanonicalUri>test-code-list</CanonicalUri>
|
||||
<LongName>Code list for tests</LongName>
|
||||
<Agency>
|
||||
<ShortName>Test Agency</ShortName>
|
||||
<Identifier>TEST</Identifier>
|
||||
</Agency>
|
||||
<LocationUri>https://example.com/codelists/test.xml</LocationUri>
|
||||
</Identification>
|
||||
<CanonicalVersionUri>test-code-list-v1</CanonicalVersionUri>
|
||||
<ColumnSet>
|
||||
<Column Id="code" />
|
||||
<Column Id="name" />
|
||||
<Column Id="category" />
|
||||
</ColumnSet>
|
||||
<SimpleCodeList>
|
||||
<Row>
|
||||
<Value ColumnRef="code"><SimpleValue>A</SimpleValue></Value>
|
||||
<Value ColumnRef="name"><SimpleValue>Alpha</SimpleValue></Value>
|
||||
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
|
||||
</Row>
|
||||
<Row>
|
||||
<Value ColumnRef="code"><SimpleValue>B</SimpleValue></Value>
|
||||
<Value ColumnRef="name"><SimpleValue>Beta</SimpleValue></Value>
|
||||
<Value ColumnRef="category"><SimpleValue>Group 2</SimpleValue></Value>
|
||||
</Row>
|
||||
<Row>
|
||||
<Value ColumnRef="code"><SimpleValue>C</SimpleValue></Value>
|
||||
<Value ColumnRef="name"><SimpleValue>Gamma</SimpleValue></Value>
|
||||
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
|
||||
</Row>
|
||||
</SimpleCodeList>
|
||||
</CodeList>
|
||||
"""
|
||||
|
||||
|
||||
class TestCodeListImport(FrappeTestCase):
|
||||
def test_import_genericode_rejects_remote_file_url(self):
|
||||
self.set_upload_context(
|
||||
file_name="trusted.xml",
|
||||
file_url="https://example.com/codelists/trusted.xml",
|
||||
)
|
||||
|
||||
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
|
||||
):
|
||||
code_list_import.import_genericode()
|
||||
|
||||
mock_get.assert_not_called()
|
||||
|
||||
def test_import_genericode_rejects_file_scheme_url(self):
|
||||
self.set_upload_context(
|
||||
file_name="trusted.xml",
|
||||
file_url="file:///tmp/trusted.xml",
|
||||
)
|
||||
|
||||
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
|
||||
):
|
||||
code_list_import.import_genericode()
|
||||
|
||||
mock_get.assert_not_called()
|
||||
|
||||
def test_import_genericode_from_trusted_url(self):
|
||||
response = Mock()
|
||||
response.content = SAMPLE_GENERICODE
|
||||
response.raise_for_status.return_value = None
|
||||
|
||||
with patch(
|
||||
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
|
||||
return_value=response,
|
||||
) as mock_get:
|
||||
import_result = code_list_import.import_genericode_from_url(
|
||||
"https://example.com/codelists/trusted.xml"
|
||||
)
|
||||
|
||||
self.assert_import_response(import_result)
|
||||
mock_get.assert_called_once_with(
|
||||
"https://example.com/codelists/trusted.xml",
|
||||
timeout=code_list_import.GENERICODE_FETCH_TIMEOUT,
|
||||
)
|
||||
|
||||
file_doc = frappe.get_doc("File", import_result["file"])
|
||||
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
|
||||
self.assertFalse(file_doc.file_url.startswith("https://"))
|
||||
|
||||
def test_import_genericode_from_trusted_url_propagates_fetch_errors(self):
|
||||
with patch(
|
||||
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
|
||||
side_effect=requests.Timeout,
|
||||
):
|
||||
with self.assertRaises(requests.Timeout):
|
||||
code_list_import.import_genericode_from_url("https://example.com/codelists/trusted.xml")
|
||||
|
||||
def test_import_genericode_from_uploaded_file_returns_metadata(self):
|
||||
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
|
||||
|
||||
import_result = code_list_import.import_genericode()
|
||||
|
||||
self.assert_import_response(import_result)
|
||||
|
||||
file_doc = frappe.get_doc("File", import_result["file"])
|
||||
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
|
||||
|
||||
def test_process_genericode_import_reads_file_doc_content(self):
|
||||
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
|
||||
|
||||
import_result = code_list_import.import_genericode()
|
||||
count = code_list_import.process_genericode_import(
|
||||
code_list_name=import_result["code_list"],
|
||||
file_name=import_result["file"],
|
||||
code_column="code",
|
||||
title_column="name",
|
||||
)
|
||||
|
||||
self.assertEqual(count, 3)
|
||||
self.assertEqual(frappe.db.count("Common Code", {"code_list": import_result["code_list"]}), 3)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Common Code",
|
||||
{"code_list": import_result["code_list"], "common_code": "A"},
|
||||
"title",
|
||||
),
|
||||
"Alpha",
|
||||
)
|
||||
|
||||
def test_import_genericode_from_local_file_url(self):
|
||||
source_file = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": "library_genericode.xml",
|
||||
"content": SAMPLE_GENERICODE,
|
||||
"is_private": 1,
|
||||
}
|
||||
).insert()
|
||||
self.set_upload_context(file_name=source_file.file_name, file_url=source_file.file_url)
|
||||
|
||||
import_result = code_list_import.import_genericode()
|
||||
|
||||
self.assert_import_response(import_result)
|
||||
|
||||
def set_upload_context(
|
||||
self,
|
||||
content: bytes | None = None,
|
||||
file_name: str = "genericode.xml",
|
||||
file_url: str | None = None,
|
||||
docname: str | None = None,
|
||||
):
|
||||
attrs = ("form_dict", "uploaded_file", "uploaded_file_url", "uploaded_filename")
|
||||
originals = {attr: getattr(frappe.local, attr, None) for attr in attrs}
|
||||
|
||||
frappe.local.form_dict = frappe._dict(doctype="Code List", docname=docname)
|
||||
frappe.local.uploaded_file = content
|
||||
frappe.local.uploaded_file_url = file_url
|
||||
frappe.local.uploaded_filename = file_name
|
||||
|
||||
def restore():
|
||||
for attr, value in originals.items():
|
||||
setattr(frappe.local, attr, value)
|
||||
|
||||
self.addCleanup(restore)
|
||||
|
||||
def assert_import_response(self, import_result):
|
||||
self.assertEqual(
|
||||
set(import_result),
|
||||
{
|
||||
"code_list",
|
||||
"code_list_title",
|
||||
"file",
|
||||
"columns",
|
||||
"example_values",
|
||||
"filterable_columns",
|
||||
},
|
||||
)
|
||||
self.assertEqual(import_result["code_list"], "test-code-list-v1")
|
||||
self.assertEqual(import_result["code_list_title"], "Test Code List")
|
||||
self.assertEqual(import_result["columns"], ["code", "name", "category"])
|
||||
self.assertEqual(import_result["example_values"]["code"], ["A", "B", "C"])
|
||||
self.assertEqual(import_result["example_values"]["name"], ["Alpha", "Beta", "Gamma"])
|
||||
self.assertEqual(import_result["example_values"]["category"], ["Group 1", "Group 2", "Group 1"])
|
||||
self.assertCountEqual(import_result["filterable_columns"]["category"], ["Group 1", "Group 2"])
|
||||
self.assertTrue(frappe.db.exists("Code List", import_result["code_list"]))
|
||||
self.assertTrue(frappe.db.exists("File", import_result["file"]))
|
||||
@@ -9,6 +9,8 @@ from frappe.model.document import Document
|
||||
from frappe.utils.data import get_link_to_form
|
||||
from lxml import etree
|
||||
|
||||
from erpnext.edi.doctype.code_list.code_list_import import parse_genericode_content, read_file_bytes
|
||||
|
||||
|
||||
class CommonCode(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -86,15 +88,15 @@ def simple_hash(input_string, length=6):
|
||||
|
||||
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()
|
||||
file_doc = frappe.get_doc("File", file_name)
|
||||
file_doc.check_permission("read")
|
||||
root = parse_genericode_content(read_file_bytes(file_doc))
|
||||
|
||||
# Construct the XPath expression
|
||||
xpath_expr = ".//SimpleCodeList/Row"
|
||||
filter_conditions = [
|
||||
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
|
||||
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'"
|
||||
for column_ref, value in (filters or {}).items()
|
||||
]
|
||||
if filter_conditions:
|
||||
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
|
||||
@@ -102,7 +104,7 @@ def import_genericode(code_list: str, file_name: str, column_map: dict, filters:
|
||||
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: CommonCode = frappe.new_doc("Common Code")
|
||||
common_code.code_list = code_list
|
||||
common_code.from_genericode(column_map, xml_element)
|
||||
common_code.save()
|
||||
|
||||
@@ -120,7 +120,7 @@ class BlanketOrder(Document):
|
||||
|
||||
def validate_item_qty(self):
|
||||
for d in self.items:
|
||||
if d.qty < 0:
|
||||
if flt(d.qty) < 0:
|
||||
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))
|
||||
|
||||
|
||||
|
||||
@@ -228,6 +228,18 @@ class WorkOrder(Document):
|
||||
if self.production_plan_sub_assembly_item:
|
||||
return
|
||||
|
||||
production_item = self.production_item
|
||||
|
||||
if self.material_request_item and (
|
||||
mr_plan_item := frappe.get_value(
|
||||
"Material Request Item", self.material_request_item, "material_request_plan_item"
|
||||
)
|
||||
):
|
||||
if main_item_code := frappe.get_value(
|
||||
"Material Request Plan Item", mr_plan_item, "main_item_code"
|
||||
):
|
||||
production_item = main_item_code
|
||||
|
||||
if self.sales_order:
|
||||
self.check_sales_order_on_hold_or_close()
|
||||
|
||||
@@ -248,8 +260,8 @@ class WorkOrder(Document):
|
||||
& (SalesOrder.docstatus == 1)
|
||||
& (SalesOrder.name == self.sales_order)
|
||||
& (
|
||||
(SalesOrderItem.item_code == self.production_item)
|
||||
| (ProductBundleItem.item_code == self.production_item)
|
||||
(SalesOrderItem.item_code == production_item)
|
||||
| (ProductBundleItem.item_code == production_item)
|
||||
)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
@@ -268,7 +280,7 @@ class WorkOrder(Document):
|
||||
& (SalesOrder.skip_delivery_note == 0)
|
||||
& (SalesOrderItem.item_code == PackedItem.parent_item)
|
||||
& (SalesOrder.docstatus == 1)
|
||||
& (PackedItem.item_code == self.production_item)
|
||||
& (PackedItem.item_code == production_item)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
@@ -10,7 +10,18 @@ def execute():
|
||||
)
|
||||
if data:
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
frappe.db.bulk_update(
|
||||
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
|
||||
)
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
try:
|
||||
frappe.db.bulk_update(
|
||||
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
|
||||
)
|
||||
quotations = frappe.get_all(
|
||||
"Quotation Item",
|
||||
filters={"name": ["in", [d.quotation_item for d in data]]},
|
||||
pluck="parent",
|
||||
distinct=True,
|
||||
)
|
||||
for quotation in quotations:
|
||||
doc = frappe.get_doc("Quotation", quotation)
|
||||
doc.set_status(update=True, update_modified=False)
|
||||
finally:
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ pincode }} {{ city | upper }}<br>
|
||||
{{ country | upper }}
|
||||
4
erpnext/regional/address_template/templates/denmark.html
Normal file
4
erpnext/regional/address_template/templates/denmark.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ pincode }} {{ city | upper }}<br>
|
||||
{{ country | upper }}
|
||||
@@ -123,6 +123,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
|
||||
) {
|
||||
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
this.frm.add_custom_button(__("Update Items"), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: this.frm,
|
||||
@@ -137,8 +138,6 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
this.frm.trigger("set_as_lost_dialog");
|
||||
});
|
||||
}
|
||||
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.core.doctype.sms_settings.sms_settings import send_sms
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import functions as fn
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
@@ -41,73 +42,117 @@ class SMSCenter(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_receiver_list(self):
|
||||
rec, where_clause = "", ""
|
||||
if self.send_to == "All Customer Contact":
|
||||
where_clause = " and dl.link_doctype = 'Customer'"
|
||||
if self.customer:
|
||||
where_clause += (
|
||||
" and dl.link_name = '%s'" % self.customer.replace("'", "'")
|
||||
or " and ifnull(dl.link_name, '') != ''"
|
||||
)
|
||||
if self.send_to == "All Supplier Contact":
|
||||
where_clause = " and dl.link_doctype = 'Supplier'"
|
||||
if self.supplier:
|
||||
where_clause += (
|
||||
" and dl.link_name = '%s'" % self.supplier.replace("'", "'")
|
||||
or " and ifnull(dl.link_name, '') != ''"
|
||||
)
|
||||
if self.send_to == "All Sales Partner Contact":
|
||||
where_clause = " and dl.link_doctype = 'Sales Partner'"
|
||||
if self.sales_partner:
|
||||
where_clause += (
|
||||
"and dl.link_name = '%s'" % self.sales_partner.replace("'", "'")
|
||||
or " and ifnull(dl.link_name, '') != ''"
|
||||
)
|
||||
query = None
|
||||
|
||||
if self.send_to == "":
|
||||
return
|
||||
|
||||
if self.send_to in [
|
||||
"All Contact",
|
||||
"All Customer Contact",
|
||||
"All Supplier Contact",
|
||||
"All Sales Partner Contact",
|
||||
]:
|
||||
rec = frappe.db.sql(
|
||||
"""select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')),
|
||||
c.mobile_no from `tabContact` c, `tabDynamic Link` dl where ifnull(c.mobile_no,'')!='' and
|
||||
c.docstatus != 2 and dl.parent = c.name%s"""
|
||||
% where_clause
|
||||
)
|
||||
query = self.get_contact_query_for_all_contacts()
|
||||
|
||||
elif self.send_to == "All Lead (Open)":
|
||||
rec = frappe.db.sql(
|
||||
"""select lead_name, mobile_no from `tabLead` where
|
||||
ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'"""
|
||||
)
|
||||
query = self.get_contact_query_for_all_open_leads()
|
||||
|
||||
elif self.send_to == "All Employee (Active)":
|
||||
where_clause = (
|
||||
self.department and " and department = '%s'" % self.department.replace("'", "'") or ""
|
||||
)
|
||||
where_clause += self.branch and " and branch = '%s'" % self.branch.replace("'", "'") or ""
|
||||
|
||||
rec = frappe.db.sql(
|
||||
"""select employee_name, cell_number from
|
||||
`tabEmployee` where status = 'Active' and docstatus < 2 and
|
||||
ifnull(cell_number,'')!='' %s"""
|
||||
% where_clause
|
||||
)
|
||||
query = self.get_contact_query_for_all_active_employee()
|
||||
|
||||
elif self.send_to == "All Sales Person":
|
||||
rec = frappe.db.sql(
|
||||
"""select sales_person_name,
|
||||
tabEmployee.cell_number from `tabSales Person` left join tabEmployee
|
||||
on `tabSales Person`.employee = tabEmployee.name
|
||||
where ifnull(tabEmployee.cell_number,'')!=''"""
|
||||
)
|
||||
query = self.get_contact_query_for_all_sales_person()
|
||||
|
||||
rec = query.run(as_list=1)
|
||||
|
||||
rec_list = ""
|
||||
for d in rec:
|
||||
rec_list += d[0] + " - " + d[1] + "\n"
|
||||
self.receiver_list = rec_list
|
||||
|
||||
def get_contact_query_for_all_contacts(self):
|
||||
Contact = frappe.qb.DocType("Contact")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
query = (
|
||||
frappe.qb.from_(Contact)
|
||||
.join(DynamicLink)
|
||||
.on(DynamicLink.parent == Contact.name)
|
||||
.select(
|
||||
fn.Concat(fn.IfNull(Contact.first_name, ""), " ", fn.IfNull(Contact.last_name, "")),
|
||||
Contact.mobile_no,
|
||||
)
|
||||
.where((fn.IfNull(Contact.mobile_no, "") != "") & (Contact.docstatus != 2))
|
||||
)
|
||||
|
||||
if self.send_to == "All Customer Contact":
|
||||
query = query.where(DynamicLink.link_doctype == "Customer")
|
||||
query = (
|
||||
query.where(DynamicLink.link_name == self.customer)
|
||||
if self.customer
|
||||
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
|
||||
)
|
||||
|
||||
elif self.send_to == "All Supplier Contact":
|
||||
query = query.where(DynamicLink.link_doctype == "Supplier")
|
||||
query = (
|
||||
query.where(DynamicLink.link_name == self.supplier)
|
||||
if self.supplier
|
||||
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
|
||||
)
|
||||
|
||||
elif self.send_to == "All Sales Partner Contact":
|
||||
query = query.where(DynamicLink.link_doctype == "Sales Partner")
|
||||
query = (
|
||||
query.where(DynamicLink.link_name == self.sales_partner)
|
||||
if self.sales_partner
|
||||
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
|
||||
)
|
||||
return query
|
||||
|
||||
def get_contact_query_for_all_open_leads(self):
|
||||
Lead = frappe.qb.DocType("Lead")
|
||||
query = (
|
||||
frappe.qb.from_(Lead)
|
||||
.select(Lead.lead_name, Lead.mobile)
|
||||
.where((fn.IfNull(Lead.mobile_no, "") != "") & (Lead.docstatus != 2) & (Lead.status == "Open"))
|
||||
)
|
||||
return query
|
||||
|
||||
def get_contact_query_for_all_active_employee(self):
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
query = (
|
||||
frappe.qb.from_(Employee)
|
||||
.select(Employee.employee_name, Employee.cell_number)
|
||||
.where(
|
||||
(Employee.status == "Active")
|
||||
& (Employee.docstatus != 2)
|
||||
& (fn.IfNull(Employee.cell_number, "") != "")
|
||||
)
|
||||
)
|
||||
|
||||
if self.department:
|
||||
query = query.where(Employee.department == self.department)
|
||||
|
||||
if self.branch:
|
||||
query = query.where(Employee.branch == self.branch)
|
||||
|
||||
return query
|
||||
|
||||
def get_contact_query_for_all_sales_person(self):
|
||||
SalesPerson = frappe.qb.DocType("Sales Person")
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesPerson)
|
||||
.left_join(Employee)
|
||||
.on(SalesPerson.employee == Employee.name)
|
||||
.select(SalesPerson.sales_person_name, Employee.cell_number)
|
||||
.where(fn.IfNull(Employee.cell_number, "") != "")
|
||||
)
|
||||
|
||||
return query
|
||||
|
||||
def get_receiver_nos(self):
|
||||
receiver_nos = []
|
||||
if self.receiver_list:
|
||||
|
||||
@@ -510,7 +510,14 @@ class PurchaseReceipt(BuyingController):
|
||||
else flt(item.net_amount, item.precision("net_amount"))
|
||||
)
|
||||
|
||||
outgoing_amount = item.qty * item.base_net_rate
|
||||
outgoing_amount = (
|
||||
flt((item.base_net_amount / item.received_qty) * item.qty, item.precision("base_net_amount"))
|
||||
if item.received_qty
|
||||
and frappe.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
else item.base_net_amount
|
||||
)
|
||||
if self.is_internal_transfer() and item.valuation_rate:
|
||||
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
|
||||
credit_amount = outgoing_amount
|
||||
|
||||
@@ -394,7 +394,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
f"""
|
||||
SELECT distinct item_code, item_name
|
||||
FROM `tab{from_doctype}`
|
||||
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
|
||||
JOIN `tab{parent_doctype}` ON `tab{parent_doctype}`.name = `tab{from_doctype}`.parent
|
||||
WHERE parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and item_code like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
|
||||
""",
|
||||
|
||||
@@ -925,6 +925,7 @@ class SerialandBatchBundle(Document):
|
||||
parent.voucher_type,
|
||||
parent.voucher_no,
|
||||
)
|
||||
.distinct()
|
||||
.where(
|
||||
(child.parent != self.name)
|
||||
& (parent.item_code == self.item_code)
|
||||
|
||||
@@ -270,8 +270,7 @@
|
||||
"oldfieldname": "transfer_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -617,7 +616,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-02 14:05:23.116017",
|
||||
"modified": "2026-04-27 11:40:38.294196",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
||||
@@ -612,5 +612,5 @@ class FIFOSlots:
|
||||
sr_item = frappe.db.get_value(
|
||||
"Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True
|
||||
)
|
||||
if sr_item.qty and sr_item.current_qty:
|
||||
if sr_item and sr_item.qty and sr_item.current_qty:
|
||||
self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty
|
||||
|
||||
@@ -947,6 +947,9 @@ class update_entries_after:
|
||||
if not self.wh_data.qty_after_transaction:
|
||||
self.wh_data.stock_value = 0.0
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
sle.incoming_rate = 0
|
||||
|
||||
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
|
||||
self.wh_data.prev_stock_value = self.wh_data.stock_value
|
||||
|
||||
|
||||
@@ -10,9 +10,8 @@ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
|
||||
from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_available_serial_nos,
|
||||
)
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import get_available_serial_nos
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
||||
from erpnext.stock.valuation import FIFOValuation, LIFOValuation
|
||||
@@ -124,11 +123,19 @@ def get_stock_balance(
|
||||
}
|
||||
|
||||
extra_cond = ""
|
||||
|
||||
if inventory_dimensions_dict:
|
||||
inventory_dimensions_fieldname = [d.get("fieldname") for d in get_inventory_dimensions()]
|
||||
|
||||
for field, value in inventory_dimensions_dict.items():
|
||||
column = frappe.utils.sanitize_column(field)
|
||||
if field not in inventory_dimensions_fieldname:
|
||||
frappe.throw(
|
||||
_("{0} is not a valid {1} fieldname.").format(
|
||||
frappe.bold(field), frappe.bold("Inventory Dimension")
|
||||
)
|
||||
)
|
||||
args[field] = value
|
||||
extra_cond += f" and {column} = %({field})s"
|
||||
extra_cond += f" and {field} = %({field})s"
|
||||
|
||||
last_entry = get_previous_sle(args, extra_cond=extra_cond)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user