mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 04:29:18 +00:00
Compare commits
36 Commits
mergify/bp
...
v15.104.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aede87290 | ||
|
|
1b2c7ca21f | ||
|
|
db3a40409f | ||
|
|
041f99c926 | ||
|
|
b88f3f69b0 | ||
|
|
dba8abbabf | ||
|
|
c1591c37db | ||
|
|
3a2dc6f9ee | ||
|
|
a2626ed55f | ||
|
|
0cc77274cb | ||
|
|
2597eaad51 | ||
|
|
39aaefc202 | ||
|
|
75344e9e82 | ||
|
|
d39072a689 | ||
|
|
f4a1f04566 | ||
|
|
1d14ba1639 | ||
|
|
a270c02bb4 | ||
|
|
94900cb8b8 | ||
|
|
c1be262357 | ||
|
|
65d8a176a6 | ||
|
|
572d8530b6 | ||
|
|
0fa8cc76f5 | ||
|
|
9e10dec903 | ||
|
|
c912df95cb | ||
|
|
915315ef1b | ||
|
|
1ffd814f92 | ||
|
|
c6e7cf13b5 | ||
|
|
1ee03f41f2 | ||
|
|
c2f2331d49 | ||
|
|
5af5de3315 | ||
|
|
9b49a27af6 | ||
|
|
bcc52090c9 | ||
|
|
2ca9b75aa6 | ||
|
|
68c79a4a79 | ||
|
|
2574c4c18c | ||
|
|
bc9f3a38ce |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.99.1"
|
||||
__version__ = "15.104.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -34,13 +34,6 @@
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Anlagen im Bau": {
|
||||
"is_group": 1,
|
||||
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
|
||||
"account_number": "0498",
|
||||
"account_type": "Capital Work in Progress"
|
||||
}
|
||||
},
|
||||
"Accumulated Depreciation": {
|
||||
"account_type": "Accumulated Depreciation"
|
||||
}
|
||||
@@ -324,21 +317,13 @@
|
||||
"account_number": "3800",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
|
||||
"account_number": "3960",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Herstellungskosten": {
|
||||
"account_number": "4996",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
|
||||
"account_number": "2310",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
|
||||
"account_number": "2320",
|
||||
"account_type": "Expense Account"
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Verwaltungskosten": {
|
||||
"account_number": "4997",
|
||||
@@ -355,7 +340,7 @@
|
||||
"is_group": 1,
|
||||
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
|
||||
"account_number": "4830",
|
||||
"account_type": "Depreciation"
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Gebäude": {
|
||||
"account_number": "4831",
|
||||
|
||||
@@ -82,15 +82,13 @@ class AccountingDimension(Document):
|
||||
else:
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def on_update(self):
|
||||
def after_insert(self):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
|
||||
)
|
||||
frappe.flags.accounting_dimensions = None
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
@@ -105,6 +103,10 @@ class AccountingDimension(Document):
|
||||
if not self.fieldname:
|
||||
self.fieldname = scrub(self.label)
|
||||
|
||||
def on_update(self):
|
||||
frappe.flags.accounting_dimensions = None
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
|
||||
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
|
||||
@@ -1,41 +1,108 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2018-11-22 23:47:02.804568",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"tax_type",
|
||||
"tax_rate"
|
||||
],
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-11-22 23:47:02.804568",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "tax_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "tax_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate"
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax Rate",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-30 23:49:27.020639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-12-21 23:51:39.445198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
@@ -839,7 +839,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
paid_amount: function (frm) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
@@ -860,7 +860,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
|
||||
@@ -2306,20 +2306,22 @@ 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 = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
|
||||
condition = " and voucher_type={} and voucher_no={}".format(
|
||||
frappe.db.escape(args["voucher_type"]), 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 += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
|
||||
condition += " and cost_center='%s'" % 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}={frappe.db.escape(args.get(dim.fieldname))}"
|
||||
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
@@ -2328,19 +2330,18 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
}
|
||||
|
||||
for fieldname, date_fields in date_fields_dict.items():
|
||||
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
|
||||
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
|
||||
|
||||
if args.get(date_fields[0]) and args.get(date_fields[1]):
|
||||
condition += f" and {fieldname} between {from_date} and {to_date}"
|
||||
condition += " and {} between '{}' and '{}'".format(
|
||||
fieldname, args.get(date_fields[0]), 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} >= {from_date}"
|
||||
condition += f" and {fieldname} >= '{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} <= {to_date}"
|
||||
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
@@ -2560,7 +2561,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}={frappe.db.escape(filters.get(dim.fieldname))}"
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
|
||||
@@ -200,30 +200,6 @@ 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",
|
||||
@@ -1961,37 +1937,6 @@ 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.period_start_date,
|
||||
to_date: frm.doc.period_end_date,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
|
||||
@@ -812,7 +812,6 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
@@ -859,7 +858,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-20 16:16:12.322024",
|
||||
"modified": "2024-05-07 15:56:54.343317",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Item",
|
||||
|
||||
@@ -658,7 +658,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if pricing_rule.is_recursive:
|
||||
transaction_qty = sum(
|
||||
[
|
||||
flt(row.qty)
|
||||
row.qty
|
||||
for row in doc.items
|
||||
if not row.is_free_item
|
||||
and row.item_code == args.item_code
|
||||
|
||||
@@ -115,12 +115,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
doc.docstatus == 1 &&
|
||||
doc.outstanding_amount != 0 &&
|
||||
!doc.on_hold &&
|
||||
frappe.model.can_create("Payment Entry")
|
||||
) {
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
|
||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
@@ -131,13 +126,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
doc.docstatus == 1 &&
|
||||
doc.outstanding_amount > 0 &&
|
||||
!cint(doc.is_return) &&
|
||||
!doc.on_hold &&
|
||||
frappe.boot.user.in_create.includes("Payment Request")
|
||||
) {
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
@@ -471,14 +460,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = ["expense_account", "discount_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||
"expense_account",
|
||||
"discount_account",
|
||||
"cost_center",
|
||||
"project",
|
||||
]);
|
||||
}
|
||||
|
||||
on_submit() {
|
||||
@@ -587,6 +575,12 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
frappe.ui.form.on("Purchase Invoice", {
|
||||
setup: function (frm) {
|
||||
frm.custom_make_buttons = {
|
||||
|
||||
@@ -109,7 +109,6 @@
|
||||
"sales_invoice_item",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"delivered_by_supplier",
|
||||
"item_weight_details",
|
||||
"weight_per_unit",
|
||||
"total_weight",
|
||||
@@ -732,6 +731,7 @@
|
||||
"label": "Valuation Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -979,21 +979,12 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delivered_by_supplier",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Delivered by Supplier",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-06 08:08:40.782395",
|
||||
"modified": "2025-10-14 13:01:54.441511",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -31,7 +31,6 @@ class PurchaseInvoiceItem(Document):
|
||||
conversion_factor: DF.Float
|
||||
cost_center: DF.Link | None
|
||||
deferred_expense_account: DF.Link | None
|
||||
delivered_by_supplier: DF.Check
|
||||
description: DF.TextEditor | None
|
||||
discount_amount: DF.Currency
|
||||
discount_percentage: DF.Percent
|
||||
|
||||
@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
|
||||
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
@@ -136,15 +136,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
if (doc.outstanding_amount > 0) {
|
||||
if (frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request_with_schedule();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
cur_frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
this.frm.add_custom_button(
|
||||
__("Invoice Discounting"),
|
||||
this.make_invoice_discounting.bind(this),
|
||||
@@ -168,7 +166,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
);
|
||||
}
|
||||
}
|
||||
this.toggle_get_items();
|
||||
|
||||
// Show buttons only when pos view is active
|
||||
if (cint(doc.docstatus == 0) && cur_frm.page.current_view_name !== "pos" && !doc.is_return) {
|
||||
this.frm.cscript.sales_order_btn();
|
||||
this.frm.cscript.delivery_note_btn();
|
||||
this.frm.cscript.quotation_btn();
|
||||
}
|
||||
|
||||
this.set_default_print_format();
|
||||
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
|
||||
@@ -254,93 +258,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
}
|
||||
|
||||
toggle_get_items() {
|
||||
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
|
||||
|
||||
buttons.forEach((label) => {
|
||||
this.frm.remove_custom_button(label, "Get Items From");
|
||||
});
|
||||
|
||||
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.frm.doc.is_return) {
|
||||
this.frm.cscript.sales_order_btn();
|
||||
this.frm.cscript.quotation_btn();
|
||||
this.frm.cscript.timesheet_btn();
|
||||
}
|
||||
|
||||
this.frm.cscript.delivery_note_btn();
|
||||
}
|
||||
|
||||
timesheet_btn() {
|
||||
var me = this;
|
||||
|
||||
me.frm.add_custom_button(
|
||||
__("Timesheet"),
|
||||
function () {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Item Code"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {
|
||||
is_sales_item: 1,
|
||||
customer: me.frm.doc.customer,
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: me.frm.doc.project,
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const data = d.get_values();
|
||||
me.frm.events.add_timesheet_data(me.frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
item_code: data.item_code,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
sales_order_btn() {
|
||||
var me = this;
|
||||
this.$sales_order_btn = this.frm.add_custom_button(
|
||||
@@ -405,12 +322,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
this.$delivery_note_btn = this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
function () {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer"),
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
source_doctype: "Delivery Note",
|
||||
@@ -423,7 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
var filters = {
|
||||
docstatus: 1,
|
||||
company: me.frm.doc.company,
|
||||
is_return: me.frm.doc.is_return,
|
||||
is_return: 0,
|
||||
};
|
||||
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
|
||||
return {
|
||||
@@ -542,14 +453,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = ["income_account", "discount_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||
"income_account",
|
||||
"discount_account",
|
||||
"cost_center",
|
||||
]);
|
||||
}
|
||||
|
||||
set_dynamic_labels() {
|
||||
@@ -685,14 +594,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
this.calculate_taxes_and_totals();
|
||||
}
|
||||
|
||||
apply_tds(frm) {
|
||||
this.frm.clear_table("tax_withholding_entries");
|
||||
}
|
||||
|
||||
is_return() {
|
||||
this.toggle_get_items();
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
@@ -1138,6 +1039,71 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
|
||||
frm.add_custom_button(
|
||||
__("Timesheet"),
|
||||
function () {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
label: __("From"),
|
||||
fieldname: "from_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Item Code"),
|
||||
fieldname: "item_code",
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
get_query: () => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {
|
||||
is_sales_item: 1,
|
||||
customer: frm.doc.customer,
|
||||
has_variants: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
label: __("To"),
|
||||
fieldname: "to_time",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
label: __("Project"),
|
||||
fieldname: "project",
|
||||
fieldtype: "Link",
|
||||
options: "Project",
|
||||
default: frm.doc.project,
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
const data = d.get_values();
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project,
|
||||
item_code: data.item_code,
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __("Get Timesheets"),
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.is_debit_note) {
|
||||
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
||||
}
|
||||
|
||||
@@ -2917,7 +2917,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.submit()
|
||||
|
||||
# Check if adjustment entry is created
|
||||
self.assertFalse(
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
|
||||
@@ -25,10 +25,6 @@ frappe.ui.form.on("Shipping Rule", {
|
||||
},
|
||||
calculate_based_on: function (frm) {
|
||||
frm.trigger("toggle_reqd");
|
||||
if (frm.doc.calculate_based_on === "Fixed") {
|
||||
frm.clear_table("conditions");
|
||||
frm.refresh_field("conditions");
|
||||
}
|
||||
},
|
||||
toggle_reqd: function (frm) {
|
||||
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed");
|
||||
|
||||
@@ -58,11 +58,6 @@ class ShippingRule(Document):
|
||||
self.validate_overlapping_shipping_rule_conditions()
|
||||
|
||||
def validate_from_to_values(self):
|
||||
if self.calculate_based_on == "Fixed":
|
||||
if self.conditions:
|
||||
self.set("conditions", [])
|
||||
return
|
||||
|
||||
zero_to_values = []
|
||||
|
||||
for d in self.get("conditions"):
|
||||
|
||||
@@ -34,17 +34,6 @@ 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,49 +120,3 @@ 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,17 +53,6 @@ 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,17 +36,6 @@ 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,7 +194,6 @@ 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)
|
||||
|
||||
@@ -361,7 +360,6 @@ class ReceivablePayableReport:
|
||||
posting_date,
|
||||
account_currency,
|
||||
cost_center,
|
||||
project,
|
||||
sum(invoiced) `invoiced`,
|
||||
sum(paid) `paid`,
|
||||
sum(credit_note) `credit_note`,
|
||||
@@ -390,7 +388,6 @@ class ReceivablePayableReport:
|
||||
"credit_note_in_account_currency",
|
||||
"outstanding_in_account_currency",
|
||||
"cost_center",
|
||||
"project",
|
||||
]:
|
||||
_d[field] = x.get(field)
|
||||
|
||||
@@ -928,7 +925,6 @@ class ReceivablePayableReport:
|
||||
ple.against_voucher_no,
|
||||
ple.party_type,
|
||||
ple.cost_center,
|
||||
ple.project,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
@@ -996,9 +992,6 @@ 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):
|
||||
@@ -1238,7 +1231,6 @@ 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"),
|
||||
@@ -1411,7 +1403,6 @@ 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},
|
||||
@@ -1431,7 +1422,6 @@ 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,
|
||||
@@ -1460,7 +1450,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, ple.project, 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, 0, 0, 0, 0, 0, 0);
|
||||
end if;
|
||||
end;
|
||||
"""
|
||||
@@ -1502,7 +1492,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,52 +1204,3 @@ 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,17 +53,6 @@ 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 parenttype='Purchase Invoice'
|
||||
and base_tax_amount_after_discount_amount != 0
|
||||
group by parent, account_head, add_deduct_tax
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
|
||||
@@ -6,7 +6,6 @@ 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):
|
||||
@@ -27,52 +26,6 @@ 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,7 +5,6 @@ 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):
|
||||
@@ -76,43 +75,6 @@ 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(
|
||||
{
|
||||
|
||||
@@ -119,8 +119,8 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map):
|
||||
|
||||
row.update(
|
||||
{
|
||||
"tax_withholding_category": tax_withholding_category or "",
|
||||
"party_entity_type": party_map.get(party, {}).get(party_type),
|
||||
"section_code": tax_withholding_category or "",
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
@@ -141,7 +141,7 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map):
|
||||
else:
|
||||
entries[key] = row
|
||||
out = list(entries.values())
|
||||
out.sort(key=lambda x: (x["tax_withholding_category"], x["transaction_date"], x["ref_no"]))
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"], x["ref_no"]))
|
||||
|
||||
return out
|
||||
|
||||
@@ -205,9 +205,9 @@ def get_columns(filters):
|
||||
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
|
||||
columns = [
|
||||
{
|
||||
"label": _("Tax Withholding Category"),
|
||||
"label": _("Section Code"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldname": "section_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 90,
|
||||
},
|
||||
@@ -236,12 +236,7 @@ def get_columns(filters):
|
||||
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _(f"{filters.get('party_type', 'Party')} Type"),
|
||||
"fieldname": "party_entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
|
||||
]
|
||||
)
|
||||
if filters.party_type == "Supplier":
|
||||
|
||||
@@ -118,7 +118,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
voucher_expected_values = expected_values[i]
|
||||
voucher_actual_values = (
|
||||
voucher.ref_no,
|
||||
voucher.tax_withholding_category,
|
||||
voucher.section_code,
|
||||
voucher.rate,
|
||||
voucher.base_tax_withholding_net_total,
|
||||
voucher.base_total,
|
||||
|
||||
@@ -48,25 +48,28 @@ def group_by_party_and_category(data, filters):
|
||||
party_category_wise_map = {}
|
||||
|
||||
for row in data:
|
||||
key = (row.get("party_type"), row.get("party"), row.get("tax_withholding_category"))
|
||||
party_category_wise_map.setdefault(
|
||||
key,
|
||||
(row.get("party"), row.get("section_code")),
|
||||
{
|
||||
"pan": row.get("pan"),
|
||||
"tax_id": row.get("tax_id"),
|
||||
"party": row.get("party"),
|
||||
"party_type": row.get("party_type"),
|
||||
"party_name": row.get("party_name"),
|
||||
"tax_withholding_category": row.get("tax_withholding_category"),
|
||||
"party_entity_type": row.get("party_entity_type"),
|
||||
"section_code": row.get("section_code"),
|
||||
"entity_type": row.get("entity_type"),
|
||||
"rate": row.get("rate"),
|
||||
"total_amount": 0.0,
|
||||
"tax_amount": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
party_category_wise_map.get(key)["total_amount"] += row.get("total_amount", 0.0)
|
||||
party_category_wise_map.get(key)["tax_amount"] += row.get("tax_amount", 0.0)
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get(
|
||||
"total_amount", 0.0
|
||||
)
|
||||
|
||||
party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get(
|
||||
"tax_amount", 0.0
|
||||
)
|
||||
|
||||
final_result = get_final_result(party_category_wise_map)
|
||||
|
||||
@@ -107,18 +110,13 @@ def get_columns(filters):
|
||||
columns.extend(
|
||||
[
|
||||
{
|
||||
"label": _("Tax Withholding Category"),
|
||||
"label": _("Section Code"),
|
||||
"options": "Tax Withholding Category",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldname": "section_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"label": _(f"{filters.get('party_type', 'Party')} Type"),
|
||||
"fieldname": "party_entity_type",
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
|
||||
{
|
||||
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
|
||||
"fieldname": "rate",
|
||||
|
||||
@@ -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") and entry.get("outstanding_amount") is None:
|
||||
if referenced_row.get("outstanding_amount"):
|
||||
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
|
||||
|
||||
reposting_rows.append(referenced_row)
|
||||
@@ -2320,7 +2320,6 @@ 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"
|
||||
@@ -2347,7 +2346,6 @@ 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,
|
||||
@@ -2365,7 +2363,6 @@ 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,
|
||||
|
||||
@@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", {
|
||||
});
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
onload: (frm) => {
|
||||
frm.trigger("set_required_fields");
|
||||
},
|
||||
|
||||
|
||||
@@ -440,11 +440,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (
|
||||
frappe.model.can_create("Payment Entry") &&
|
||||
flt(doc.per_billed) < 100 &&
|
||||
doc.status != "Delivered"
|
||||
) {
|
||||
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
() => this.make_payment_entry(),
|
||||
@@ -452,7 +448,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
|
||||
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
if (flt(doc.per_billed) < 100) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
@@ -709,20 +705,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (doc.schedule_date) {
|
||||
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
|
||||
row.schedule_date = doc.schedule_date;
|
||||
refresh_field("schedule_date", cdn, "items");
|
||||
} else {
|
||||
field_copy.push("schedule_date");
|
||||
}
|
||||
if (field_copy.length) {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +785,12 @@ cur_frm.cscript.update_status = function (label, status) {
|
||||
});
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
if (cur_frm.doc.is_old_subcontracting_flow) {
|
||||
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
|
||||
@@ -283,7 +283,7 @@ class RequestforQuotation(BuyingController):
|
||||
}
|
||||
)
|
||||
user.save(ignore_permissions=True)
|
||||
update_password_link = user._reset_password()
|
||||
update_password_link = user.reset_password()
|
||||
|
||||
return user, update_password_link
|
||||
|
||||
@@ -474,11 +474,6 @@ def create_supplier_quotation(doc):
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if frappe.session.user not in frappe.get_all(
|
||||
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
|
||||
):
|
||||
frappe.throw(_("Not Permitted"), frappe.PermissionError)
|
||||
|
||||
try:
|
||||
sq_doc = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -263,13 +263,6 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
|
||||
|
||||
for data in supplier_data:
|
||||
rfq.append("suppliers", data)
|
||||
frappe.new_doc(
|
||||
"Portal User",
|
||||
user="Administrator",
|
||||
parent=data.get("supplier"),
|
||||
parentfield="portal_users",
|
||||
parenttype="Supplier",
|
||||
).insert()
|
||||
|
||||
rfq.append(
|
||||
"items",
|
||||
|
||||
@@ -115,3 +115,9 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -68,7 +68,6 @@ from erpnext.stock.doctype.item.item import get_uom_conv_factor
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
_get_item_tax_template,
|
||||
_get_item_tax_template_from_item_group,
|
||||
get_conversion_factor,
|
||||
get_item_details,
|
||||
get_item_tax_map,
|
||||
@@ -326,7 +325,6 @@ class AccountsController(TransactionBase):
|
||||
# Determine if drop ship applies
|
||||
is_drop_ship = self.doctype in {
|
||||
"Purchase Order",
|
||||
"Purchase Invoice",
|
||||
"Sales Order",
|
||||
"Sales Invoice",
|
||||
} and self.is_drop_ship(self.items)
|
||||
@@ -1753,7 +1751,6 @@ 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(
|
||||
@@ -1838,7 +1835,6 @@ class AccountsController(TransactionBase):
|
||||
d.idx,
|
||||
self.cost_center,
|
||||
dimensions_dict,
|
||||
self.project,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@@ -3648,10 +3644,6 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
|
||||
}
|
||||
|
||||
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
|
||||
|
||||
if not child_item.get("item_tax_template"):
|
||||
child_item.item_tax_template = _get_item_tax_template_from_item_group(args, item.item_group)
|
||||
|
||||
if child_item.get("item_tax_template"):
|
||||
child_item.item_tax_rate = get_item_tax_map(
|
||||
parent_doc.get("company"), child_item.item_tax_template, as_json=True
|
||||
|
||||
@@ -364,17 +364,7 @@ class BuyingController(SubcontractingController):
|
||||
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
net_rate = item.qty * item.base_net_rate
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
|
||||
@@ -356,43 +356,38 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_delivery_notes_to_be_billed(
|
||||
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False
|
||||
):
|
||||
DeliveryNote = frappe.qb.DocType("Delivery Note")
|
||||
|
||||
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
|
||||
doctype = "Delivery Note"
|
||||
fields = get_fields(doctype, ["name", "customer", "posting_date"])
|
||||
|
||||
original_dn = (
|
||||
frappe.qb.from_(DeliveryNote)
|
||||
.select(DeliveryNote.name)
|
||||
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(DeliveryNote)
|
||||
.select(*[DeliveryNote[f] for f in fields])
|
||||
.where(
|
||||
(DeliveryNote.docstatus == 1)
|
||||
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
|
||||
& (DeliveryNote[searchfield].like(f"%{txt}%"))
|
||||
& (
|
||||
((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100))
|
||||
| ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100))
|
||||
| (
|
||||
(DeliveryNote.is_return == 1)
|
||||
& (DeliveryNote.per_billed < 100)
|
||||
& (DeliveryNote.return_against.isin(original_dn))
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select {fields}
|
||||
from `tabDelivery Note`
|
||||
where `tabDelivery Note`.`{key}` like {txt} and
|
||||
`tabDelivery Note`.docstatus = 1
|
||||
and status not in ('Stopped', 'Closed') {fcond}
|
||||
and (
|
||||
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
or (
|
||||
`tabDelivery Note`.is_return = 1
|
||||
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
||||
)
|
||||
)
|
||||
)
|
||||
{mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start}
|
||||
""".format(
|
||||
fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]),
|
||||
key=searchfield,
|
||||
fcond=get_filters_cond(doctype, filters, []),
|
||||
mcond=get_match_cond(doctype),
|
||||
start=start,
|
||||
page_len=page_len,
|
||||
txt="%(txt)s",
|
||||
),
|
||||
{"txt": ("%%%s%%" % txt)},
|
||||
as_dict=as_dict,
|
||||
)
|
||||
if filters and isinstance(filters, dict):
|
||||
for key, value in filters.items():
|
||||
query = query.where(DeliveryNote[key] == value)
|
||||
|
||||
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
|
||||
return query.run(as_dict=as_dict)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -616,11 +616,11 @@ class SellingController(StockController):
|
||||
if allow_at_arms_length_price:
|
||||
continue
|
||||
|
||||
rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0))
|
||||
|
||||
if flt(d.rate, d.precision("incoming_rate")) != flt(
|
||||
rate, d.precision("incoming_rate")
|
||||
):
|
||||
rate = flt(
|
||||
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
|
||||
d.precision("rate"),
|
||||
)
|
||||
if d.rate != rate:
|
||||
d.rate = rate
|
||||
frappe.msgprint(
|
||||
_(
|
||||
|
||||
@@ -186,11 +186,8 @@ class calculate_taxes_and_totals:
|
||||
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
|
||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||
)
|
||||
|
||||
do_not_round_fields = ["valuation_rate", "incoming_rate"]
|
||||
|
||||
for item in self.doc.items:
|
||||
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
|
||||
self.doc.round_floats_in(item)
|
||||
|
||||
if item.discount_percentage == 100:
|
||||
item.rate = 0.0
|
||||
@@ -693,17 +690,18 @@ class calculate_taxes_and_totals:
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = 0
|
||||
self.doc.base_rounded_total = 0
|
||||
self.doc.rounding_adjustment = 0
|
||||
return
|
||||
|
||||
else:
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
||||
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
|
||||
)
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
||||
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
|
||||
)
|
||||
|
||||
# rounding adjustment should always be the difference between grand and rounded total
|
||||
self.doc.rounding_adjustment = flt(
|
||||
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
|
||||
)
|
||||
# rounding adjustment should always be the difference vetween grand and rounded total
|
||||
self.doc.rounding_adjustment = flt(
|
||||
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
|
||||
)
|
||||
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
|
||||
class TestTaxesAndTotals(FrappeTestCase):
|
||||
def test_disabling_rounded_total_resets_base_fields(self):
|
||||
"""Disabling rounded total should also clear base rounded values."""
|
||||
so = make_sales_order(do_not_save=True)
|
||||
so.items[0].qty = 1
|
||||
so.items[0].rate = 1000.25
|
||||
so.items[0].price_list_rate = 1000.25
|
||||
so.items[0].discount_percentage = 0
|
||||
so.items[0].discount_amount = 0
|
||||
so.set("taxes", [])
|
||||
|
||||
so.disable_rounded_total = 0
|
||||
calculate_taxes_and_totals(so)
|
||||
|
||||
self.assertEqual(so.grand_total, 1000.25)
|
||||
self.assertEqual(so.rounded_total, 1000.0)
|
||||
self.assertEqual(so.rounding_adjustment, -0.25)
|
||||
self.assertEqual(so.base_grand_total, 1000.25)
|
||||
self.assertEqual(so.base_rounded_total, 1000.0)
|
||||
self.assertEqual(so.base_rounding_adjustment, -0.25)
|
||||
|
||||
# User toggles disable_rounded_total after values are already set.
|
||||
so.disable_rounded_total = 1
|
||||
|
||||
calculate_taxes_and_totals(so)
|
||||
|
||||
self.assertEqual(so.rounded_total, 0)
|
||||
self.assertEqual(so.rounding_adjustment, 0)
|
||||
self.assertEqual(so.base_rounded_total, 0)
|
||||
self.assertEqual(so.base_rounding_adjustment, 0)
|
||||
@@ -4,9 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import DateTimeLikeObject, getdate, today
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from frappe.utils import getdate
|
||||
|
||||
|
||||
def get_columns(filters, trans):
|
||||
@@ -47,10 +45,6 @@ def get_columns(filters, trans):
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
if not filters.get("fiscal_year"):
|
||||
filters["fiscal_year"] = get_fiscal_year(today())[0]
|
||||
if not filters.get("company"):
|
||||
filters["company"] = frappe.defaults.get_user_default("Company")
|
||||
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
|
||||
if not filters.get(f.lower().replace(" ", "_")):
|
||||
frappe.throw(_("{0} is mandatory").format(_(f)))
|
||||
|
||||
@@ -117,7 +117,7 @@ def get_join(filters):
|
||||
join = """JOIN `tabOpportunity Lost Reason Detail`
|
||||
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
|
||||
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
|
||||
`tabOpportunity Lost Reason Detail`.lost_reason=%(lost_reason)s
|
||||
"""
|
||||
`tabOpportunity Lost Reason Detail`.lost_reason = '{}'
|
||||
""".format(filters.get("lost_reason"))
|
||||
|
||||
return join
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import escape_html
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lxml.etree import Element
|
||||
@@ -64,16 +63,14 @@ class CodeList(Document):
|
||||
|
||||
def from_genericode(self, root: "Element"):
|
||||
"""Extract Code List details from genericode XML"""
|
||||
self.title = escape_html(root.find(".//Identification/ShortName").text)
|
||||
self.title = root.find(".//Identification/ShortName").text
|
||||
self.version = root.find(".//Identification/Version").text
|
||||
self.canonical_uri = root.find(".//CanonicalUri").text
|
||||
# optionals
|
||||
self.description = escape_html(getattr(root.find(".//Identification/LongName"), "text", None))
|
||||
self.publisher = escape_html(getattr(root.find(".//Identification/Agency/ShortName"), "text", None))
|
||||
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 = escape_html(
|
||||
getattr(root.find(".//Identification/Agency/LongName"), "text", None)
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ 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,118 +1,42 @@
|
||||
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
|
||||
|
||||
GENERICODE_FETCH_TIMEOUT = 15
|
||||
LOCAL_FILE_PREFIXES = ("/files/", "/private/files/")
|
||||
|
||||
|
||||
class RemoteGenericodeUrlNotAllowedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CodeListSelectionMismatchError(Exception):
|
||||
pass
|
||||
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:
|
||||
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)
|
||||
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
|
||||
@@ -121,7 +45,7 @@ def import_genericode_content(
|
||||
if frappe.db.exists(doctype, docname):
|
||||
code_list = frappe.get_doc(doctype, docname)
|
||||
if code_list.name != name:
|
||||
raise CodeListSelectionMismatchError
|
||||
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)
|
||||
@@ -130,13 +54,19 @@ def import_genericode_content(
|
||||
code_list.from_genericode(root)
|
||||
code_list.save()
|
||||
|
||||
file_doc = save_file(
|
||||
fname=file_name,
|
||||
content=content,
|
||||
dt=doctype,
|
||||
dn=code_list.name,
|
||||
is_private=1,
|
||||
)
|
||||
# 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()
|
||||
|
||||
# Get available columns and example values
|
||||
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
|
||||
@@ -151,16 +81,6 @@ def import_genericode_content(
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
@@ -184,7 +104,7 @@ def get_genericode_columns_and_examples(root):
|
||||
|
||||
# Get column names
|
||||
for column in root.findall(".//Column"):
|
||||
column_id = escape_html(column.get("Id"))
|
||||
column_id = column.get("Id")
|
||||
columns.append(column_id)
|
||||
example_values[column_id] = []
|
||||
filterable_columns[column_id] = set()
|
||||
@@ -192,7 +112,7 @@ def get_genericode_columns_and_examples(root):
|
||||
# Get all values and count unique occurrences
|
||||
for row in root.findall(".//SimpleCodeList/Row"):
|
||||
for value in row.findall("Value"):
|
||||
column_id = escape_html(value.get("ColumnRef"))
|
||||
column_id = value.get("ColumnRef")
|
||||
if column_id not in columns:
|
||||
# Handle undeclared column
|
||||
columns.append(column_id)
|
||||
@@ -203,7 +123,7 @@ def get_genericode_columns_and_examples(root):
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
filterable_columns[column_id].add(escape_html(simple_value.text))
|
||||
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]:
|
||||
@@ -213,7 +133,7 @@ def get_genericode_columns_and_examples(root):
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
example_values[column_id].append(escape_html(simple_value.text))
|
||||
example_values[column_id].append(simple_value.text)
|
||||
|
||||
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}
|
||||
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
# 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,8 +9,6 @@ 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
|
||||
@@ -88,15 +86,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_doc = frappe.get_doc("File", file_name)
|
||||
file_doc.check_permission("read")
|
||||
root = parse_genericode_content(read_file_bytes(file_doc))
|
||||
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 or {}).items()
|
||||
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
|
||||
]
|
||||
if filter_conditions:
|
||||
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
|
||||
@@ -104,7 +102,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 flt(d.qty) < 0:
|
||||
if d.qty < 0:
|
||||
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))
|
||||
|
||||
|
||||
|
||||
@@ -760,8 +760,6 @@ frappe.ui.form.on("BOM Item", "sourced_by_supplier", function (frm, cdt, cdn) {
|
||||
if (d.sourced_by_supplier) {
|
||||
d.rate = 0;
|
||||
refresh_field("rate", d.name, d.parentfield);
|
||||
} else {
|
||||
get_bom_material_detail(frm.doc, cdt, cdn, false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Operation",
|
||||
"options": "Operation",
|
||||
"reqd": 1
|
||||
"options": "Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -41,7 +40,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-13 12:17:33.776504",
|
||||
"modified": "2025-08-04 16:15:11.425349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Sub Operation",
|
||||
|
||||
@@ -16,7 +16,7 @@ class SubOperation(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
description: DF.SmallText | None
|
||||
operation: DF.Link
|
||||
operation: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -228,18 +228,6 @@ 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()
|
||||
|
||||
@@ -260,8 +248,8 @@ class WorkOrder(Document):
|
||||
& (SalesOrder.docstatus == 1)
|
||||
& (SalesOrder.name == self.sales_order)
|
||||
& (
|
||||
(SalesOrderItem.item_code == production_item)
|
||||
| (ProductBundleItem.item_code == production_item)
|
||||
(SalesOrderItem.item_code == self.production_item)
|
||||
| (ProductBundleItem.item_code == self.production_item)
|
||||
)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
@@ -280,7 +268,7 @@ class WorkOrder(Document):
|
||||
& (SalesOrder.skip_delivery_note == 0)
|
||||
& (SalesOrderItem.item_code == PackedItem.parent_item)
|
||||
& (SalesOrder.docstatus == 1)
|
||||
& (PackedItem.item_code == production_item)
|
||||
& (PackedItem.item_code == self.production_item)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
@@ -10,6 +10,6 @@ frappe.listview_settings["Workstation"] = {
|
||||
Setup: "blue",
|
||||
};
|
||||
|
||||
return [__(doc.status), color_map[doc.status], "status,=," + doc.status];
|
||||
return [__(doc.status), color_map[doc.status], true];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -432,4 +432,3 @@ erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
|
||||
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
|
||||
erpnext.patches.v16_0.add_portal_redirects
|
||||
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
|
||||
erpnext.patches.v16_0.depends_on_inv_dimensions
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def get_inventory_dimensions():
|
||||
return frappe.get_all(
|
||||
"Inventory Dimension",
|
||||
fields=[
|
||||
"target_fieldname as fieldname",
|
||||
"source_fieldname",
|
||||
"reference_document as doctype",
|
||||
"reqd",
|
||||
"mandatory_depends_on",
|
||||
],
|
||||
order_by="creation",
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
|
||||
def get_display_depends_on(doctype, fieldname):
|
||||
if doctype not in [
|
||||
"Stock Entry Detail",
|
||||
"Sales Invoice Item",
|
||||
"Delivery Note Item",
|
||||
"Purchase Invoice Item",
|
||||
"Purchase Receipt Item",
|
||||
]:
|
||||
return None, None
|
||||
|
||||
fieldname_start_with = "to"
|
||||
display_depends_on = ""
|
||||
|
||||
if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
|
||||
display_depends_on = "eval:parent.is_internal_supplier == 1"
|
||||
fieldname_start_with = "from"
|
||||
elif doctype != "Stock Entry Detail":
|
||||
display_depends_on = "eval:parent.is_internal_customer == 1"
|
||||
elif doctype == "Stock Entry Detail":
|
||||
display_depends_on = "eval:doc.t_warehouse"
|
||||
|
||||
return f"{fieldname_start_with}_{fieldname}", display_depends_on
|
||||
|
||||
|
||||
def execute():
|
||||
for dimension in get_inventory_dimensions():
|
||||
if frappe.db.exists(
|
||||
"Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"}
|
||||
):
|
||||
frappe.set_value(
|
||||
"Custom Field",
|
||||
{"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"},
|
||||
"depends_on",
|
||||
"eval:doc.s_warehouse",
|
||||
)
|
||||
if frappe.db.exists(
|
||||
"Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1}
|
||||
):
|
||||
frappe.set_value(
|
||||
"Custom Field",
|
||||
{"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1},
|
||||
{"mandatory_depends_on": "eval:doc.s_warehouse", "reqd": 0},
|
||||
)
|
||||
if frappe.db.exists(
|
||||
"Custom Field",
|
||||
{
|
||||
"fieldname": f"to_{dimension.fieldname}",
|
||||
"dt": "Stock Entry Detail",
|
||||
"depends_on": "eval:parent.purpose != 'Material Issue'",
|
||||
},
|
||||
):
|
||||
frappe.set_value(
|
||||
"Custom Field",
|
||||
{
|
||||
"fieldname": f"to_{dimension.fieldname}",
|
||||
"dt": "Stock Entry Detail",
|
||||
"depends_on": "eval:parent.purpose != 'Material Issue'",
|
||||
},
|
||||
"depends_on",
|
||||
"eval:doc.t_warehouse",
|
||||
)
|
||||
fieldname, display_depends_on = get_display_depends_on(dimension.doctype, dimension.fieldname)
|
||||
if display_depends_on and frappe.db.exists(
|
||||
"Custom Field", {"fieldname": fieldname, "dt": dimension.doctype}
|
||||
):
|
||||
frappe.set_value(
|
||||
"Custom Field",
|
||||
{"fieldname": fieldname, "dt": dimension.doctype},
|
||||
"mandatory_depends_on",
|
||||
display_depends_on if dimension.reqd else dimension.mandatory_depends_on,
|
||||
)
|
||||
@@ -10,18 +10,7 @@ def execute():
|
||||
)
|
||||
if data:
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
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
|
||||
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
|
||||
|
||||
@@ -364,18 +364,13 @@ class Project(Document):
|
||||
)
|
||||
|
||||
for user in self.users:
|
||||
# process only users who haven't received the welcome email yet
|
||||
if user.welcome_email_sent == 0:
|
||||
# fetch canonical User data (enabled status + latest email)
|
||||
user_info = frappe.db.get_value("User", user.user, ["enabled", "email"], as_dict=True)
|
||||
# send email only if user is enabled and has a valid email
|
||||
if user_info and user_info.enabled and user_info.email:
|
||||
frappe.sendmail(
|
||||
recipients=[user_info.email],
|
||||
subject=_("Project Collaboration Invitation"),
|
||||
content=content,
|
||||
)
|
||||
user.welcome_email_sent = 1
|
||||
frappe.sendmail(
|
||||
user.user,
|
||||
subject=_("Project Collaboration Invitation"),
|
||||
content=content,
|
||||
)
|
||||
user.welcome_email_sent = 1
|
||||
|
||||
|
||||
def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
|
||||
|
||||
@@ -25,16 +25,14 @@ erpnext.buying = {
|
||||
};
|
||||
});
|
||||
|
||||
const get_project_filters = () => ({
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
},
|
||||
this.frm.set_query("project", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("project", get_project_filters);
|
||||
this.frm.set_query("project", "items", get_project_filters);
|
||||
|
||||
if (this.frm.doc.__islocal
|
||||
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
|
||||
|
||||
@@ -176,14 +174,9 @@ erpnext.buying = {
|
||||
callback: (r) => {
|
||||
if (!r.message) return;
|
||||
|
||||
if (!this.frm.doc.billing_address) {
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
}
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
|
||||
if (
|
||||
frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") &&
|
||||
!this.frm.doc.shipping_address
|
||||
) {
|
||||
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
|
||||
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -674,27 +674,24 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
set_rounded_total() {
|
||||
var disable_rounded_total = 0;
|
||||
if (frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total", this.frm.doc.name)) {
|
||||
if(frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total", this.frm.doc.name)) {
|
||||
disable_rounded_total = this.frm.doc.disable_rounded_total;
|
||||
} else if (frappe.sys_defaults.disable_rounded_total) {
|
||||
disable_rounded_total = frappe.sys_defaults.disable_rounded_total;
|
||||
}
|
||||
|
||||
if (frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
|
||||
if (cint(disable_rounded_total)) {
|
||||
this.frm.doc.rounded_total = 0;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
} else {
|
||||
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(
|
||||
this.frm.doc.grand_total,
|
||||
this.frm.doc.currency,
|
||||
precision("rounded_total"),
|
||||
);
|
||||
this.frm.doc.rounding_adjustment = flt(
|
||||
this.frm.doc.rounded_total - this.frm.doc.grand_total,
|
||||
precision("rounding_adjustment"),
|
||||
);
|
||||
}
|
||||
if (cint(disable_rounded_total)) {
|
||||
this.frm.doc.rounded_total = 0;
|
||||
this.frm.doc.base_rounded_total = 0;
|
||||
this.frm.doc.rounding_adjustment = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
|
||||
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total,
|
||||
this.frm.doc.currency, precision("rounded_total"));
|
||||
this.frm.doc.rounding_adjustment = flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
|
||||
precision("rounding_adjustment"));
|
||||
|
||||
this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]);
|
||||
}
|
||||
|
||||
@@ -1032,7 +1032,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) &&
|
||||
!this.frm.doc.shipping_address
|
||||
) {
|
||||
const is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
|
||||
let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
|
||||
|
||||
if (!is_drop_ship) {
|
||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||
|
||||
@@ -106,19 +106,15 @@ $.extend(erpnext.queries, {
|
||||
});
|
||||
}
|
||||
|
||||
let filters = { link_doctype: "Company", link_name: doc.company || "" };
|
||||
const is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
|
||||
if (is_drop_ship) filters = {};
|
||||
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
filters: filters,
|
||||
filters: { link_doctype: "Company", link_name: doc.company },
|
||||
};
|
||||
},
|
||||
|
||||
dispatch_address_query: function (doc) {
|
||||
let filters = { link_doctype: "Company", link_name: doc.company || "" };
|
||||
const is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
|
||||
var filters = { link_doctype: "Company", link_name: doc.company || "" };
|
||||
var is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
|
||||
if (is_drop_ship) filters = {};
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
|
||||
@@ -38,7 +38,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("Primary Contact Details"),
|
||||
collapsible: 0,
|
||||
collapsible: 1,
|
||||
},
|
||||
{
|
||||
label: __("First Name"),
|
||||
@@ -69,7 +69,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
label: __("Primary Address Details"),
|
||||
collapsible: 0,
|
||||
collapsible: 1,
|
||||
},
|
||||
{
|
||||
label: __("Address Line 1"),
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ pincode }} {{ city | upper }}<br>
|
||||
{{ country | upper }}
|
||||
@@ -1,4 +0,0 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ pincode }} {{ city | upper }}<br>
|
||||
{{ country | upper }}
|
||||
@@ -1,12 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext import get_region
|
||||
|
||||
|
||||
class SouthAfricaVATSettings(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -25,9 +22,4 @@ class SouthAfricaVATSettings(Document):
|
||||
vat_accounts: DF.Table[SouthAfricaVATAccount]
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_company_region()
|
||||
|
||||
def validate_company_region(self):
|
||||
if self.company and get_region(self.company) != "South Africa":
|
||||
frappe.throw(_("Company {0} is not in South Africa.").format(frappe.bold(self.company)))
|
||||
pass
|
||||
|
||||
@@ -10,13 +10,6 @@ frappe.query_reports["VAT Audit Report"] = {
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
get_query: function () {
|
||||
return {
|
||||
filters: {
|
||||
country: "South Africa",
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
|
||||
@@ -6,11 +6,8 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Coalesce, NullIf
|
||||
from frappe.utils import formatdate, get_link_to_form
|
||||
|
||||
from erpnext import get_region
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
return VATAuditReport(filters).run()
|
||||
@@ -24,10 +21,19 @@ class VATAuditReport:
|
||||
self.doctypes = ["Purchase Invoice", "Sales Invoice"]
|
||||
|
||||
def run(self):
|
||||
self.validate_company_region()
|
||||
self.get_sa_vat_accounts()
|
||||
self.get_columns()
|
||||
for doctype in self.doctypes:
|
||||
self.select_columns = """
|
||||
name as voucher_no,
|
||||
posting_date, remarks"""
|
||||
columns = (
|
||||
", supplier as party, credit_to as account"
|
||||
if doctype == "Purchase Invoice"
|
||||
else ", customer as party, debit_to as account"
|
||||
)
|
||||
self.select_columns += columns
|
||||
|
||||
self.get_invoice_data(doctype)
|
||||
|
||||
if self.invoices:
|
||||
@@ -37,14 +43,6 @@ class VATAuditReport:
|
||||
|
||||
return self.columns, self.data
|
||||
|
||||
def validate_company_region(self):
|
||||
if self.filters.company and get_region(self.filters.company) != "South Africa":
|
||||
frappe.throw(
|
||||
_(
|
||||
"The company {0} is not in South Africa. VAT Audit Report is only available for companies in South Africa."
|
||||
).format(frappe.bold(self.filters.company))
|
||||
)
|
||||
|
||||
def get_sa_vat_accounts(self):
|
||||
self.sa_vat_accounts = frappe.get_all(
|
||||
"South Africa VAT Account", filters={"parent": self.filters.company}, pluck="account"
|
||||
@@ -56,59 +54,47 @@ class VATAuditReport:
|
||||
frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings))
|
||||
|
||||
def get_invoice_data(self, doctype):
|
||||
conditions = self.get_conditions()
|
||||
self.invoices = frappe._dict()
|
||||
invoice_doctype = frappe.qb.DocType(doctype)
|
||||
party_field = invoice_doctype.supplier if doctype == "Purchase Invoice" else invoice_doctype.customer
|
||||
account_field = (
|
||||
invoice_doctype.credit_to if doctype == "Purchase Invoice" else invoice_doctype.debit_to
|
||||
|
||||
invoice_data = frappe.db.sql(
|
||||
f"""
|
||||
SELECT
|
||||
{self.select_columns}
|
||||
FROM
|
||||
`tab{doctype}`
|
||||
WHERE
|
||||
docstatus = 1 {conditions}
|
||||
and is_opening = 'No'
|
||||
ORDER BY
|
||||
posting_date DESC
|
||||
""",
|
||||
self.filters,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(invoice_doctype)
|
||||
.select(
|
||||
invoice_doctype.name.as_("voucher_no"),
|
||||
invoice_doctype.posting_date,
|
||||
invoice_doctype.remarks,
|
||||
party_field.as_("party"),
|
||||
account_field.as_("account"),
|
||||
)
|
||||
.where(invoice_doctype.docstatus == 1)
|
||||
.where(invoice_doctype.is_opening == "No")
|
||||
.orderby(invoice_doctype.posting_date, order=frappe.qb.desc)
|
||||
)
|
||||
|
||||
if self.filters.get("company"):
|
||||
query = query.where(invoice_doctype.company == self.filters.company)
|
||||
if self.filters.get("from_date"):
|
||||
query = query.where(invoice_doctype.posting_date >= self.filters.from_date)
|
||||
if self.filters.get("to_date"):
|
||||
query = query.where(invoice_doctype.posting_date <= self.filters.to_date)
|
||||
|
||||
invoice_data = query.run(as_dict=True)
|
||||
|
||||
for row in invoice_data:
|
||||
self.invoices.setdefault(row.voucher_no, row)
|
||||
for d in invoice_data:
|
||||
self.invoices.setdefault(d.voucher_no, d)
|
||||
|
||||
def get_invoice_items(self, doctype):
|
||||
self.invoice_items = frappe._dict()
|
||||
item_doctype = frappe.qb.DocType(doctype + " Item")
|
||||
|
||||
items = (
|
||||
frappe.qb.from_(item_doctype)
|
||||
.select(
|
||||
Coalesce(NullIf(item_doctype.item_code, ""), item_doctype.item_name).as_("item"),
|
||||
item_doctype.parent,
|
||||
item_doctype.base_net_amount,
|
||||
item_doctype.is_zero_rated,
|
||||
)
|
||||
.where(item_doctype.parent.isin(list(self.invoices.keys())))
|
||||
.run(as_dict=True)
|
||||
items = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
item_code, parent, base_net_amount, is_zero_rated
|
||||
FROM
|
||||
`tab{} Item`
|
||||
WHERE
|
||||
parent in ({})
|
||||
""".format(doctype, ", ".join(["%s"] * len(self.invoices))),
|
||||
tuple(self.invoices),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for row in items:
|
||||
self.invoice_items.setdefault(row.parent, {}).setdefault(row.item, {"net_amount": 0.0})
|
||||
self.invoice_items[row.parent][row.item]["net_amount"] += row.get("base_net_amount", 0)
|
||||
self.invoice_items[row.parent][row.item]["is_zero_rated"] = row.is_zero_rated
|
||||
for d in items:
|
||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0})
|
||||
self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0)
|
||||
self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated
|
||||
|
||||
def get_items_based_on_tax_rate(self, doctype):
|
||||
self.items_based_on_tax_rate = frappe._dict()
|
||||
@@ -117,54 +103,52 @@ class VATAuditReport:
|
||||
"Purchase Taxes and Charges" if doctype == "Purchase Invoice" else "Sales Taxes and Charges"
|
||||
)
|
||||
|
||||
tax_doctype = frappe.qb.DocType(self.tax_doctype)
|
||||
self.tax_details = (
|
||||
frappe.qb.from_(tax_doctype)
|
||||
.select(tax_doctype.parent, tax_doctype.account_head, tax_doctype.item_wise_tax_detail)
|
||||
.where(tax_doctype.parenttype == doctype)
|
||||
.where(tax_doctype.docstatus == 1)
|
||||
.where(tax_doctype.parent.isin(list(self.invoices.keys())))
|
||||
.where(tax_doctype.account_head.isin(self.sa_vat_accounts))
|
||||
.orderby(tax_doctype.account_head)
|
||||
.run(as_dict=True)
|
||||
self.tax_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
parent, account_head, item_wise_tax_detail
|
||||
FROM
|
||||
`tab{}`
|
||||
WHERE
|
||||
parenttype = {} and docstatus = 1
|
||||
and parent in ({})
|
||||
ORDER BY
|
||||
account_head
|
||||
""".format(self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))),
|
||||
tuple([doctype, *list(self.invoices.keys())]),
|
||||
)
|
||||
|
||||
for tax_detail in self.tax_details:
|
||||
if not tax_detail.item_wise_tax_detail:
|
||||
continue
|
||||
for parent, account, item_wise_tax_detail in self.tax_details:
|
||||
if item_wise_tax_detail:
|
||||
try:
|
||||
if account in self.sa_vat_accounts:
|
||||
item_wise_tax_detail = json.loads(item_wise_tax_detail)
|
||||
else:
|
||||
continue
|
||||
for item_code, taxes in item_wise_tax_detail.items():
|
||||
is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated")
|
||||
# to skip items with non-zero tax rate in multiple rows
|
||||
if taxes[0] == 0 and not is_zero_rated:
|
||||
continue
|
||||
tax_rate = self.get_item_amount_map(parent, item_code, taxes)
|
||||
|
||||
try:
|
||||
item_wise_tax_detail = json.loads(tax_detail.item_wise_tax_detail)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
parent_items = self.invoice_items.get(tax_detail.parent, {})
|
||||
parent_tax_rates = self.items_based_on_tax_rate.setdefault(tax_detail.parent, {})
|
||||
|
||||
for item, taxes in item_wise_tax_detail.items():
|
||||
is_zero_rated = parent_items.get(item, {}).get("is_zero_rated")
|
||||
# to skip items with non-zero tax rate in multiple rows
|
||||
if taxes[0] == 0 and not is_zero_rated:
|
||||
if tax_rate is not None:
|
||||
rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault(
|
||||
tax_rate, []
|
||||
)
|
||||
if item_code not in rate_based_dict:
|
||||
rate_based_dict.append(item_code)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
tax_rate = self.get_item_amount_map(tax_detail.parent, item, taxes)
|
||||
if tax_rate is not None:
|
||||
rate_based_dict = parent_tax_rates.setdefault(tax_rate, [])
|
||||
if item not in rate_based_dict:
|
||||
rate_based_dict.append(item)
|
||||
|
||||
def get_item_amount_map(self, parent, item, taxes):
|
||||
item_details = self.invoice_items.get(parent, {}).get(item)
|
||||
if not item_details:
|
||||
return None
|
||||
|
||||
net_amount = item_details.get("net_amount", 0)
|
||||
def get_item_amount_map(self, parent, item_code, taxes):
|
||||
net_amount = self.invoice_items.get(parent).get(item_code).get("net_amount")
|
||||
tax_rate = taxes[0]
|
||||
tax_amount = taxes[1]
|
||||
gross_amount = net_amount + tax_amount
|
||||
|
||||
self.item_tax_rate.setdefault(parent, {}).setdefault(
|
||||
item,
|
||||
item_code,
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"gross_amount": 0.0,
|
||||
@@ -173,12 +157,24 @@ class VATAuditReport:
|
||||
},
|
||||
)
|
||||
|
||||
self.item_tax_rate[parent][item]["net_amount"] += net_amount
|
||||
self.item_tax_rate[parent][item]["tax_amount"] += tax_amount
|
||||
self.item_tax_rate[parent][item]["gross_amount"] += gross_amount
|
||||
self.item_tax_rate[parent][item_code]["net_amount"] += net_amount
|
||||
self.item_tax_rate[parent][item_code]["tax_amount"] += tax_amount
|
||||
self.item_tax_rate[parent][item_code]["gross_amount"] += gross_amount
|
||||
|
||||
return tax_rate
|
||||
|
||||
def get_conditions(self):
|
||||
conditions = ""
|
||||
for opts in (
|
||||
("company", " and company=%(company)s"),
|
||||
("from_date", " and posting_date>=%(from_date)s"),
|
||||
("to_date", " and posting_date<=%(to_date)s"),
|
||||
):
|
||||
if self.filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
|
||||
return conditions
|
||||
|
||||
def get_data(self, doctype):
|
||||
consolidated_data = self.get_consolidated_data(doctype)
|
||||
section_name = _("Purchases") if doctype == "Purchase Invoice" else _("Sales")
|
||||
|
||||
@@ -123,7 +123,6 @@ 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,
|
||||
@@ -138,6 +137,8 @@ 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")) {
|
||||
|
||||
@@ -188,7 +188,7 @@ class Quotation(SellingController):
|
||||
)
|
||||
|
||||
for row in self._items:
|
||||
if row.name not in ordered_items or row.stock_qty > ordered_items[row.name]:
|
||||
if row.name not in ordered_items or row.qty > ordered_items[row.name]:
|
||||
return "Partially Ordered"
|
||||
|
||||
return "Ordered"
|
||||
@@ -409,9 +409,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
|
||||
target.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
balance_stock_qty = obj.stock_qty - ordered_items.get(obj.name, 0.0)
|
||||
target.stock_qty = balance_stock_qty if balance_stock_qty > 0 else 0
|
||||
target.qty = flt(target.stock_qty) / flt(obj.conversion_factor)
|
||||
balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.name, 0.0)
|
||||
target.qty = balance_qty if balance_qty > 0 else 0
|
||||
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
||||
|
||||
if obj.against_blanket_order:
|
||||
target.against_blanket_order = obj.against_blanket_order
|
||||
@@ -425,7 +425,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
|
||||
2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
|
||||
3. If no selections: Simple row: Map if adequate qty
|
||||
"""
|
||||
if not ((item.stock_qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
|
||||
if not ((item.qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
|
||||
return False
|
||||
|
||||
if not selected_rows:
|
||||
@@ -560,9 +560,7 @@ def _make_customer(source_name, ignore_permissions=False):
|
||||
if quotation.quotation_to == "Customer":
|
||||
return frappe.get_doc("Customer", quotation.party_name)
|
||||
elif quotation.quotation_to == "CRM Deal":
|
||||
customer_name = frappe.get_value("Customer", {"crm_deal": quotation.party_name})
|
||||
if customer_name:
|
||||
return frappe.get_doc("Customer", customer_name)
|
||||
return frappe.get_doc("Customer", {"crm_deal": quotation.party_name})
|
||||
|
||||
# Check if a Customer already exists for the Lead or Prospect.
|
||||
existing_customer = None
|
||||
|
||||
@@ -175,61 +175,6 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
self.assertTrue(quotation.payment_schedule)
|
||||
|
||||
def test_terms_attachments_are_copied_to_quotation(self):
|
||||
terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
|
||||
first_attachment = make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
terms.name,
|
||||
content="First terms attachment",
|
||||
)
|
||||
|
||||
quotation = make_quotation(do_not_save=1)
|
||||
quotation.tc_name = terms.name
|
||||
quotation.insert()
|
||||
|
||||
self.assertEqual(get_attachment_urls("Quotation", quotation.name), {first_attachment.file_url})
|
||||
|
||||
second_attachment = make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
terms.name,
|
||||
content="Second terms attachment",
|
||||
)
|
||||
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
|
||||
quotation.save()
|
||||
|
||||
quotation_attachments = get_attachment_urls("Quotation", quotation.name)
|
||||
self.assertEqual(quotation_attachments, {first_attachment.file_url})
|
||||
self.assertNotIn(second_attachment.file_url, quotation_attachments)
|
||||
|
||||
new_terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
|
||||
new_terms_attachment = make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
new_terms.name,
|
||||
content="Attachment from updated terms",
|
||||
)
|
||||
quotation.tc_name = new_terms.name
|
||||
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
|
||||
quotation.save()
|
||||
|
||||
self.assertEqual(
|
||||
get_attachment_urls("Quotation", quotation.name),
|
||||
{first_attachment.file_url, new_terms_attachment.file_url},
|
||||
)
|
||||
|
||||
def test_terms_attachments_are_not_copied_when_disabled(self):
|
||||
terms = make_terms_and_conditions(copy_attachments_to_transaction=False)
|
||||
make_file_attachment(
|
||||
"Terms and Conditions",
|
||||
terms.name,
|
||||
content="Terms attachment should stay on the template",
|
||||
)
|
||||
|
||||
quotation = make_quotation(do_not_save=1)
|
||||
quotation.tc_name = terms.name
|
||||
quotation.insert()
|
||||
|
||||
self.assertFalse(get_attachment_urls("Quotation", quotation.name))
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"automatically_fetch_payment_terms": 1},
|
||||
@@ -1197,42 +1142,6 @@ def get_quotation_dict(party_name=None, item_code=None):
|
||||
}
|
||||
|
||||
|
||||
def make_terms_and_conditions(copy_attachments_to_transaction=False):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Terms and Conditions",
|
||||
"title": f"_Test Terms and Conditions {frappe.generate_hash(length=8)}",
|
||||
"selling": 1,
|
||||
"terms": "Test terms",
|
||||
"copy_attachments_to_transaction": 1 if copy_attachments_to_transaction else 0,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def make_file_attachment(doctype, docname, content):
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_name": f"terms-attachment-{frappe.generate_hash(length=8)}.txt",
|
||||
"attached_to_doctype": doctype,
|
||||
"attached_to_name": docname,
|
||||
"content": content,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def get_attachment_urls(doctype, docname):
|
||||
return {
|
||||
file.file_url
|
||||
for file in frappe.get_all(
|
||||
"File",
|
||||
filters={"attached_to_doctype": doctype, "attached_to_name": docname},
|
||||
fields=["file_url"],
|
||||
)
|
||||
if file.file_url
|
||||
}
|
||||
|
||||
|
||||
def make_quotation(**args):
|
||||
qo = frappe.new_doc("Quotation")
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -773,13 +773,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
}
|
||||
// payment request
|
||||
if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
|
||||
if (frappe.boot.user.in_create.includes("Payment Request")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
() => this.make_payment_request_with_schedule(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
() => this.make_payment_request(),
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -835,24 +833,6 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
this.order_type(doc);
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
if (doc.delivery_date) {
|
||||
frappe.model.set_value(cdt, cdn, "delivery_date", doc.delivery_date);
|
||||
} else {
|
||||
field_copy.push("delivery_date");
|
||||
}
|
||||
if (field_copy.length) {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
}
|
||||
}
|
||||
|
||||
create_pick_list() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",
|
||||
|
||||
@@ -533,7 +533,6 @@ class SalesOrder(SellingController):
|
||||
self.update_reserved_qty()
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
self.update_blanket_order()
|
||||
|
||||
def update_reserved_qty(self, so_item_rows=None):
|
||||
"""update requested qty (before ordered_qty is updated)"""
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
|
||||
|
||||
@@ -42,117 +41,73 @@ class SMSCenter(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_receiver_list(self):
|
||||
query = None
|
||||
|
||||
if self.send_to == "":
|
||||
return
|
||||
|
||||
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, '') != ''"
|
||||
)
|
||||
if self.send_to in [
|
||||
"All Contact",
|
||||
"All Customer Contact",
|
||||
"All Supplier Contact",
|
||||
"All Sales Partner Contact",
|
||||
]:
|
||||
query = self.get_contact_query_for_all_contacts()
|
||||
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
|
||||
)
|
||||
|
||||
elif self.send_to == "All Lead (Open)":
|
||||
query = self.get_contact_query_for_all_open_leads()
|
||||
rec = frappe.db.sql(
|
||||
"""select lead_name, mobile_no from `tabLead` where
|
||||
ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'"""
|
||||
)
|
||||
|
||||
elif self.send_to == "All Employee (Active)":
|
||||
query = self.get_contact_query_for_all_active_employee()
|
||||
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
|
||||
)
|
||||
|
||||
elif self.send_to == "All Sales Person":
|
||||
query = self.get_contact_query_for_all_sales_person()
|
||||
|
||||
rec = query.run(as_list=1)
|
||||
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,'')!=''"""
|
||||
)
|
||||
|
||||
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:
|
||||
|
||||
@@ -1,176 +1,122 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType, Field, Order
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.utils import QueryBuilder
|
||||
from frappe.utils.data import comma_or
|
||||
|
||||
SALES_TRANSACTION_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note", "POS Invoice"]
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return SalesPartnerCommissionSummaryReport(filters).run()
|
||||
columns = get_columns(filters)
|
||||
data = get_entries(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
class SalesPartnerSummaryReport:
|
||||
"""
|
||||
Base class to generate Sales Partner Summary related Reports.
|
||||
"""
|
||||
def get_columns(filters):
|
||||
if not filters.get("doctype"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
|
||||
dt: DocType
|
||||
date_field: str
|
||||
date_label: str
|
||||
columns: list
|
||||
data: list
|
||||
query: QueryBuilder
|
||||
filters: dict
|
||||
columns = [
|
||||
{
|
||||
"label": _(filters["doctype"]),
|
||||
"options": filters["doctype"],
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Customer"),
|
||||
"options": "Customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Data",
|
||||
"width": 80,
|
||||
},
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"options": "Territory",
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Sales Partner"),
|
||||
"options": "Sales Partner",
|
||||
"fieldname": "sales_partner",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Commission Rate %"),
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total Commission"),
|
||||
"fieldname": "total_commission",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
def __init__(self, filters: dict):
|
||||
self.filters = filters
|
||||
self.columns = []
|
||||
return columns
|
||||
|
||||
def run(self):
|
||||
self.validate_filters()
|
||||
self.prepare_columns()
|
||||
self.get_data()
|
||||
|
||||
return self.columns, self.data
|
||||
|
||||
def validate_filters(self):
|
||||
if not self.filters.get("doctype"):
|
||||
frappe.throw(_("Please select the document type first."))
|
||||
|
||||
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
|
||||
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
|
||||
|
||||
if not self.filters.get("company"):
|
||||
frappe.throw(_("Please select a company."))
|
||||
|
||||
if (
|
||||
self.filters.get("from_date")
|
||||
and self.filters.get("to_date")
|
||||
and self.filters.get("from_date") > self.filters.get("to_date")
|
||||
):
|
||||
frappe.throw(_("From Date cannot be greater than To Date."))
|
||||
|
||||
self._set_date_field_and_label()
|
||||
|
||||
def _set_date_field_and_label(self):
|
||||
self.date_field = (
|
||||
"transaction_date" if self.filters.get("doctype") == "Sales Order" else "posting_date"
|
||||
)
|
||||
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
|
||||
|
||||
def prepare_columns(self):
|
||||
def get_entries(filters):
|
||||
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
|
||||
company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency")
|
||||
conditions = get_conditions(filters, date_field)
|
||||
entries = frappe.db.sql(
|
||||
"""
|
||||
Extend this method to add columns on the report. Use `make_column` to add more columns.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
SELECT
|
||||
name, customer, territory, {} as posting_date, base_net_total as amount,
|
||||
sales_partner, commission_rate, total_commission, '{}' as currency
|
||||
FROM
|
||||
`tab{}`
|
||||
WHERE
|
||||
{} and docstatus = 1 and sales_partner is not null
|
||||
and sales_partner != '' order by name desc, sales_partner
|
||||
""".format(date_field, company_currency, filters.get("doctype"), conditions),
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
def get_data(self):
|
||||
self.build_report_query()
|
||||
|
||||
self.data = self.query.run(as_dict=1)
|
||||
|
||||
def build_report_query(self):
|
||||
self._build_report_base_query()
|
||||
self.extend_report_query()
|
||||
self._apply_common_filters()
|
||||
self.apply_filters()
|
||||
|
||||
def _build_report_base_query(self):
|
||||
self.dt = DocType(self.filters.get("doctype"))
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
|
||||
|
||||
self.query = (
|
||||
frappe.qb.from_(self.dt)
|
||||
.select(
|
||||
self.dt.name,
|
||||
self.dt.customer,
|
||||
self.dt.territory,
|
||||
Field(self.date_field, "posting_date", table=self.dt),
|
||||
self.dt.sales_partner,
|
||||
self.dt.commission_rate,
|
||||
ConstantColumn(company_currency).as_("currency"),
|
||||
)
|
||||
.where(
|
||||
(self.dt.docstatus == 1) & (self.dt.sales_partner.notnull()) & (self.dt.sales_partner != "")
|
||||
)
|
||||
.orderby(self.dt.name, order=Order.desc)
|
||||
.orderby(self.dt.sales_partner)
|
||||
)
|
||||
|
||||
def extend_report_query(self):
|
||||
"""
|
||||
Extend this method to select more columns on the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _apply_common_filters(self):
|
||||
for field in ["company", "customer", "territory", "sales_partner"]:
|
||||
if self.filters.get(field):
|
||||
self.query = self.query.where(Field(field, table=self.dt) == self.filters.get(field))
|
||||
|
||||
if self.filters.get("from_date"):
|
||||
self.query = self.query.where(
|
||||
Field(self.date_field, table=self.dt) >= self.filters.get("from_date")
|
||||
)
|
||||
|
||||
if self.filters.get("to_date"):
|
||||
self.query = self.query.where(
|
||||
Field(self.date_field, table=self.dt) <= self.filters.get("to_date")
|
||||
)
|
||||
|
||||
def apply_filters(self):
|
||||
"""
|
||||
Extend this method to add more conditions on the query.
|
||||
"""
|
||||
pass
|
||||
|
||||
def make_column(
|
||||
self, label: str, fieldname: str, fieldtype: str, width: int = 140, options: str = "", hidden: int = 0
|
||||
):
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=label,
|
||||
fieldname=fieldname,
|
||||
fieldtype=fieldtype,
|
||||
options=options,
|
||||
width=width,
|
||||
hidden=hidden,
|
||||
)
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
class SalesPartnerCommissionSummaryReport(SalesPartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
def get_conditions(filters, date_field):
|
||||
conditions = "1=1"
|
||||
|
||||
self.make_column(_("Customer"), "customer", "Link", options="Customer")
|
||||
for field in ["company", "customer", "territory"]:
|
||||
if filters.get(field):
|
||||
conditions += f" and {field} = %({field})s"
|
||||
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
if filters.get("sales_partner"):
|
||||
conditions += " and sales_partner = %(sales_partner)s"
|
||||
|
||||
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
|
||||
if filters.get("from_date"):
|
||||
conditions += f" and {date_field} >= %(from_date)s"
|
||||
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
if filters.get("to_date"):
|
||||
conditions += f" and {date_field} <= %(to_date)s"
|
||||
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
|
||||
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
|
||||
|
||||
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
|
||||
|
||||
self.make_column(_("Total Commission"), "total_commission", "Currency", 120, "currency")
|
||||
|
||||
def extend_report_query(self):
|
||||
self.query = self.query.select(
|
||||
self.dt.base_net_total.as_("amount"),
|
||||
self.dt.total_commission,
|
||||
)
|
||||
return conditions
|
||||
|
||||
@@ -3,14 +3,6 @@
|
||||
|
||||
frappe.query_reports["Sales Partner Transaction Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "sales_partner",
|
||||
label: __("Sales Partner"),
|
||||
@@ -36,6 +28,14 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
|
||||
@@ -3,84 +3,144 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
|
||||
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
|
||||
SalesPartnerSummaryReport,
|
||||
)
|
||||
from frappe import _, msgprint
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return SalesPartnerTransactionSummaryReport(filters=filters).run()
|
||||
columns = get_columns(filters)
|
||||
data = get_entries(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
class SalesPartnerTransactionSummaryReport(SalesPartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
def get_columns(filters):
|
||||
if not filters.get("doctype"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
|
||||
self.make_column(_("Customer"), "customer", "Link", options="Customer")
|
||||
columns = [
|
||||
{
|
||||
"label": _(filters["doctype"]),
|
||||
"options": filters["doctype"],
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Customer"),
|
||||
"options": "Customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"options": "Territory",
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item Group",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Brand"),
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"options": "Brand",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
|
||||
{"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
{
|
||||
"label": _("Sales Partner"),
|
||||
"options": "Sales Partner",
|
||||
"fieldname": "sales_partner",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Commission Rate %"),
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
]
|
||||
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
return columns
|
||||
|
||||
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
|
||||
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
def get_entries(filters):
|
||||
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
|
||||
|
||||
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
|
||||
conditions = get_conditions(filters, date_field)
|
||||
entries = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency,
|
||||
dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount,
|
||||
((dt_item.base_net_amount * dt.commission_rate) / 100) as commission,
|
||||
dt_item.brand, dt.sales_partner, dt.commission_rate, dt_item.item_group, dt_item.item_code
|
||||
FROM
|
||||
`tab{doctype}` dt, `tab{doctype} Item` dt_item
|
||||
WHERE
|
||||
{cond} and dt.name = dt_item.parent and dt.docstatus = 1
|
||||
and dt.sales_partner is not null and dt.sales_partner != ''
|
||||
order by dt.name desc, dt.sales_partner
|
||||
""".format(date_field=date_field, doctype=filters.get("doctype"), cond=conditions),
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
|
||||
return entries
|
||||
|
||||
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
|
||||
|
||||
self.make_column(_("Quantity"), "qty", "Float", 120)
|
||||
def get_conditions(filters, date_field):
|
||||
conditions = "1=1"
|
||||
|
||||
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
|
||||
for field in ["company", "customer", "territory", "sales_partner"]:
|
||||
if filters.get(field):
|
||||
conditions += f" and dt.{field} = %({field})s"
|
||||
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
if filters.get("from_date"):
|
||||
conditions += f" and dt.{date_field} >= %(from_date)s"
|
||||
|
||||
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
|
||||
if filters.get("to_date"):
|
||||
conditions += f" and dt.{date_field} <= %(to_date)s"
|
||||
|
||||
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
|
||||
if not filters.get("show_return_entries"):
|
||||
conditions += " and dt_item.qty > 0.0"
|
||||
|
||||
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
|
||||
if filters.get("brand"):
|
||||
conditions += " and dt_item.brand = %(brand)s"
|
||||
|
||||
def extend_report_query(self):
|
||||
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
|
||||
if filters.get("item_group"):
|
||||
lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"])
|
||||
|
||||
self.query = (
|
||||
self.query.join(self.dt_item)
|
||||
.on(self.dt.name == self.dt_item.parent)
|
||||
.select(
|
||||
self.dt_item.base_net_rate.as_("rate"),
|
||||
self.dt_item.qty,
|
||||
self.dt_item.base_net_amount.as_("amount"),
|
||||
Case()
|
||||
.when(
|
||||
self.dt_item.grant_commission.eq(1),
|
||||
((self.dt_item.base_net_amount * self.dt.commission_rate) / 100),
|
||||
)
|
||||
.else_(0)
|
||||
.as_("commission"),
|
||||
self.dt_item.brand,
|
||||
self.dt_item.item_group,
|
||||
self.dt_item.item_code,
|
||||
)
|
||||
)
|
||||
conditions += f""" and dt_item.item_group in (select name from
|
||||
`tabItem Group` where lft >= {lft} and rgt <= {rgt})"""
|
||||
|
||||
def apply_filters(self):
|
||||
if not self.filters.get("show_return_entries"):
|
||||
self.query = self.query.where(self.dt_item.qty > 0.0)
|
||||
|
||||
if self.filters.get("brand"):
|
||||
self.query = self.query.where(self.dt_item.brand == self.filters.get("brand"))
|
||||
|
||||
if self.filters.get("item_group"):
|
||||
lft, rgt = frappe.get_cached_value("Item Group", self.filters.get("item_group"), ["lft", "rgt"])
|
||||
if item_groups := frappe.get_all(
|
||||
"Item Group", filters=[["lft", ">=", lft], ["rgt", "<=", rgt]], pluck="name"
|
||||
):
|
||||
self.query = self.query.where(self.dt_item.item_group.isin(item_groups))
|
||||
return conditions
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"disabled",
|
||||
"column_break_ofhb",
|
||||
"copy_attachments_to_transaction",
|
||||
"applicable_modules_section",
|
||||
"selling",
|
||||
"buying",
|
||||
@@ -74,22 +72,12 @@
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ofhb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "copy_attachments_to_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Copy Attachments to Transaction"
|
||||
}
|
||||
],
|
||||
"icon": "icon-legal",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-29 22:51:49.285298",
|
||||
"modified": "2024-01-30 12:47:52.325531",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Terms and Conditions",
|
||||
|
||||
@@ -21,7 +21,6 @@ class TermsandConditions(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
buying: DF.Check
|
||||
copy_attachments_to_transaction: DF.Check
|
||||
disabled: DF.Check
|
||||
selling: DF.Check
|
||||
terms: DF.TextEditor | None
|
||||
|
||||
@@ -372,15 +372,6 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
||||
});
|
||||
}
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
this.frm.script_manager.copy_from_first_row("items", row, ["project"]);
|
||||
}
|
||||
}
|
||||
|
||||
make_sales_invoice() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
|
||||
@@ -759,6 +759,7 @@
|
||||
"label": "Incoming Rate",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -951,7 +952,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-07 15:44:20.892151",
|
||||
"modified": "2025-05-31 18:51:32.651562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
"field_order": [
|
||||
"dimension_details_tab",
|
||||
"dimension_name",
|
||||
"column_break_4",
|
||||
"reference_document",
|
||||
"column_break_4",
|
||||
"disabled",
|
||||
"field_mapping_section",
|
||||
"source_fieldname",
|
||||
"column_break_9",
|
||||
@@ -92,6 +93,12 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply to All Inventory Documents"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_fieldname",
|
||||
"fieldtype": "Data",
|
||||
@@ -152,7 +159,6 @@
|
||||
"label": "Conditional Rule Examples"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.apply_to_all_doctypes",
|
||||
"description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.",
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Small Text",
|
||||
@@ -182,7 +188,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-08 10:10:16.884388",
|
||||
"modified": "2025-07-07 15:51:29.329064",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Inventory Dimension",
|
||||
|
||||
@@ -31,6 +31,7 @@ class InventoryDimension(Document):
|
||||
apply_to_all_doctypes: DF.Check
|
||||
condition: DF.Code | None
|
||||
dimension_name: DF.Data
|
||||
disabled: DF.Check
|
||||
document_type: DF.Link | None
|
||||
fetch_from_parent: DF.Literal[None]
|
||||
istable: DF.Check
|
||||
@@ -74,6 +75,7 @@ class InventoryDimension(Document):
|
||||
|
||||
old_doc = self._doc_before_save
|
||||
allow_to_edit_fields = [
|
||||
"disabled",
|
||||
"fetch_from_parent",
|
||||
"type_of_transaction",
|
||||
"condition",
|
||||
@@ -117,7 +119,6 @@ class InventoryDimension(Document):
|
||||
def reset_value(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.type_of_transaction = ""
|
||||
self.mandatory_depends_on = ""
|
||||
|
||||
self.istable = 0
|
||||
for field in ["document_type", "condition"]:
|
||||
@@ -182,12 +183,8 @@ class InventoryDimension(Document):
|
||||
label=_(label),
|
||||
depends_on="eval:doc.s_warehouse" if doctype == "Stock Entry Detail" else "",
|
||||
search_index=1,
|
||||
reqd=1
|
||||
if self.reqd and not self.mandatory_depends_on and doctype != "Stock Entry Detail"
|
||||
else 0,
|
||||
mandatory_depends_on="eval:doc.s_warehouse"
|
||||
if self.reqd and doctype == "Stock Entry Detail"
|
||||
else self.mandatory_depends_on,
|
||||
reqd=self.reqd,
|
||||
mandatory_depends_on=self.mandatory_depends_on,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -299,13 +296,12 @@ class InventoryDimension(Document):
|
||||
options=self.reference_document,
|
||||
label=label,
|
||||
depends_on=display_depends_on,
|
||||
mandatory_depends_on=display_depends_on if self.reqd else self.mandatory_depends_on,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def field_exists(doctype, fieldname) -> str | None:
|
||||
def field_exists(doctype, fieldname) -> str or None:
|
||||
return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name")
|
||||
|
||||
|
||||
@@ -378,6 +374,7 @@ def get_document_wise_inventory_dimensions(doctype) -> dict:
|
||||
"type_of_transaction",
|
||||
"fetch_from_parent",
|
||||
],
|
||||
filters={"disabled": 0},
|
||||
or_filters={"document_type": doctype, "apply_to_all_doctypes": 1},
|
||||
)
|
||||
|
||||
@@ -400,6 +397,7 @@ def get_inventory_dimensions():
|
||||
"reference_document as doctype",
|
||||
"validate_negative_stock",
|
||||
],
|
||||
filters={"disabled": 0},
|
||||
)
|
||||
|
||||
frappe.local.inventory_dimensions = dimensions
|
||||
|
||||
@@ -220,9 +220,9 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
doc = create_inventory_dimension(
|
||||
reference_document="Pallet",
|
||||
type_of_transaction="Outward",
|
||||
dimension_name="Pallet 75",
|
||||
dimension_name="Pallet",
|
||||
apply_to_all_doctypes=0,
|
||||
document_type="Delivery Note Item",
|
||||
document_type="Stock Entry Detail",
|
||||
)
|
||||
|
||||
doc.reqd = 1
|
||||
@@ -230,7 +230,7 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.get_value(
|
||||
"Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item", "reqd": 1}, "name"
|
||||
"Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -791,20 +791,6 @@ class Item(Document):
|
||||
{"company": defaults.get("company"), "default_warehouse": defaults.default_warehouse},
|
||||
)
|
||||
|
||||
item_group = frappe.get_cached_doc("Item Group", self.item_group)
|
||||
if not self.taxes and item_group.taxes:
|
||||
for tax in item_group.taxes:
|
||||
self.append(
|
||||
"taxes",
|
||||
{
|
||||
"item_tax_template": tax.item_tax_template,
|
||||
"tax_category": tax.tax_category,
|
||||
"valid_from": tax.valid_from,
|
||||
"minimum_net_rate": tax.minimum_net_rate,
|
||||
"maximum_net_rate": tax.maximum_net_rate,
|
||||
},
|
||||
)
|
||||
|
||||
def update_variants(self):
|
||||
if self.flags.dont_update_variants or frappe.db.get_single_value(
|
||||
"Item Variant Settings", "do_not_update_variants"
|
||||
|
||||
@@ -368,13 +368,11 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
|
||||
items_add(doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = ["expense_account", "cost_center"];
|
||||
if (doc.project) {
|
||||
frappe.model.set_value(cdt, cdn, "project", doc.project);
|
||||
} else {
|
||||
field_copy.push("project");
|
||||
}
|
||||
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
|
||||
this.frm.script_manager.copy_from_first_row("items", row, [
|
||||
"expense_account",
|
||||
"cost_center",
|
||||
"project",
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -510,14 +510,7 @@ class PurchaseReceipt(BuyingController):
|
||||
else flt(item.net_amount, item.precision("net_amount"))
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
outgoing_amount = item.qty * item.base_net_rate
|
||||
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
|
||||
@@ -673,9 +666,6 @@ class PurchaseReceipt(BuyingController):
|
||||
or stock_asset_rbnb
|
||||
)
|
||||
|
||||
if self.is_return and item.expense_account:
|
||||
loss_account = item.expense_account
|
||||
|
||||
cost_center = item.cost_center or frappe.get_cached_value(
|
||||
"Company", self.company, "cost_center"
|
||||
)
|
||||
|
||||
@@ -734,6 +734,7 @@
|
||||
"oldfieldname": "valuation_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"precision": "6",
|
||||
"print_hide": 1,
|
||||
"print_width": "80px",
|
||||
"read_only": 1,
|
||||
@@ -1042,7 +1043,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||
"fieldname": "add_serial_batch_for_rejected_qty",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No (Rejected Qty)"
|
||||
@@ -1057,7 +1058,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
|
||||
"depends_on": "eval:doc.use_serial_batch_fields === 0",
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
@@ -1148,7 +1149,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-29 16:01:34.154697",
|
||||
"modified": "2025-10-14 12:59:20.384056",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
||||
@@ -58,7 +58,6 @@ frappe.ui.form.on("Quality Inspection", {
|
||||
if (doc.reference_type && doc.reference_name) {
|
||||
let filters = {
|
||||
from: doctype,
|
||||
parent_doctype: doc.reference_type,
|
||||
inspection_type: doc.inspection_type,
|
||||
};
|
||||
|
||||
|
||||
@@ -364,11 +364,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
from_doctype = cstr(filters.get("from"))
|
||||
parent_doctype = cstr(filters.get("parent_doctype"))
|
||||
if not from_doctype or not frappe.db.exists("DocType", from_doctype):
|
||||
return []
|
||||
|
||||
mcond = get_match_cond(parent_doctype or from_doctype)
|
||||
mcond = get_match_cond(from_doctype)
|
||||
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
|
||||
|
||||
if filters.get("parent"):
|
||||
@@ -392,10 +391,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""
|
||||
SELECT distinct `tab{from_doctype}`.item_code, `tab{from_doctype}`.item_name
|
||||
SELECT distinct item_code, item_name
|
||||
FROM `tab{from_doctype}`
|
||||
JOIN `tab{parent_doctype}` ON `tab{parent_doctype}`.name = `tab{from_doctype}`.parent
|
||||
WHERE `tab{from_doctype}`.parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and `tab{from_doctype}`.item_code like %(txt)s
|
||||
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
|
||||
{qi_condition} {cond} {mcond}
|
||||
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
|
||||
""",
|
||||
|
||||
@@ -356,15 +356,8 @@ def repost(doc):
|
||||
message = message.get("message")
|
||||
|
||||
status = "Failed"
|
||||
# If failed because of a recoverable error (timeout, deadlock), set status to In Progress
|
||||
# so the scheduler automatically retries instead of leaving it permanently failed.
|
||||
# NOTE: isinstance check comes first because the traceback string matching is unreliable
|
||||
# when SIGALRM kills the process mid-C-extension (JobTimeoutException may not appear
|
||||
# in the traceback if the exception handler itself was interrupted).
|
||||
traceback_lower = traceback.lower() if traceback else ""
|
||||
if isinstance(e, RecoverableErrors) or (
|
||||
traceback_lower and ("timeout" in traceback_lower or "deadlock found" in traceback_lower)
|
||||
):
|
||||
# If failed because of timeout, set status to In Progress
|
||||
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
|
||||
status = "In Progress"
|
||||
|
||||
if traceback:
|
||||
|
||||
@@ -925,7 +925,6 @@ class SerialandBatchBundle(Document):
|
||||
parent.voucher_type,
|
||||
parent.voucher_no,
|
||||
)
|
||||
.distinct()
|
||||
.where(
|
||||
(child.parent != self.name)
|
||||
& (parent.item_code == self.item_code)
|
||||
|
||||
@@ -346,9 +346,6 @@ class StockEntry(StockController):
|
||||
def _set_serial_batch_for_disassembly_from_available_materials(self):
|
||||
available_materials = get_available_materials(self.work_order, self)
|
||||
for row in self.items:
|
||||
if row.serial_no or row.batch_no or row.serial_and_batch_bundle:
|
||||
continue
|
||||
|
||||
warehouse = row.s_warehouse or row.t_warehouse
|
||||
materials = available_materials.get((row.item_code, warehouse))
|
||||
if not materials:
|
||||
|
||||
@@ -270,7 +270,8 @@
|
||||
"oldfieldname": "transfer_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -616,7 +617,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-27 11:40:38.294196",
|
||||
"modified": "2026-03-02 14:05:23.116017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
||||
@@ -1793,47 +1793,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
elif s.id_plant == plant_b.name:
|
||||
self.assertEqual(s.actual_qty, 3)
|
||||
|
||||
def test_serial_no_status_with_backdated_stock_reco(self):
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
item_code = self.make_item(
|
||||
"Test Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SERIAL.###",
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(nowdate(), -2),
|
||||
warehouse=warehouse,
|
||||
qty=1,
|
||||
rate=80,
|
||||
purpose="Opening Stock",
|
||||
)
|
||||
|
||||
serial_no = get_serial_nos_from_bundle(reco.items[0].serial_and_batch_bundle)[0]
|
||||
|
||||
create_delivery_note(
|
||||
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
|
||||
)
|
||||
|
||||
self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
|
||||
reco = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
posting_date=add_days(nowdate(), -1),
|
||||
warehouse=warehouse,
|
||||
qty=1,
|
||||
rate=90,
|
||||
)
|
||||
|
||||
self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -101,23 +101,49 @@ class Warehouse(NestedSet):
|
||||
def warn_about_multiple_warehouse_account(self):
|
||||
"If Warehouse value is split across multiple accounts, warn."
|
||||
|
||||
if not frappe.db.count("Stock Ledger Entry", {"warehouse": self.name}):
|
||||
def get_accounts_where_value_is_booked(name):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
ac = frappe.qb.DocType("Account")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(sle)
|
||||
.join(gle)
|
||||
.on(sle.voucher_no == gle.voucher_no)
|
||||
.join(ac)
|
||||
.on(ac.name == gle.account)
|
||||
.select(gle.account)
|
||||
.distinct()
|
||||
.where((sle.warehouse == name) & (ac.account_type == "Stock"))
|
||||
.orderby(sle.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
old_wh_account = doc_before_save.account if doc_before_save else None
|
||||
old_wh_account = frappe.db.get_value("Warehouse", self.name, "account")
|
||||
|
||||
if self.is_new() or (self.account and old_wh_account == self.account):
|
||||
return
|
||||
# WH account is being changed or set get all accounts against which wh value is booked
|
||||
if self.account != old_wh_account:
|
||||
accounts = get_accounts_where_value_is_booked(self.name)
|
||||
accounts = [d.account for d in accounts]
|
||||
|
||||
frappe.msgprint(
|
||||
title=_("Warning: Account changed for warehouse"),
|
||||
indicator="orange",
|
||||
msg=_(
|
||||
"Stock entries exist with the old account. Changing the account may lead to a mismatch between the warehouse closing balance and the account closing balance. The overall closing balance will still match, but not for the specific account."
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
if not accounts or (len(accounts) == 1 and self.account in accounts):
|
||||
# if same singular account has stock value booked ignore
|
||||
return
|
||||
|
||||
warning = _("Warehouse's Stock Value has already been booked in the following accounts:")
|
||||
account_str = "<br>" + ", ".join(frappe.bold(ac) for ac in accounts)
|
||||
reason = "<br><br>" + _(
|
||||
"Booking stock value across multiple accounts will make it harder to track stock and account value."
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
warning + account_str + reason,
|
||||
title=_("Multiple Warehouse Accounts"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
def check_if_sle_exists(self):
|
||||
return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})
|
||||
|
||||
@@ -7,7 +7,6 @@ import json
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
@@ -673,9 +672,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_tax_template(
|
||||
args: str | dict, item: Document | None = None, out: dict | None = None
|
||||
) -> str | None:
|
||||
def get_item_tax_template(args, item=None, out=None):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
@@ -690,7 +687,11 @@ def get_item_tax_template(
|
||||
item_tax_template = _get_item_tax_template(args, item.taxes, out)
|
||||
|
||||
if not item_tax_template:
|
||||
item_tax_template = _get_item_tax_template_from_item_group(args, item.item_group, out)
|
||||
item_group = item.item_group
|
||||
while item_group and not item_tax_template:
|
||||
item_group_doc = frappe.get_cached_doc("Item Group", item_group)
|
||||
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
||||
item_group = item_group_doc.parent_item_group
|
||||
|
||||
if out and args.get("child_doctype") and item_tax_template:
|
||||
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
|
||||
@@ -698,26 +699,7 @@ def get_item_tax_template(
|
||||
return item_tax_template
|
||||
|
||||
|
||||
def _get_item_tax_template_from_item_group(
|
||||
args: dict, item_group: str, out: dict | None = None
|
||||
) -> str | None:
|
||||
from frappe.utils.nestedset import get_ancestors_of
|
||||
|
||||
ancestors = get_ancestors_of("Item Group", item_group)
|
||||
for group in [item_group, *ancestors]:
|
||||
group_doc = frappe.get_cached_doc("Item Group", group)
|
||||
item_tax_template = _get_item_tax_template(args, group_doc.taxes, out)
|
||||
if item_tax_template:
|
||||
return item_tax_template
|
||||
return None
|
||||
|
||||
|
||||
def _get_item_tax_template(
|
||||
args: dict,
|
||||
taxes,
|
||||
out: dict | None = None,
|
||||
for_validate: bool = False,
|
||||
) -> str | list[str] | None:
|
||||
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
if out is None:
|
||||
out = {}
|
||||
taxes_with_validity = []
|
||||
@@ -1049,7 +1031,6 @@ def insert_item_price(args):
|
||||
currency=args.currency,
|
||||
uom=args.stock_uom,
|
||||
price_list=args.price_list,
|
||||
valid_from=transaction_date,
|
||||
)
|
||||
item_price.insert()
|
||||
frappe.msgprint(
|
||||
@@ -1074,7 +1055,6 @@ def insert_item_price(args):
|
||||
"currency": args.currency,
|
||||
"price_list_rate": price_list_rate,
|
||||
"uom": args.stock_uom,
|
||||
"valid_from": transaction_date,
|
||||
}
|
||||
)
|
||||
item_price.insert()
|
||||
|
||||
@@ -219,7 +219,7 @@ def get_item_warehouse_batch_map(filters, float_precision):
|
||||
)
|
||||
|
||||
qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision)
|
||||
qty_dict.bal_value += flt(d.stock_value_difference)
|
||||
qty_dict.bal_value += flt(d.stock_value_difference, float_precision)
|
||||
|
||||
return iwb_map
|
||||
|
||||
|
||||
@@ -90,62 +90,45 @@ def get_data(filters) -> list[dict]:
|
||||
batch_negative_data = []
|
||||
|
||||
flt_precision = frappe.db.get_default("float_precision") or 2
|
||||
distinct_batches = set()
|
||||
for company in companies:
|
||||
warehouses = get_warehouses(filters, company)
|
||||
for warehouse in warehouses:
|
||||
for batch in batches:
|
||||
_c, data = stock_ledger_execute(
|
||||
frappe._dict(
|
||||
for batch in batches:
|
||||
_c, data = stock_ledger_execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"batch_no": batch,
|
||||
"from_date": add_to_date(today(), years=-12),
|
||||
"to_date": today(),
|
||||
"segregate_serial_batch_bundle": 1,
|
||||
"warehouse": filters.get("warehouse"),
|
||||
"valuation_field_type": "Currency",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
previous_qty = 0
|
||||
for row in data:
|
||||
if flt(row.get("qty_after_transaction"), flt_precision) < 0:
|
||||
batch_negative_data.append(
|
||||
{
|
||||
"company": company,
|
||||
"batch_no": batch,
|
||||
"from_date": add_to_date(today(), years=-12),
|
||||
"to_date": today(),
|
||||
"segregate_serial_batch_bundle": 1,
|
||||
"warehouse": warehouse,
|
||||
"valuation_field_type": "Currency",
|
||||
"posting_date": row.get("date"),
|
||||
"batch_no": row.get("batch_no"),
|
||||
"item_code": row.get("item_code"),
|
||||
"item_name": row.get("item_name"),
|
||||
"warehouse": row.get("warehouse"),
|
||||
"actual_qty": row.get("actual_qty"),
|
||||
"qty_after_transaction": row.get("qty_after_transaction"),
|
||||
"previous_qty": previous_qty,
|
||||
"voucher_type": row.get("voucher_type"),
|
||||
"voucher_no": row.get("voucher_no"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
previous_qty = 0
|
||||
for row in data:
|
||||
key = (row.get("warehouse"), batch)
|
||||
if key in distinct_batches:
|
||||
continue
|
||||
|
||||
if flt(row.get("qty_after_transaction"), flt_precision) < 0:
|
||||
batch_negative_data.append(
|
||||
{
|
||||
"posting_date": row.get("date"),
|
||||
"batch_no": row.get("batch_no"),
|
||||
"item_code": row.get("item_code"),
|
||||
"item_name": row.get("item_name"),
|
||||
"warehouse": row.get("warehouse"),
|
||||
"actual_qty": row.get("actual_qty"),
|
||||
"qty_after_transaction": row.get("qty_after_transaction"),
|
||||
"previous_qty": previous_qty,
|
||||
"voucher_type": row.get("voucher_type"),
|
||||
"voucher_no": row.get("voucher_no"),
|
||||
}
|
||||
)
|
||||
|
||||
distinct_batches.add(key)
|
||||
|
||||
previous_qty = row.get("qty_after_transaction")
|
||||
previous_qty = row.get("qty_after_transaction")
|
||||
|
||||
return batch_negative_data
|
||||
|
||||
|
||||
def get_warehouses(filters, company):
|
||||
warehouse_filters = {"company": company, "disabled": 0}
|
||||
if filters.get("warehouse"):
|
||||
warehouse_filters["name"] = filters["warehouse"]
|
||||
|
||||
return frappe.get_all("Warehouse", pluck="name", filters=warehouse_filters)
|
||||
|
||||
|
||||
def get_batches(filters):
|
||||
batch_filters = {}
|
||||
if filters.get("item_code"):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user