mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-29 19:48:27 +00:00
Merge branch 'develop' into unit-price-contract-2
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
exclude: 'node_modules|.git'
|
||||
default_stages: [commit]
|
||||
default_stages: [pre-commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<p>Powerful, Intuitive and Open-Source ERP</p>
|
||||
</p>
|
||||
|
||||
[](https://frappe.school)<br><br>
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
|
||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||
|
||||
@@ -141,6 +142,7 @@ To setup the repository locally follow the steps mentioned below:
|
||||
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
|
||||
1. [Report Security Vulnerabilities](https://erpnext.com/security)
|
||||
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
2. [Translations](https://crowdin.com/project/frappe)
|
||||
|
||||
|
||||
## Logo and Trademark Policy
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"column_break_xrnd",
|
||||
"use_sales_invoice_in_pos",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
@@ -532,14 +534,26 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.",
|
||||
"fieldname": "use_sales_invoice_in_pos",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Sales Invoice"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-23 13:15:44.077853",
|
||||
"modified": "2025-03-30 20:47:17.954736",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -564,8 +578,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ class AccountsSettings(Document):
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_sales_invoice_in_pos: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -92,6 +93,9 @@ class AccountsSettings(Document):
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if old_doc.use_sales_invoice_in_pos != self.use_sales_invoice_in_pos:
|
||||
self.validate_invoice_mode_switch_in_pos()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
@@ -135,3 +139,15 @@ class AccountsSettings(Document):
|
||||
if self.has_value_changed("reconciliation_queue_size"):
|
||||
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
||||
frappe.throw(_("Queue Size should be between 5 and 100"))
|
||||
|
||||
def validate_invoice_mode_switch_in_pos(self):
|
||||
pos_opening_entries_count = frappe.db.count(
|
||||
"POS Opening Entry", filters={"docstatus": 1, "status": "Open"}
|
||||
)
|
||||
if pos_opening_entries_count:
|
||||
frappe.throw(
|
||||
_("{0} can be enabled/disabled after all the POS Opening Entries are closed.").format(
|
||||
frappe.bold(_("Use Sales Invoice"))
|
||||
),
|
||||
title=_("Switch Invoice Mode Error"),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model import mapper
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import add_days, nowdate, today
|
||||
|
||||
@@ -77,6 +80,36 @@ class TestDunning(IntegrationTestCase):
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
def test_fetch_overdue_payments(self):
|
||||
"""
|
||||
Create SI with overdue payment. Check if overdue payment is fetched in Dunning.
|
||||
"""
|
||||
si1 = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -1 * 6),
|
||||
qty=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
si2 = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -1 * 6),
|
||||
qty=1,
|
||||
rate=300,
|
||||
)
|
||||
|
||||
dunning = create_dunning_from_sales_invoice(si1.name)
|
||||
dunning.overdue_payments = []
|
||||
|
||||
method = "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning"
|
||||
updated_dunning = mapper.map_docs(method, json.dumps([si1.name, si2.name]), dunning)
|
||||
|
||||
self.assertEqual(len(updated_dunning.overdue_payments), 2)
|
||||
|
||||
self.assertEqual(updated_dunning.overdue_payments[0].sales_invoice, si1.name)
|
||||
self.assertEqual(updated_dunning.overdue_payments[0].outstanding, si1.outstanding_amount)
|
||||
|
||||
self.assertEqual(updated_dunning.overdue_payments[1].sales_invoice, si2.name)
|
||||
self.assertEqual(updated_dunning.overdue_payments[1].outstanding, si2.outstanding_amount)
|
||||
|
||||
def test_dunning_and_payment_against_partially_due_invoice(self):
|
||||
"""
|
||||
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
|
||||
|
||||
@@ -3025,6 +3025,8 @@ def get_payment_entry(
|
||||
party_account_currency if payment_type == "Receive" else bank.account_currency
|
||||
)
|
||||
pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency
|
||||
pe.paid_from_account_type = frappe.db.get_value("Account", pe.paid_from, "account_type")
|
||||
pe.paid_to_account_type = frappe.db.get_value("Account", pe.paid_to, "account_type")
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
@@ -3388,26 +3390,25 @@ def set_paid_amount_and_received_amount(
|
||||
if party_account_currency == bank.account_currency:
|
||||
paid_amount = received_amount = abs(outstanding_amount)
|
||||
else:
|
||||
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
|
||||
if payment_type == "Receive":
|
||||
paid_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
received_amount = bank_amount
|
||||
else:
|
||||
if bank and company_currency != bank.account_currency:
|
||||
received_amount = paid_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
# settings if it is for receive
|
||||
paid_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
received_amount = bank_amount
|
||||
else:
|
||||
received_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
paid_amount = bank_amount
|
||||
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
|
||||
if bank and company_currency != bank.account_currency:
|
||||
# doc currency can be different from bank currency
|
||||
posting_date = doc.get("posting_date") or doc.get("transaction_date")
|
||||
conversion_rate = get_exchange_rate(
|
||||
bank.account_currency, party_account_currency, posting_date
|
||||
)
|
||||
received_amount = paid_amount / conversion_rate
|
||||
else:
|
||||
if bank and company_currency != bank.account_currency:
|
||||
paid_amount = received_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
# if party account currency and bank currency is different then populate paid amount as well
|
||||
paid_amount = received_amount * doc.get("conversion_rate", 1)
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
|
||||
# if payment type is pay, then paid amount and received amount are swapped
|
||||
if payment_type == "Pay":
|
||||
paid_amount, received_amount = received_amount, paid_amount
|
||||
|
||||
return paid_amount, received_amount
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("POS Closing Entry", {
|
||||
onload: function (frm) {
|
||||
onload: async function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log"];
|
||||
frm.set_query("pos_profile", function (doc) {
|
||||
return {
|
||||
@@ -36,6 +36,15 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
}
|
||||
});
|
||||
|
||||
const is_pos_using_sales_invoice = await frappe.db.get_single_value(
|
||||
"Accounts Settings",
|
||||
"use_sales_invoice_in_pos"
|
||||
);
|
||||
|
||||
if (is_pos_using_sales_invoice) {
|
||||
frm.set_df_property("pos_transactions", "hidden", 1);
|
||||
}
|
||||
|
||||
set_html_data(frm);
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
@@ -83,6 +92,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
|
||||
() => frm.trigger("set_opening_amounts"),
|
||||
() => frm.trigger("get_pos_invoices"),
|
||||
() => frm.trigger("get_sales_invoices"),
|
||||
() => frappe.dom.unfreeze(),
|
||||
]);
|
||||
}
|
||||
@@ -113,7 +123,25 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
},
|
||||
callback: (r) => {
|
||||
let pos_docs = r.message;
|
||||
set_form_data(pos_docs, frm);
|
||||
set_pos_transaction_form_data(pos_docs, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
get_sales_invoices(frm) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_sales_invoices",
|
||||
args: {
|
||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
|
||||
pos_profile: frm.doc.pos_profile,
|
||||
user: frm.doc.user,
|
||||
},
|
||||
callback: (r) => {
|
||||
let sales_docs = r.message;
|
||||
set_sales_invoice_transaction_form_data(sales_docs, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
},
|
||||
@@ -132,9 +160,40 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
row.expected_amount = row.opening_amount;
|
||||
}
|
||||
|
||||
const is_pos_using_sales_invoice = await frappe.db.get_single_value(
|
||||
"Accounts Settings",
|
||||
"use_sales_invoice_in_pos"
|
||||
);
|
||||
|
||||
if (is_pos_using_sales_invoice) {
|
||||
await Promise.all([
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices",
|
||||
args: {
|
||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
|
||||
pos_profile: frm.doc.pos_profile,
|
||||
user: frm.doc.user,
|
||||
},
|
||||
callback: (r) => {
|
||||
let pos_invoices = r.message;
|
||||
for (let doc of pos_invoices) {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm, false);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
}
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices",
|
||||
method: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_sales_invoices",
|
||||
args: {
|
||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
|
||||
@@ -142,8 +201,8 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
user: frm.doc.user,
|
||||
},
|
||||
callback: (r) => {
|
||||
let pos_invoices = r.message;
|
||||
for (let doc of pos_invoices) {
|
||||
let sales_invoices = r.message;
|
||||
for (let doc of sales_invoices) {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
@@ -155,6 +214,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
frappe.dom.unfreeze();
|
||||
},
|
||||
});
|
||||
@@ -166,7 +226,7 @@ frappe.ui.form.on("POS Closing Entry Detail", {
|
||||
},
|
||||
});
|
||||
|
||||
function set_form_data(data, frm) {
|
||||
function set_pos_transaction_form_data(data, frm) {
|
||||
data.forEach((d) => {
|
||||
add_to_pos_transaction(d, frm);
|
||||
frm.doc.grand_total += flt(d.grand_total);
|
||||
@@ -177,6 +237,17 @@ function set_form_data(data, frm) {
|
||||
});
|
||||
}
|
||||
|
||||
function set_sales_invoice_transaction_form_data(data, frm) {
|
||||
data.forEach((d) => {
|
||||
add_to_sales_invoice_transaction(d, frm);
|
||||
frm.doc.grand_total += flt(d.grand_total);
|
||||
frm.doc.net_total += flt(d.net_total);
|
||||
frm.doc.total_quantity += flt(d.total_qty);
|
||||
refresh_payments(d, frm, true);
|
||||
refresh_taxes(d, frm);
|
||||
});
|
||||
}
|
||||
|
||||
function add_to_pos_transaction(d, frm) {
|
||||
frm.add_child("pos_transactions", {
|
||||
pos_invoice: d.name,
|
||||
@@ -186,6 +257,15 @@ function add_to_pos_transaction(d, frm) {
|
||||
});
|
||||
}
|
||||
|
||||
function add_to_sales_invoice_transaction(d, frm) {
|
||||
frm.add_child("sales_invoice_transactions", {
|
||||
sales_invoice: d.name,
|
||||
posting_date: d.posting_date,
|
||||
grand_total: d.grand_total,
|
||||
customer: d.customer,
|
||||
});
|
||||
}
|
||||
|
||||
function refresh_payments(d, frm, is_new) {
|
||||
d.payments.forEach((p) => {
|
||||
const payment = frm.doc.payment_reconciliation.find(
|
||||
@@ -226,6 +306,7 @@ function refresh_taxes(d, frm) {
|
||||
|
||||
function reset_values(frm) {
|
||||
frm.set_value("pos_transactions", []);
|
||||
frm.set_value("sales_invoice_transactions", []);
|
||||
frm.set_value("payment_reconciliation", []);
|
||||
frm.set_value("taxes", []);
|
||||
frm.set_value("grand_total", 0);
|
||||
@@ -235,6 +316,7 @@ function reset_values(frm) {
|
||||
|
||||
function refresh_fields(frm) {
|
||||
frm.refresh_field("pos_transactions");
|
||||
frm.refresh_field("sales_invoice_transactions");
|
||||
frm.refresh_field("payment_reconciliation");
|
||||
frm.refresh_field("taxes");
|
||||
frm.refresh_field("grand_total");
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"user",
|
||||
"section_break_12",
|
||||
"pos_transactions",
|
||||
"sales_invoice_transactions",
|
||||
"section_break_9",
|
||||
"payment_reconciliation_details",
|
||||
"section_break_11",
|
||||
@@ -227,8 +228,15 @@
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_invoice_transactions",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sales Invoice Transactions",
|
||||
"options": "Sales Invoice Reference"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
{
|
||||
@@ -236,7 +244,7 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-27 13:10:14.073467",
|
||||
"modified": "2025-03-19 19:49:58.845697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
@@ -285,8 +293,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ class POSClosingEntry(StatusUpdater):
|
||||
from erpnext.accounts.doctype.pos_closing_entry_taxes.pos_closing_entry_taxes import (
|
||||
POSClosingEntryTaxes,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
|
||||
POSInvoiceReference,
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
|
||||
from erpnext.accounts.doctype.sales_invoice_reference.sales_invoice_reference import (
|
||||
SalesInvoiceReference,
|
||||
)
|
||||
|
||||
amended_from: DF.Link | None
|
||||
@@ -45,6 +46,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
pos_transactions: DF.Table[POSInvoiceReference]
|
||||
posting_date: DF.Date
|
||||
posting_time: DF.Time
|
||||
sales_invoice_transactions: DF.Table[SalesInvoiceReference]
|
||||
status: DF.Literal["Draft", "Submitted", "Queued", "Failed", "Cancelled"]
|
||||
taxes: DF.Table[POSClosingEntryTaxes]
|
||||
total_quantity: DF.Float
|
||||
@@ -58,8 +60,20 @@ class POSClosingEntry(StatusUpdater):
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
|
||||
self.validate_duplicate_pos_invoices()
|
||||
self.validate_pos_invoices()
|
||||
self.is_pos_using_sales_invoice = frappe.db.get_single_value(
|
||||
"Accounts Settings", "use_sales_invoice_in_pos"
|
||||
)
|
||||
|
||||
if self.is_pos_using_sales_invoice == 0:
|
||||
self.validate_duplicate_pos_invoices()
|
||||
self.validate_pos_invoices()
|
||||
|
||||
if self.is_pos_using_sales_invoice == 1:
|
||||
if len(self.pos_transactions) != 0:
|
||||
frappe.throw(_("POS Invoices can't be added when Sales Invoice is enabled"))
|
||||
|
||||
self.validate_duplicate_sales_invoices()
|
||||
self.validate_sales_invoices()
|
||||
|
||||
def validate_duplicate_pos_invoices(self):
|
||||
pos_occurences = {}
|
||||
@@ -114,6 +128,71 @@ class POSClosingEntry(StatusUpdater):
|
||||
|
||||
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
|
||||
|
||||
def validate_duplicate_sales_invoices(self):
|
||||
sales_invoice_occurrences = {}
|
||||
for idx, inv in enumerate(self.sales_invoice_transactions, 1):
|
||||
sales_invoice_occurrences.setdefault(inv.sales_invoice, []).append(idx)
|
||||
|
||||
error_list = []
|
||||
for key, value in sales_invoice_occurrences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_("{0} is added multiple times on rows: {1}").format(frappe.bold(key), frappe.bold(value))
|
||||
)
|
||||
|
||||
if error_list:
|
||||
frappe.throw(error_list, title=_("Duplicate Sales Invoices found"), as_list=True)
|
||||
|
||||
def validate_sales_invoices(self):
|
||||
invalid_rows = []
|
||||
for d in self.sales_invoice_transactions:
|
||||
invalid_row = {"idx": d.idx}
|
||||
sales_invoice = frappe.db.get_values(
|
||||
"Sales Invoice",
|
||||
d.sales_invoice,
|
||||
[
|
||||
"pos_profile",
|
||||
"docstatus",
|
||||
"is_pos",
|
||||
"owner",
|
||||
"is_created_using_pos",
|
||||
"is_consolidated",
|
||||
"pos_closing_entry",
|
||||
],
|
||||
as_dict=1,
|
||||
)[0]
|
||||
if sales_invoice.pos_closing_entry:
|
||||
invalid_row.setdefault("msg", []).append(_("Sales Invoice is already consolidated"))
|
||||
invalid_rows.append(invalid_row)
|
||||
continue
|
||||
if sales_invoice.is_pos == 0:
|
||||
invalid_row.setdefault("msg", []).append(_("Sales Invoice does not have Payments"))
|
||||
if sales_invoice.is_created_using_pos == 0:
|
||||
invalid_row.setdefault("msg", []).append(_("Sales Invoice is not created using POS"))
|
||||
if sales_invoice.pos_profile != self.pos_profile:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
|
||||
)
|
||||
if sales_invoice.docstatus != 1:
|
||||
invalid_row.setdefault("msg", []).append(_("Sales Invoice is not submitted"))
|
||||
if sales_invoice.owner != self.user:
|
||||
invalid_row.setdefault("msg", []).append(
|
||||
_("Sales Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
||||
)
|
||||
|
||||
if invalid_row.get("msg"):
|
||||
invalid_rows.append(invalid_row)
|
||||
|
||||
if not invalid_rows:
|
||||
return
|
||||
|
||||
error_list = []
|
||||
for row in invalid_rows:
|
||||
for msg in row.get("msg"):
|
||||
error_list.append(_("Row #{}: {}").format(row.get("idx"), msg))
|
||||
|
||||
frappe.throw(error_list, title=_("Invalid Sales Invoices"), as_list=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_reconciliation_details(self):
|
||||
currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
@@ -130,9 +209,13 @@ class POSClosingEntry(StatusUpdater):
|
||||
docname=f"POS Opening Entry/{self.pos_opening_entry}",
|
||||
)
|
||||
|
||||
self.update_sales_invoices_closing_entry()
|
||||
|
||||
def on_cancel(self):
|
||||
unconsolidate_pos_invoices(closing_entry=self)
|
||||
|
||||
self.update_sales_invoices_closing_entry(cancel=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def retry(self):
|
||||
consolidate_pos_invoices(closing_entry=self)
|
||||
@@ -143,6 +226,12 @@ class POSClosingEntry(StatusUpdater):
|
||||
opening_entry.set_status()
|
||||
opening_entry.save()
|
||||
|
||||
def update_sales_invoices_closing_entry(self, cancel=False):
|
||||
for d in self.sales_invoice_transactions:
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice", d.sales_invoice, "pos_closing_entry", self.name if not cancel else None
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -173,6 +262,33 @@ def get_pos_invoices(start, end, pos_profile, user):
|
||||
return data
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_sales_invoices(start, end, pos_profile, user):
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, timestamp(posting_date, posting_time) as "timestamp"
|
||||
from
|
||||
`tabSales Invoice`
|
||||
where
|
||||
owner = %s
|
||||
and docstatus = 1
|
||||
and is_pos = 1
|
||||
and pos_profile = %s
|
||||
and is_created_using_pos = 1
|
||||
and ifnull(pos_closing_entry,'') = ''
|
||||
""",
|
||||
(user, pos_profile),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
data = [d for d in data if get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end)]
|
||||
# need to get taxes and payments so can't avoid get_doc
|
||||
data = [frappe.get_doc("Sales Invoice", d.name).as_dict() for d in data]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def make_closing_entry_from_opening(opening_entry):
|
||||
closing_entry = frappe.new_doc("POS Closing Entry")
|
||||
closing_entry.pos_opening_entry = opening_entry.name
|
||||
@@ -185,7 +301,20 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
closing_entry.net_total = 0
|
||||
closing_entry.total_quantity = 0
|
||||
|
||||
invoices = get_pos_invoices(
|
||||
is_pos_using_sales_invoice = frappe.db.get_single_value("Accounts Settings", "use_sales_invoice_in_pos")
|
||||
|
||||
pos_invoices = (
|
||||
get_pos_invoices(
|
||||
closing_entry.period_start_date,
|
||||
closing_entry.period_end_date,
|
||||
closing_entry.pos_profile,
|
||||
closing_entry.user,
|
||||
)
|
||||
if is_pos_using_sales_invoice == 0
|
||||
else []
|
||||
)
|
||||
|
||||
sales_invoices = get_sales_invoices(
|
||||
closing_entry.period_start_date,
|
||||
closing_entry.period_end_date,
|
||||
closing_entry.pos_profile,
|
||||
@@ -193,6 +322,7 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
)
|
||||
|
||||
pos_transactions = []
|
||||
sales_invoice_transactions = []
|
||||
taxes = []
|
||||
payments = []
|
||||
for detail in opening_entry.balance_details:
|
||||
@@ -206,7 +336,7 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
)
|
||||
)
|
||||
|
||||
for d in invoices:
|
||||
for d in pos_invoices:
|
||||
pos_transactions.append(
|
||||
frappe._dict(
|
||||
{
|
||||
@@ -217,6 +347,20 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for d in sales_invoices:
|
||||
sales_invoice_transactions.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"sales_invoice": d.name,
|
||||
"posting_date": d.posting_date,
|
||||
"grand_total": d.grand_total,
|
||||
"customer": d.customer,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
for d in [*pos_invoices, *sales_invoices]:
|
||||
closing_entry.grand_total += flt(d.grand_total)
|
||||
closing_entry.net_total += flt(d.net_total)
|
||||
closing_entry.total_quantity += flt(d.total_qty)
|
||||
@@ -246,6 +390,7 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
)
|
||||
|
||||
closing_entry.set("pos_transactions", pos_transactions)
|
||||
closing_entry.set("sales_invoice_transactions", sales_invoice_transactions)
|
||||
closing_entry.set("payment_reconciliation", payments)
|
||||
closing_entry.set("taxes", taxes)
|
||||
|
||||
|
||||
@@ -289,6 +289,46 @@ class TestPOSClosingEntry(IntegrationTestCase):
|
||||
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||
self.assertEqual(batch_qty_with_pos, 10.0)
|
||||
|
||||
def test_closing_entries_with_sales_invoice(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
# Deleting all opening entry
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
|
||||
with self.change_settings("Accounts Settings", {"use_sales_invoice_in_pos": 1}):
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_si = create_sales_invoice(qty=10, do_not_save=1)
|
||||
pos_si.is_pos = 1
|
||||
pos_si.pos_profile = pos_profile.name
|
||||
pos_si.is_created_using_pos = 1
|
||||
pos_si.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
pos_si.save()
|
||||
pos_si.submit()
|
||||
|
||||
pos_si2 = create_sales_invoice(qty=5, do_not_save=1)
|
||||
pos_si2.is_pos = 1
|
||||
pos_si2.pos_profile = pos_profile.name
|
||||
pos_si2.is_created_using_pos = 1
|
||||
pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
pos_si2.save()
|
||||
pos_si2.submit()
|
||||
|
||||
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||
payment = pcv_doc.payment_reconciliation[0]
|
||||
|
||||
self.assertEqual(payment.mode_of_payment, "Cash")
|
||||
|
||||
for d in pcv_doc.payment_reconciliation:
|
||||
if d.mode_of_payment == "Cash":
|
||||
d.closing_amount = 1500
|
||||
|
||||
pcv_doc.submit()
|
||||
|
||||
self.assertEqual(pcv_doc.total_quantity, 15)
|
||||
self.assertEqual(pcv_doc.net_total, 1500)
|
||||
|
||||
|
||||
def init_user_and_profile(**args):
|
||||
user = "test@example.com"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
@@ -17,13 +18,10 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
class PartialPaymentValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -197,6 +195,7 @@ class POSInvoice(SalesInvoice):
|
||||
# run on validate method of selling controller
|
||||
super(SalesInvoice, self).validate()
|
||||
self.validate_pos_opening_entry()
|
||||
self.validate_is_pos_using_sales_invoice()
|
||||
self.validate_auto_set_posting_time()
|
||||
self.validate_mode_of_payment()
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
@@ -244,6 +243,9 @@ class POSInvoice(SalesInvoice):
|
||||
update_coupon_code_count(self.coupon_code, "used")
|
||||
self.clear_unallocated_mode_of_payments()
|
||||
|
||||
if self.is_return and self.is_pos_using_sales_invoice:
|
||||
self.create_and_add_consolidated_sales_invoice()
|
||||
|
||||
def before_cancel(self):
|
||||
if (
|
||||
self.consolidated_invoice
|
||||
@@ -287,6 +289,47 @@ class POSInvoice(SalesInvoice):
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
frappe.qb.from_(sip).delete().where(sip.parent == self.name).where(sip.amount == 0).run()
|
||||
|
||||
def create_and_add_consolidated_sales_invoice(self):
|
||||
sales_inv = self.create_return_sales_invoice()
|
||||
self.db_set("consolidated_invoice", sales_inv.name)
|
||||
self.set_status(update=True)
|
||||
|
||||
def create_return_sales_invoice(self):
|
||||
return_sales_invoice = frappe.new_doc("Sales Invoice")
|
||||
return_sales_invoice.is_pos = 1
|
||||
return_sales_invoice.is_return = 1
|
||||
map_doc(self, return_sales_invoice, table_map={"doctype": return_sales_invoice.doctype})
|
||||
return_sales_invoice.is_created_using_pos = 1
|
||||
return_sales_invoice.is_consolidated = 1
|
||||
return_sales_invoice.return_against = frappe.db.get_value(
|
||||
"POS Invoice", self.return_against, "consolidated_invoice"
|
||||
)
|
||||
items, taxes, payments = [], [], []
|
||||
for d in self.items:
|
||||
si_item = map_child_doc(d, return_sales_invoice, {"doctype": "Sales Invoice Item"})
|
||||
si_item.pos_invoice = self.name
|
||||
si_item.pos_invoice_item = d.name
|
||||
si_item.sales_invoice_item = get_sales_invoice_item_from_consolidated_invoice(
|
||||
self.return_against, d.pos_invoice_item
|
||||
)
|
||||
items.append(si_item)
|
||||
|
||||
for d in self.get("taxes"):
|
||||
tax = map_child_doc(d, return_sales_invoice, {"doctype": "Sales Taxes and Charges"})
|
||||
taxes.append(tax)
|
||||
|
||||
for d in self.get("payments"):
|
||||
payment = map_child_doc(d, return_sales_invoice, {"doctype": "Sales Invoice Payment"})
|
||||
payments.append(payment)
|
||||
|
||||
return_sales_invoice.set("items", items)
|
||||
return_sales_invoice.set("taxes", taxes)
|
||||
return_sales_invoice.set("payments", payments)
|
||||
return_sales_invoice.save()
|
||||
return_sales_invoice.submit()
|
||||
|
||||
return return_sales_invoice
|
||||
|
||||
def delink_serial_and_batch_bundle(self):
|
||||
for row in self.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
@@ -378,6 +421,13 @@ class POSInvoice(SalesInvoice):
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
def validate_is_pos_using_sales_invoice(self):
|
||||
self.is_pos_using_sales_invoice = frappe.db.get_single_value(
|
||||
"Accounts Settings", "use_sales_invoice_in_pos"
|
||||
)
|
||||
if self.is_pos_using_sales_invoice and not self.is_return:
|
||||
frappe.throw(_("Sales Invoice mode is activated in POS. Please create Sales Invoice instead."))
|
||||
|
||||
def validate_serialised_or_batched_item(self):
|
||||
error_msg = []
|
||||
for d in self.get("items"):
|
||||
@@ -502,20 +552,6 @@ class POSInvoice(SalesInvoice):
|
||||
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
def validate_full_payment(self):
|
||||
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if self.is_return and self.paid_amount != invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
if self.paid_amount < invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Invoice is not allowed."), exc=PartialPaymentValidationError
|
||||
)
|
||||
|
||||
def set_status(self, update=False, status=None, update_modified=True):
|
||||
if self.is_new():
|
||||
if self.get("amended_from"):
|
||||
|
||||
@@ -7,8 +7,9 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import PartialPaymentValidationError, make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import PartialPaymentValidationError
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
@@ -16,6 +16,7 @@ from frappe.utils.scheduler import is_scheduler_inactive
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
|
||||
from erpnext.controllers.taxes_and_totals import ItemWiseTaxDetail
|
||||
|
||||
|
||||
@@ -238,7 +239,7 @@ class POSInvoiceMergeLog(Document):
|
||||
si_item.pos_invoice = doc.name
|
||||
si_item.pos_invoice_item = item.name
|
||||
if doc.is_return:
|
||||
si_item.sales_invoice_item = get_sales_invoice_item(
|
||||
si_item.sales_invoice_item = get_sales_invoice_item_from_consolidated_invoice(
|
||||
doc.return_against, item.pos_invoice_item
|
||||
)
|
||||
if item.serial_and_batch_bundle:
|
||||
@@ -303,10 +304,17 @@ class POSInvoiceMergeLog(Document):
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
accounting_dimensions_fields = [d.fieldname for d in accounting_dimensions]
|
||||
dimension_values = frappe.db.get_value(
|
||||
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions_fields, as_dict=1
|
||||
"POS Profile",
|
||||
{"name": invoice.pos_profile},
|
||||
[*accounting_dimensions_fields, "cost_center", "project"],
|
||||
as_dict=1,
|
||||
)
|
||||
for dimension in accounting_dimensions:
|
||||
dimension_value = dimension_values.get(dimension.fieldname)
|
||||
dimension_value = (
|
||||
data[0].get(dimension.fieldname)
|
||||
if data[0].get(dimension.fieldname)
|
||||
else dimension_values.get(dimension.fieldname)
|
||||
)
|
||||
|
||||
if not dimension_value and (dimension.mandatory_for_pl or dimension.mandatory_for_bs):
|
||||
frappe.throw(
|
||||
@@ -318,6 +326,14 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
invoice.set(dimension.fieldname, dimension_value)
|
||||
|
||||
invoice.set(
|
||||
"cost_center",
|
||||
data[0].get("cost_center") if data[0].get("cost_center") else dimension_values.get("cost_center"),
|
||||
)
|
||||
invoice.set(
|
||||
"project", data[0].get("project") if data[0].get("project") else dimension_values.get("project")
|
||||
)
|
||||
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
invoice.pos_profile = ""
|
||||
@@ -337,7 +353,7 @@ class POSInvoiceMergeLog(Document):
|
||||
for doc in invoice_docs:
|
||||
doc.load_from_db()
|
||||
inv = sales_invoice
|
||||
if doc.is_return:
|
||||
if doc.is_return and credit_notes:
|
||||
for key, value in credit_notes.items():
|
||||
if doc.name in value:
|
||||
inv = key
|
||||
@@ -446,9 +462,34 @@ def get_invoice_customer_map(pos_invoices):
|
||||
pos_invoice_customer_map.setdefault(customer, [])
|
||||
pos_invoice_customer_map[customer].append(invoice)
|
||||
|
||||
for customer, invoices in pos_invoice_customer_map.items():
|
||||
pos_invoice_customer_map[customer] = split_invoices_by_accounting_dimension(invoices)
|
||||
|
||||
return pos_invoice_customer_map
|
||||
|
||||
|
||||
def split_invoices_by_accounting_dimension(pos_invoices):
|
||||
# pos_invoices = {
|
||||
# {'dim_field1': 'dim_field1_value1', 'dim_field2': 'dim_field2_value1'}: [],
|
||||
# {'dim_field1': 'dim_field1_value2', 'dim_field2': 'dim_field2_value1'}: []
|
||||
# }
|
||||
pos_invoice_accounting_dimensions_map = {}
|
||||
for invoice in pos_invoices:
|
||||
dimension_fields = [d.fieldname for d in get_checks_for_pl_and_bs_accounts()]
|
||||
accounting_dimensions = frappe.db.get_value(
|
||||
"POS Invoice", invoice.pos_invoice, [*dimension_fields, "cost_center", "project"], as_dict=1
|
||||
)
|
||||
|
||||
accounting_dimensions_dic_hash = hashlib.sha256(
|
||||
json.dumps(accounting_dimensions).encode()
|
||||
).hexdigest()
|
||||
|
||||
pos_invoice_accounting_dimensions_map.setdefault(accounting_dimensions_dic_hash, [])
|
||||
pos_invoice_accounting_dimensions_map[accounting_dimensions_dic_hash].append(invoice)
|
||||
|
||||
return pos_invoice_accounting_dimensions_map
|
||||
|
||||
|
||||
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
|
||||
invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions"))
|
||||
if frappe.flags.in_test and not invoices:
|
||||
@@ -532,20 +573,21 @@ def split_invoices(invoices):
|
||||
|
||||
def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
try:
|
||||
for customer, invoices in invoice_by_customer.items():
|
||||
for _invoices in split_invoices(invoices):
|
||||
merge_log = frappe.new_doc("POS Invoice Merge Log")
|
||||
merge_log.posting_date = (
|
||||
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
|
||||
)
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
merge_log.set("pos_invoices", _invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
merge_log.submit()
|
||||
for customer, invoices_acc_dim in invoice_by_customer.items():
|
||||
for invoices in invoices_acc_dim.values():
|
||||
for _invoices in split_invoices(invoices):
|
||||
merge_log = frappe.new_doc("POS Invoice Merge Log")
|
||||
merge_log.posting_date = (
|
||||
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
|
||||
)
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
merge_log.set("pos_invoices", _invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
merge_log.submit()
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status="Submitted")
|
||||
closing_entry.db_set("error_message", "")
|
||||
@@ -633,26 +675,3 @@ def get_error_message(message) -> str:
|
||||
return message["message"]
|
||||
except Exception:
|
||||
return str(message)
|
||||
|
||||
|
||||
def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
|
||||
try:
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
SalesInvoiceItem = DocType("Sales Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoice)
|
||||
.from_(SalesInvoiceItem)
|
||||
.select(SalesInvoiceItem.name)
|
||||
.where(
|
||||
(SalesInvoice.name == SalesInvoiceItem.parent)
|
||||
& (SalesInvoice.is_return == 0)
|
||||
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
||||
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run(as_dict=True)
|
||||
return result[0].name if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -472,3 +472,58 @@ class TestPOSInvoiceMergeLog(IntegrationTestCase):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_separate_consolidated_invoice_for_different_accounting_dimensions(self):
|
||||
"""
|
||||
Creating 3 POS Invoices where first POS Invoice has different Cost Center than the other two.
|
||||
Consolidate the Invoices.
|
||||
Check whether the first POS Invoice is consolidated with a separate Sales Invoice than the other two.
|
||||
Check whether the second and third POS Invoice are consolidated with the same Sales Invoice.
|
||||
"""
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
create_cost_center(cost_center_name="_Test POS Cost Center 1", is_group=0)
|
||||
create_cost_center(cost_center_name="_Test POS Cost Center 2", is_group=0)
|
||||
|
||||
try:
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300})
|
||||
pos_inv.cost_center = "_Test POS Cost Center 1 - _TC"
|
||||
pos_inv.save()
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
|
||||
pos_inv.cost_center = "_Test POS Cost Center 2 - _TC"
|
||||
pos_inv2.save()
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(rate=2300, do_not_submit=1)
|
||||
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 2300})
|
||||
pos_inv.cost_center = "_Test POS Cost Center 2 - _TC"
|
||||
pos_inv3.save()
|
||||
pos_inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||
|
||||
pos_inv2.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv2.consolidated_invoice))
|
||||
|
||||
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
|
||||
pos_inv3.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
|
||||
self.assertTrue(pos_inv2.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
@@ -417,6 +417,7 @@
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
|
||||
@@ -144,8 +144,10 @@
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"company_shipping_address_section",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
"dispatch_address_display",
|
||||
"column_break_126",
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
"company_billing_address_section",
|
||||
"billing_address",
|
||||
@@ -1548,7 +1550,7 @@
|
||||
{
|
||||
"fieldname": "company_shipping_address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Company Shipping Address"
|
||||
"label": "Shipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_126",
|
||||
@@ -1635,13 +1637,28 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Sender",
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "dispatch_address_display",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Dispatch Address",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dispatch_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Select Dispatch Address ",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-14 11:39:04.564610",
|
||||
"modified": "2025-04-09 16:49:22.175081",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1696,6 +1713,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "posting_date, supplier, bill_no, base_grand_total, outstanding_amount",
|
||||
"sender_field": "sender",
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -117,6 +117,8 @@ class PurchaseInvoice(BuyingController):
|
||||
currency: DF.Link | None
|
||||
disable_rounded_total: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
dispatch_address: DF.Link | None
|
||||
dispatch_address_display: DF.TextEditor | None
|
||||
due_date: DF.Date | None
|
||||
from_date: DF.Date | None
|
||||
grand_total: DF.Currency
|
||||
|
||||
@@ -2705,13 +2705,13 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
To test if after applying discount on grand total,
|
||||
the grand total is calculated correctly without any rounding errors
|
||||
"""
|
||||
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice = make_purchase_invoice(qty=3, rate=100, do_not_save=True, do_not_submit=True)
|
||||
invoice.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 21.39,
|
||||
"qty": 3,
|
||||
"rate": 50.3,
|
||||
},
|
||||
)
|
||||
invoice.append(
|
||||
@@ -2720,18 +2720,19 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 15.5,
|
||||
"rate": 15,
|
||||
},
|
||||
)
|
||||
|
||||
# the grand total here will be 255.71
|
||||
# the grand total here will be 518.54
|
||||
invoice.disable_rounded_total = 1
|
||||
# apply discount on grand total to adjust the grand total to 255
|
||||
invoice.discount_amount = 0.71
|
||||
# apply discount on grand total to adjust the grand total to 518
|
||||
invoice.discount_amount = 0.54
|
||||
|
||||
invoice.save()
|
||||
|
||||
# check if grand total is 496 and not something like 254.99 due to rounding errors
|
||||
self.assertEqual(invoice.grand_total, 255)
|
||||
# check if grand total is 518 and not something like 517.99 due to rounding errors
|
||||
self.assertEqual(invoice.grand_total, 518)
|
||||
|
||||
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
|
||||
"""
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
"fieldname": "item_wise_tax_detail",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Detail ",
|
||||
"label": "Item Wise Tax Detail",
|
||||
"oldfieldname": "item_wise_tax_detail",
|
||||
"oldfieldtype": "Small Text",
|
||||
"print_hide": 1,
|
||||
@@ -267,18 +267,20 @@
|
||||
"report_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-22 19:17:02.377473",
|
||||
"modified": "2025-04-15 13:14:48.936047",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
|
||||
if (this.frm.doc.is_created_using_pos && !this.frm.doc.is_return) {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
}
|
||||
}
|
||||
|
||||
make_invoice_discounting() {
|
||||
@@ -778,24 +782,6 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
};
|
||||
});
|
||||
},
|
||||
// When multiple companies are set up. in case company name is changed set default company address
|
||||
company: function (frm) {
|
||||
if (frm.doc.company) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
|
||||
debounce: 2000,
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("company_address", r.message);
|
||||
} else {
|
||||
frm.set_value("company_address", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
frm.redemption_conversion_factor = null;
|
||||
},
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"update_billed_amount_in_delivery_note",
|
||||
"is_debit_note",
|
||||
"amended_from",
|
||||
"is_created_using_pos",
|
||||
"pos_closing_entry",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -2199,6 +2201,23 @@
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_created_using_pos",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is created using POS",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_created_using_pos",
|
||||
"fieldname": "pos_closing_entry",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "POS Closing Entry",
|
||||
"options": "POS Closing Entry",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -51,6 +51,10 @@ from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amou
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
|
||||
class PartialPaymentValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class SalesInvoice(SellingController):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -133,6 +137,7 @@ class SalesInvoice(SellingController):
|
||||
inter_company_invoice_reference: DF.Link | None
|
||||
is_cash_or_non_trade_discount: DF.Check
|
||||
is_consolidated: DF.Check
|
||||
is_created_using_pos: DF.Check
|
||||
is_debit_note: DF.Check
|
||||
is_discounted: DF.Check
|
||||
is_internal_customer: DF.Check
|
||||
@@ -162,6 +167,7 @@ class SalesInvoice(SellingController):
|
||||
plc_conversion_rate: DF.Float
|
||||
po_date: DF.Date | None
|
||||
po_no: DF.Data | None
|
||||
pos_closing_entry: DF.Link | None
|
||||
pos_profile: DF.Link | None
|
||||
posting_date: DF.Date
|
||||
posting_time: DF.Time | None
|
||||
@@ -306,6 +312,10 @@ class SalesInvoice(SellingController):
|
||||
if cint(self.is_pos):
|
||||
self.validate_pos()
|
||||
|
||||
if cint(self.is_created_using_pos):
|
||||
self.validate_created_using_pos()
|
||||
self.validate_full_payment()
|
||||
|
||||
self.validate_dropship_item()
|
||||
|
||||
if cint(self.update_stock):
|
||||
@@ -528,7 +538,22 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
frappe.throw(msg, title=_("Not Allowed"))
|
||||
|
||||
def check_if_created_using_pos_and_pos_closing_entry_generated(self):
|
||||
if self.doctype == "Sales Invoice" and self.is_created_using_pos and self.pos_closing_entry:
|
||||
pos_closing_entry_docstatus = frappe.db.get_value(
|
||||
"POS Closing Entry", self.pos_closing_entry, "docstatus"
|
||||
)
|
||||
if pos_closing_entry_docstatus == 1:
|
||||
frappe.throw(
|
||||
msg=_("To cancel this Sales Invoice you need to cancel the POS Closing Entry {}.").format(
|
||||
get_link_to_form("POS Closing Entry", self.pos_closing_entry)
|
||||
),
|
||||
title=_("Not Allowed"),
|
||||
)
|
||||
|
||||
def before_cancel(self):
|
||||
# check if generated via POS and already included in POS Closing Entry
|
||||
self.check_if_created_using_pos_and_pos_closing_entry_generated()
|
||||
self.check_if_consolidated_invoice()
|
||||
|
||||
super().before_cancel()
|
||||
@@ -598,6 +623,15 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
if (
|
||||
self.doctype == "Sales Invoice"
|
||||
and self.is_pos
|
||||
and self.is_return
|
||||
and self.is_created_using_pos
|
||||
and not self.pos_closing_entry
|
||||
):
|
||||
self.cancel_pos_invoice_credit_note_generated_during_sales_invoice_mode()
|
||||
|
||||
def update_status_updater_args(self):
|
||||
if not cint(self.update_stock):
|
||||
return
|
||||
@@ -669,6 +703,15 @@ class SalesInvoice(SellingController):
|
||||
timesheet.flags.ignore_validate_update_after_submit = True
|
||||
timesheet.db_update_all()
|
||||
|
||||
def cancel_pos_invoice_credit_note_generated_during_sales_invoice_mode(self):
|
||||
pos_invoices = frappe.get_all(
|
||||
"POS Invoice", filters={"consolidated_invoice": self.name}, pluck="name"
|
||||
)
|
||||
if pos_invoices:
|
||||
for pos_invoice in pos_invoices:
|
||||
pos_invoice_doc = frappe.get_doc("POS Invoice", pos_invoice)
|
||||
pos_invoice_doc.cancel()
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_missing_values(self, for_validate=False):
|
||||
pos = self.set_pos_fields(for_validate)
|
||||
@@ -704,6 +747,13 @@ class SalesInvoice(SellingController):
|
||||
"allow_print_before_pay": pos.get("allow_print_before_pay"),
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_mode_of_payments(self):
|
||||
if self.pos_profile:
|
||||
pos_profile = frappe.get_cached_doc("POS Profile", self.pos_profile)
|
||||
update_multi_mode_option(self, pos_profile)
|
||||
self.paid_amount = 0
|
||||
|
||||
def update_time_sheet(self, sales_invoice):
|
||||
for d in self.timesheets:
|
||||
if d.time_sheet:
|
||||
@@ -1025,6 +1075,32 @@ class SalesInvoice(SellingController):
|
||||
) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
|
||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
||||
|
||||
def validate_created_using_pos(self):
|
||||
if self.is_created_using_pos and not self.pos_profile:
|
||||
frappe.throw(_("POS Profile is mandatory to mark this invoice as POS Transaction."))
|
||||
|
||||
self.is_pos_using_sales_invoice = frappe.db.get_single_value(
|
||||
"Accounts Settings", "use_sales_invoice_in_pos"
|
||||
)
|
||||
if not self.is_pos_using_sales_invoice and not self.is_return:
|
||||
frappe.throw(_("Transactions using Sales Invoice in POS are disabled."))
|
||||
|
||||
def validate_full_payment(self):
|
||||
invoice_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||
|
||||
if self.docstatus == 1:
|
||||
if self.is_return and self.paid_amount != invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
|
||||
if self.paid_amount < invoice_total:
|
||||
frappe.throw(
|
||||
msg=_("Partial Payment in POS Transactions are not allowed."),
|
||||
exc=PartialPaymentValidationError,
|
||||
)
|
||||
|
||||
def validate_warehouse(self):
|
||||
super().validate_warehouse()
|
||||
|
||||
@@ -2295,7 +2371,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
# Invert Addresses
|
||||
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address
|
||||
target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name
|
||||
)
|
||||
update_address(
|
||||
target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name
|
||||
)
|
||||
update_address(
|
||||
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
|
||||
@@ -2717,9 +2796,11 @@ def create_dunning(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.closing_text = letter_text.get("closing_text")
|
||||
target.language = letter_text.get("language")
|
||||
|
||||
# update outstanding
|
||||
# update outstanding from doc
|
||||
if source.payment_schedule and len(source.payment_schedule) == 1:
|
||||
target.overdue_payments[0].outstanding = source.get("outstanding_amount")
|
||||
for row in target.overdue_payments:
|
||||
if row.payment_schedule == source.payment_schedule[0].name:
|
||||
row.outstanding = source.get("outstanding_amount")
|
||||
|
||||
target.validate()
|
||||
|
||||
|
||||
@@ -4386,6 +4386,27 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
|
||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
||||
|
||||
def test_pos_sales_invoice_creation_during_pos_invoice_mode(self):
|
||||
# Deleting all opening entry
|
||||
frappe.db.sql("delete from `tabPOS Opening Entry`")
|
||||
|
||||
with self.change_settings("Accounts Settings", {"use_sales_invoice_in_pos": 0}):
|
||||
pos_profile = make_pos_profile()
|
||||
|
||||
pos_profile.payments = []
|
||||
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
|
||||
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(qty=10, do_not_save=True)
|
||||
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
pos.is_created_using_pos = 1
|
||||
|
||||
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
|
||||
self.assertRaises(frappe.ValidationError, pos.insert)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-03-19 15:01:28.834774",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sales_invoice",
|
||||
"posting_date",
|
||||
"column_break_fear",
|
||||
"customer",
|
||||
"grand_total",
|
||||
"is_return",
|
||||
"return_against"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "sales_invoice",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Sales Invoice",
|
||||
"options": "Sales Invoice",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_fear",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.customer",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "sales_invoice.is_return",
|
||||
"fieldname": "is_return",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Return",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.return_against",
|
||||
"fieldname": "return_against",
|
||||
"fieldtype": "Link",
|
||||
"label": "Return Against",
|
||||
"options": "Sales Invoice",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_invoice.grand_total",
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-20 01:14:57.890299",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Reference",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class SalesInvoiceReference(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
customer: DF.Link
|
||||
grand_total: DF.Currency
|
||||
is_return: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
posting_date: DF.Date
|
||||
return_against: DF.Link | None
|
||||
sales_invoice: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -71,6 +71,7 @@ def get_party_details(
|
||||
party_address=None,
|
||||
company_address=None,
|
||||
shipping_address=None,
|
||||
dispatch_address=None,
|
||||
pos_profile=None,
|
||||
):
|
||||
if not party:
|
||||
@@ -92,6 +93,7 @@ def get_party_details(
|
||||
party_address,
|
||||
company_address,
|
||||
shipping_address,
|
||||
dispatch_address,
|
||||
pos_profile,
|
||||
)
|
||||
|
||||
@@ -111,6 +113,7 @@ def _get_party_details(
|
||||
party_address=None,
|
||||
company_address=None,
|
||||
shipping_address=None,
|
||||
dispatch_address=None,
|
||||
pos_profile=None,
|
||||
):
|
||||
party_details = frappe._dict(
|
||||
@@ -134,6 +137,7 @@ def _get_party_details(
|
||||
party_address,
|
||||
company_address,
|
||||
shipping_address,
|
||||
dispatch_address,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
@@ -191,34 +195,51 @@ def set_address_details(
|
||||
party_address=None,
|
||||
company_address=None,
|
||||
shipping_address=None,
|
||||
dispatch_address=None,
|
||||
*,
|
||||
ignore_permissions=False,
|
||||
):
|
||||
billing_address_field = (
|
||||
# party_billing
|
||||
party_billing_field = (
|
||||
"customer_address" if party_type in ["Lead", "Prospect"] else party_type.lower() + "_address"
|
||||
)
|
||||
party_details[billing_address_field] = party_address or get_default_address(party_type, party.name)
|
||||
|
||||
party_details[party_billing_field] = party_address or get_default_address(party_type, party.name)
|
||||
if doctype:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
|
||||
get_fetch_values(doctype, party_billing_field, party_details[party_billing_field])
|
||||
)
|
||||
# address display
|
||||
party_details.address_display = render_address(
|
||||
party_details[billing_address_field], check_permissions=not ignore_permissions
|
||||
)
|
||||
# shipping address
|
||||
if party_type in ["Customer", "Lead"]:
|
||||
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
|
||||
party_type, party.name
|
||||
)
|
||||
party_details.shipping_address = render_address(
|
||||
party_details["shipping_address_name"], check_permissions=not ignore_permissions
|
||||
)
|
||||
if doctype:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
|
||||
)
|
||||
|
||||
party_details.address_display = render_address(
|
||||
party_details[party_billing_field], check_permissions=not ignore_permissions
|
||||
)
|
||||
|
||||
# party_shipping
|
||||
if party_type in ["Customer", "Lead"]:
|
||||
party_shipping_field = "shipping_address_name"
|
||||
party_shipping_display = "shipping_address"
|
||||
default_shipping = shipping_address
|
||||
|
||||
else:
|
||||
# Supplier
|
||||
party_shipping_field = "dispatch_address"
|
||||
party_shipping_display = "dispatch_address_display"
|
||||
default_shipping = dispatch_address
|
||||
|
||||
party_details[party_shipping_field] = default_shipping or get_party_shipping_address(
|
||||
party_type, party.name
|
||||
)
|
||||
|
||||
party_details[party_shipping_display] = render_address(
|
||||
party_details[party_shipping_field], check_permissions=not ignore_permissions
|
||||
)
|
||||
|
||||
if doctype:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, party_shipping_field, party_details[party_shipping_field])
|
||||
)
|
||||
|
||||
# company_address
|
||||
if company_address:
|
||||
party_details.company_address = company_address
|
||||
else:
|
||||
@@ -256,22 +277,20 @@ def set_address_details(
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address),
|
||||
)
|
||||
|
||||
party_address, shipping_address = (
|
||||
party_details.get(billing_address_field),
|
||||
party_details.shipping_address_name,
|
||||
party_billing, party_shipping = (
|
||||
party_details.get(party_billing_field),
|
||||
party_details.get(party_shipping_field),
|
||||
)
|
||||
|
||||
party_details["tax_category"] = get_address_tax_category(
|
||||
party.get("tax_category"),
|
||||
party_address,
|
||||
shipping_address if party_type != "Supplier" else party_address,
|
||||
party.get("tax_category"), party_billing, party_shipping
|
||||
)
|
||||
|
||||
if doctype in TRANSACTION_TYPES:
|
||||
with temporary_flag("company", company):
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_address, shipping_address
|
||||
return party_billing, party_shipping
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
|
||||
@@ -100,7 +100,7 @@ class PartyLedgerSummaryReport:
|
||||
conditions.append(doctype.territory.isin(self.filters.territory))
|
||||
|
||||
if self.filters.get(group_field):
|
||||
conditions.append(doctype.get(group_field).isin(self.filters.get(group_field)))
|
||||
conditions.append(doctype[group_field].isin(self.filters.get(group_field)))
|
||||
|
||||
if self.filters.payment_terms_template:
|
||||
conditions.append(doctype.payment_terms == self.filters.payment_terms_template)
|
||||
|
||||
@@ -519,9 +519,6 @@ def get_accounting_entries(
|
||||
.where(gl_entry.company == filters.company)
|
||||
)
|
||||
|
||||
if group_by_account:
|
||||
query = query.groupby(gl_entry.account)
|
||||
|
||||
ignore_is_opening = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_is_opening_check_for_reporting"
|
||||
)
|
||||
@@ -551,6 +548,9 @@ def get_accounting_entries(
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
|
||||
if group_by_account:
|
||||
query += " GROUP BY `account`"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
|
||||
|
||||
|
||||
@@ -59,3 +59,33 @@ class TestSupplierLedgerSummary(AccountsTestMixin, IntegrationTestCase):
|
||||
for field in expected:
|
||||
with self.subTest(field=field):
|
||||
self.assertEqual(report_output[0].get(field), expected.get(field))
|
||||
|
||||
def test_supplier_ledger_summary_with_filters(self):
|
||||
self.create_purchase_invoice()
|
||||
|
||||
supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group")
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"supplier_group": supplier_group,
|
||||
}
|
||||
|
||||
expected = {
|
||||
"party": "_Test Supplier",
|
||||
"party_name": "_Test Supplier",
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 300.0,
|
||||
"paid_amount": 0,
|
||||
"return_amount": 0,
|
||||
"closing_balance": 300.0,
|
||||
"currency": "INR",
|
||||
"supplier_name": "_Test Supplier",
|
||||
}
|
||||
|
||||
report_output = execute(filters)[1]
|
||||
self.assertEqual(len(report_output), 1)
|
||||
for field in expected:
|
||||
with self.subTest(field=field):
|
||||
self.assertEqual(report_output[0].get(field), expected.get(field))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import getdate
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -33,6 +34,7 @@ def execute(filters=None):
|
||||
|
||||
def validate_filters(filters):
|
||||
"""Validate if dates are properly set"""
|
||||
filters = frappe._dict(filters or {})
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
@@ -68,7 +70,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
@@ -439,12 +441,22 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
def get_tax_rate_map(filters):
|
||||
rate_map = frappe.get_all(
|
||||
"Tax Withholding Rate",
|
||||
filters={
|
||||
"from_date": ("<=", filters.get("from_date")),
|
||||
"to_date": (">=", filters.get("to_date")),
|
||||
},
|
||||
fields=["parent", "tax_withholding_rate"],
|
||||
as_list=1,
|
||||
filters={"from_date": ("<=", filters.to_date), "to_date": (">=", filters.from_date)},
|
||||
fields=["parent", "tax_withholding_rate", "from_date", "to_date"],
|
||||
)
|
||||
|
||||
return frappe._dict(rate_map)
|
||||
rate_list = frappe._dict()
|
||||
|
||||
for rate in rate_map:
|
||||
rate_list.setdefault(rate.parent, []).append(frappe._dict(rate))
|
||||
|
||||
return rate_list
|
||||
|
||||
|
||||
def get_tax_withholding_rates(tax_withholding, posting_date):
|
||||
# returns the row that matches with the fiscal year from posting date
|
||||
for rate in tax_withholding:
|
||||
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
|
||||
return rate.tax_withholding_rate
|
||||
|
||||
return 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import today
|
||||
from frappe.utils import add_to_date, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -60,6 +60,56 @@ class TestTaxWithholdingDetails(AccountsTestMixin, IntegrationTestCase):
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def test_date_filters_in_multiple_tax_withholding_rules(self):
|
||||
create_tax_category("TDS - 3", rate=10, account="TDS - _TC", cumulative_threshold=1)
|
||||
# insert new rate in same fiscal year
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": add_to_date(mid_year, days=1),
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
},
|
||||
)
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = "TDS - 3"
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(
|
||||
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
|
||||
)
|
||||
inv_2.set_posting_time = 1
|
||||
|
||||
inv_1.apply_tds = 1
|
||||
inv_2.tax_withholding_category = "TDS - 3"
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
result = execute(
|
||||
frappe._dict(
|
||||
company="_Test Company",
|
||||
party_type="Supplier",
|
||||
from_date=fiscal_year[1],
|
||||
to_date=fiscal_year[2],
|
||||
)
|
||||
)[1]
|
||||
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
def check_expected_values(self, result, expected_values):
|
||||
for i in range(len(result)):
|
||||
voucher = frappe._dict(result[i])
|
||||
|
||||
@@ -661,10 +661,6 @@ frappe.ui.form.on("Asset", {
|
||||
} else {
|
||||
frm.set_value("purchase_invoice_item", data.purchase_invoice_item);
|
||||
}
|
||||
|
||||
let is_editable = !data.is_multiple_items; // if multiple items, then fields should be read-only
|
||||
frm.set_df_property("gross_purchase_amount", "read_only", is_editable);
|
||||
frm.set_df_property("asset_quantity", "read_only", is_editable);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"options": "ACC-ASS-.YYYY.-"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "item_code",
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fetch_if_empty": 1,
|
||||
@@ -477,6 +478,7 @@
|
||||
"fieldname": "total_asset_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Asset Cost",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -485,6 +487,7 @@
|
||||
"fieldname": "additional_asset_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional Asset Cost",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -588,7 +591,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-02-20 14:09:05.421913",
|
||||
"modified": "2025-04-24 15:31:47.373274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -626,10 +629,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,15 @@ from erpnext.controllers.accounts_controller import AccountsController
|
||||
|
||||
class Asset(AccountsController):
|
||||
# begin: auto-generated types
|
||||
# ruff: noqa
|
||||
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
|
||||
from frappe.types import DF
|
||||
|
||||
additional_asset_cost: DF.Currency
|
||||
amended_from: DF.Link | None
|
||||
@@ -117,6 +118,7 @@ class Asset(AccountsController):
|
||||
total_asset_cost: DF.Currency
|
||||
total_number_of_depreciations: DF.Int
|
||||
value_after_depreciation: DF.Currency
|
||||
# ruff: noqa
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -1168,7 +1170,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
frappe.throw(_(f"Selected {doctype} does not contain the Item Code {item_code}"))
|
||||
|
||||
first_item = matching_items[0]
|
||||
is_multiple_items = len(matching_items) > 1
|
||||
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
@@ -1177,7 +1178,6 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
"asset_location": first_item.get("asset_location"),
|
||||
"is_multiple_items": is_multiple_items,
|
||||
"purchase_receipt_item": first_item.name if doctype == "Purchase Receipt" else None,
|
||||
"purchase_invoice_item": first_item.name if doctype == "Purchase Invoice" else None,
|
||||
}
|
||||
|
||||
@@ -135,9 +135,11 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.increase_asset_value()
|
||||
|
||||
total_repair_cost = self.get_total_value_of_stock_consumed()
|
||||
if self.capitalize_repair_cost:
|
||||
self.asset_doc.total_asset_cost += self.repair_cost
|
||||
self.asset_doc.additional_asset_cost += self.repair_cost
|
||||
total_repair_cost += self.repair_cost
|
||||
self.asset_doc.total_asset_cost += total_repair_cost
|
||||
self.asset_doc.additional_asset_cost += total_repair_cost
|
||||
|
||||
if self.get("stock_consumption"):
|
||||
self.check_for_stock_items_and_warehouse()
|
||||
@@ -176,9 +178,11 @@ class AssetRepair(AccountsController):
|
||||
|
||||
self.decrease_asset_value()
|
||||
|
||||
total_repair_cost = self.get_total_value_of_stock_consumed()
|
||||
if self.capitalize_repair_cost:
|
||||
self.asset_doc.total_asset_cost -= self.repair_cost
|
||||
self.asset_doc.additional_asset_cost -= self.repair_cost
|
||||
total_repair_cost += self.repair_cost
|
||||
self.asset_doc.total_asset_cost -= total_repair_cost
|
||||
self.asset_doc.additional_asset_cost -= total_repair_cost
|
||||
|
||||
if self.get("capitalize_repair_cost"):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
|
||||
@@ -110,8 +110,10 @@
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"shipping_address_section",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
"dispatch_address_display",
|
||||
"column_break_99",
|
||||
"shipping_address",
|
||||
"shipping_address_display",
|
||||
"company_billing_address_section",
|
||||
"billing_address",
|
||||
@@ -1291,6 +1293,20 @@
|
||||
"hidden": 1,
|
||||
"label": "Has Unit Price Items",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dispatch_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Dispatch Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dispatch_address_display",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Dispatch Address Details",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1298,7 +1314,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-03 16:48:08.697520",
|
||||
"modified": "2025-04-09 16:54:08.836106",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -92,6 +92,8 @@ class PurchaseOrder(BuyingController):
|
||||
customer_name: DF.Data | None
|
||||
disable_rounded_total: DF.Check
|
||||
discount_amount: DF.Currency
|
||||
dispatch_address: DF.Link | None
|
||||
dispatch_address_display: DF.TextEditor | None
|
||||
from_date: DF.Date | None
|
||||
grand_total: DF.Currency
|
||||
group_same_items: DF.Check
|
||||
|
||||
@@ -158,6 +158,14 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
);
|
||||
|
||||
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Supplier Quotation Comparison"),
|
||||
function () {
|
||||
frm.trigger("show_supplier_quotation_comparison");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
@@ -165,6 +173,20 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
}
|
||||
},
|
||||
|
||||
show_supplier_quotation_comparison(frm) {
|
||||
const today = new Date();
|
||||
const oneMonthAgo = new Date(today);
|
||||
oneMonthAgo.setMonth(today.getMonth() - 1);
|
||||
|
||||
frappe.route_options = {
|
||||
company: frm.doc.company,
|
||||
from_date: moment(oneMonthAgo).format("YYYY-MM-DD"),
|
||||
to_date: moment(today).format("YYYY-MM-DD"),
|
||||
request_for_quotation: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("query-report", "Supplier Quotation Comparison");
|
||||
},
|
||||
|
||||
make_supplier_quotation: function (frm) {
|
||||
var doc = frm.doc;
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
|
||||
@@ -2415,13 +2415,12 @@ class AccountsController(TransactionBase):
|
||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
d.base_outstanding = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_outstanding")
|
||||
)
|
||||
d.base_outstanding = d.base_payment_amount
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
d.base_outstanding = d.base_payment_amount
|
||||
else:
|
||||
self.fetch_payment_terms_from_order(
|
||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||
|
||||
@@ -141,6 +141,7 @@ class BuyingController(SubcontractingController):
|
||||
company=self.company,
|
||||
party_address=self.get("supplier_address"),
|
||||
shipping_address=self.get("shipping_address"),
|
||||
dispatch_address=self.get("dispatch_address"),
|
||||
company_address=self.get("billing_address"),
|
||||
fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"),
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
@@ -242,6 +243,7 @@ class BuyingController(SubcontractingController):
|
||||
address_dict = {
|
||||
"supplier_address": "address_display",
|
||||
"shipping_address": "shipping_address_display",
|
||||
"dispatch_address": "dispatch_address_display",
|
||||
"billing_address": "billing_address_display",
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ from collections import OrderedDict, defaultdict
|
||||
import frappe
|
||||
from frappe import qb, scrub
|
||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||
from frappe.permissions import has_permission
|
||||
from frappe.query_builder import Criterion, CustomFunction
|
||||
from frappe.query_builder.functions import Concat, Locate, Sum
|
||||
from frappe.utils import nowdate, today, unique
|
||||
from frappe.utils import cint, nowdate, today, unique
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
@@ -20,10 +21,28 @@ from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_templat
|
||||
# searches for active employees
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
def employee_query(
|
||||
doctype,
|
||||
txt,
|
||||
searchfield,
|
||||
start,
|
||||
page_len,
|
||||
filters,
|
||||
reference_doctype: str | None = None,
|
||||
ignore_user_permissions: bool = False,
|
||||
):
|
||||
doctype = "Employee"
|
||||
conditions = []
|
||||
fields = get_fields(doctype, ["name", "employee_name"])
|
||||
ignore_permissions = False
|
||||
|
||||
if reference_doctype and ignore_user_permissions:
|
||||
ignore_permissions = has_ignored_field(reference_doctype, doctype) and has_permission(
|
||||
doctype,
|
||||
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
|
||||
)
|
||||
|
||||
mcond = "" if ignore_permissions else get_match_cond(doctype)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabEmployee`
|
||||
@@ -42,13 +61,32 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
"fields": ", ".join(fields),
|
||||
"key": searchfield,
|
||||
"fcond": get_filters_cond(doctype, filters, conditions),
|
||||
"mcond": get_match_cond(doctype),
|
||||
"mcond": mcond,
|
||||
}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
)
|
||||
|
||||
|
||||
def has_ignored_field(reference_doctype, doctype):
|
||||
meta = frappe.get_meta(reference_doctype)
|
||||
for field in meta.fields:
|
||||
if not field.ignore_user_permissions:
|
||||
continue
|
||||
if field.fieldtype == "Link" and field.options == doctype:
|
||||
return True
|
||||
elif field.fieldtype == "Dynamic Link":
|
||||
options = meta.get_link_doctype(field.fieldname)
|
||||
if not options:
|
||||
continue
|
||||
if isinstance(options, str):
|
||||
options = options.split("\n")
|
||||
if doctype in options or "DocType" in options:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# searches for leads which are not converted
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -928,7 +966,7 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
return frappe.get_all(
|
||||
"UOM",
|
||||
filters={"name": ["like", f"%{txt}%"]},
|
||||
filters={"name": ["like", f"%{txt}%"], "enabled": 1},
|
||||
fields=["name"],
|
||||
limit_start=start,
|
||||
limit_page_length=page_len,
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||
|
||||
import erpnext
|
||||
@@ -347,6 +348,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
"Company", company, "default_warehouse_for_sales_return"
|
||||
)
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
inv_is_consolidated, inv_is_pos = frappe.db.get_value(
|
||||
"Sales Invoice", source_name, ["is_consolidated", "is_pos"]
|
||||
)
|
||||
if inv_is_consolidated and inv_is_pos:
|
||||
frappe.throw(
|
||||
_("Cannot create return for consolidated invoice {0}.").format(source_name),
|
||||
title=_("Cannot Create Return"),
|
||||
)
|
||||
|
||||
def set_missing_values(source, target):
|
||||
doc = frappe.get_doc(target)
|
||||
doc.is_return = 1
|
||||
@@ -377,6 +388,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
if doc.get("is_return"):
|
||||
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
|
||||
doc.consolidated_invoice = ""
|
||||
if doc.doctype == "Sales Invoice":
|
||||
doc.pos_closing_entry = ""
|
||||
# no copy enabled for party_account_currency
|
||||
doc.party_account_currency = source.party_account_currency
|
||||
doc.set("payments", [])
|
||||
@@ -1169,26 +1182,49 @@ def get_payment_data(invoice):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_invoice_item_returned_qty(pos_invoice, customer, item_row_name):
|
||||
is_return, docstatus = frappe.db.get_value("POS Invoice", pos_invoice, ["is_return", "docstatus"])
|
||||
def get_invoice_item_returned_qty(doctype, invoice, customer, item_row_name):
|
||||
is_return, docstatus = frappe.db.get_value(doctype, invoice, ["is_return", "docstatus"])
|
||||
if not is_return and docstatus == 1:
|
||||
return get_returned_qty_map_for_row(pos_invoice, customer, item_row_name, "POS Invoice")
|
||||
return get_returned_qty_map_for_row(invoice, customer, item_row_name, doctype)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_pos_invoice_returnable(pos_invoice):
|
||||
def is_invoice_returnable(doctype, invoice):
|
||||
is_return, docstatus, customer = frappe.db.get_value(
|
||||
"POS Invoice", pos_invoice, ["is_return", "docstatus", "customer"]
|
||||
doctype, invoice, ["is_return", "docstatus", "customer"]
|
||||
)
|
||||
if is_return or docstatus == 0:
|
||||
return False
|
||||
|
||||
invoice_item_qty = frappe.db.get_all("POS Invoice Item", {"parent": pos_invoice}, ["name", "qty"])
|
||||
invoice_item_qty = frappe.db.get_all(f"{doctype} Item", {"parent": invoice}, ["name", "qty"])
|
||||
|
||||
already_full_returned = 0
|
||||
for d in invoice_item_qty:
|
||||
returned_qty = get_returned_qty_map_for_row(pos_invoice, customer, d.name, "POS Invoice")
|
||||
returned_qty = get_returned_qty_map_for_row(invoice, customer, d.name, doctype)
|
||||
if returned_qty.qty == d.qty:
|
||||
already_full_returned += 1
|
||||
|
||||
return len(invoice_item_qty) != already_full_returned
|
||||
|
||||
|
||||
def get_sales_invoice_item_from_consolidated_invoice(return_against_pos_invoice, pos_invoice_item):
|
||||
try:
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
SalesInvoiceItem = DocType("Sales Invoice Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(SalesInvoice)
|
||||
.from_(SalesInvoiceItem)
|
||||
.select(SalesInvoiceItem.name)
|
||||
.where(
|
||||
(SalesInvoice.name == SalesInvoiceItem.parent)
|
||||
& (SalesInvoice.is_return == 0)
|
||||
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
|
||||
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run(as_dict=True)
|
||||
return result[0].name if result else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -92,8 +92,8 @@ status_map = {
|
||||
"Delivery Note": [
|
||||
["Draft", None],
|
||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
|
||||
@@ -991,7 +991,13 @@ class StockController(AccountsController):
|
||||
def update_billing_percentage(self, update_modified=True):
|
||||
target_ref_field = "amount"
|
||||
if self.doctype == "Delivery Note":
|
||||
target_ref_field = "amount - (returned_qty * rate)"
|
||||
total_amount = total_returned = 0
|
||||
for item in self.items:
|
||||
total_amount += flt(item.amount)
|
||||
total_returned += flt(item.returned_qty * item.rate)
|
||||
|
||||
if total_returned < total_amount:
|
||||
target_ref_field = "(amount - (returned_qty * rate))"
|
||||
|
||||
self._update_percent_field(
|
||||
{
|
||||
@@ -1044,6 +1050,16 @@ class StockController(AccountsController):
|
||||
|
||||
def validate_qi_presence(self, row):
|
||||
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
|
||||
if self.doctype in [
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
] and frappe.db.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
):
|
||||
return
|
||||
|
||||
if not row.quality_inspection:
|
||||
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
|
||||
row.idx, frappe.bold(row.item_code)
|
||||
@@ -1156,6 +1172,12 @@ class StockController(AccountsController):
|
||||
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
return
|
||||
|
||||
self.__inter_company_reference = (
|
||||
self.get("inter_company_reference")
|
||||
if self.doctype == "Purchase Invoice"
|
||||
else self.get("inter_company_invoice_reference")
|
||||
)
|
||||
|
||||
item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
|
||||
if not item_wise_transfer_qty:
|
||||
return
|
||||
@@ -1185,15 +1207,11 @@ class StockController(AccountsController):
|
||||
bold(key[1]),
|
||||
bold(flt(transferred_qty, precision)),
|
||||
bold(parent_doctype),
|
||||
get_link_to_form(parent_doctype, self.get("inter_company_reference")),
|
||||
get_link_to_form(parent_doctype, self.__inter_company_reference),
|
||||
)
|
||||
)
|
||||
|
||||
def get_item_wise_inter_transfer_qty(self):
|
||||
reference_field = "inter_company_reference"
|
||||
if self.doctype == "Purchase Invoice":
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
|
||||
parent_doctype = {
|
||||
"Purchase Receipt": "Delivery Note",
|
||||
"Purchase Invoice": "Sales Invoice",
|
||||
@@ -1213,7 +1231,7 @@ class StockController(AccountsController):
|
||||
child_tab.item_code,
|
||||
child_tab.qty,
|
||||
)
|
||||
.where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1))
|
||||
.where((parent_tab.name == self.__inter_company_reference) & (parent_tab.docstatus == 1))
|
||||
)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
@@ -21,8 +21,6 @@ from erpnext.deprecation_dumpster import deprecated
|
||||
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template, get_item_tax_map
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
logger = frappe.logger(__name__)
|
||||
|
||||
ItemWiseTaxDetail = frappe._dict
|
||||
|
||||
|
||||
@@ -384,22 +382,22 @@ class calculate_taxes_and_totals:
|
||||
self._calculate()
|
||||
|
||||
def calculate_taxes(self):
|
||||
self.grand_total_diff = 0
|
||||
doc = self.doc
|
||||
if not doc.get("taxes"):
|
||||
return
|
||||
|
||||
# maintain actual tax rate based on idx
|
||||
actual_tax_dict = dict(
|
||||
[
|
||||
[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
|
||||
for tax in self.doc.get("taxes")
|
||||
for tax in doc.taxes
|
||||
if tax.charge_type == "Actual"
|
||||
]
|
||||
)
|
||||
|
||||
logger.debug(f"{self.doc} ...")
|
||||
for n, item in enumerate(self._items):
|
||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||
logger.debug(f" Item {n}: {item.item_code}" + (f" - {item_tax_map}" if item_tax_map else ""))
|
||||
for i, tax in enumerate(self.doc.get("taxes")):
|
||||
for i, tax in enumerate(doc.taxes):
|
||||
# tax_amount represents the amount of tax for the current step
|
||||
current_net_amount, current_tax_amount = self.get_current_tax_and_net_amount(
|
||||
item, tax, item_tax_map
|
||||
@@ -438,37 +436,42 @@ class calculate_taxes_and_totals:
|
||||
tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount)
|
||||
else:
|
||||
tax.grand_total_for_current_item = flt(
|
||||
self.doc.get("taxes")[i - 1].grand_total_for_current_item + current_tax_amount
|
||||
doc.taxes[i - 1].grand_total_for_current_item + current_tax_amount
|
||||
)
|
||||
|
||||
# set precision in the last item iteration
|
||||
if n == len(self._items) - 1:
|
||||
self.round_off_totals(tax)
|
||||
self._set_in_company_currency(
|
||||
tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]
|
||||
discount_amount_applied = self.discount_amount_applied
|
||||
if doc.apply_discount_on == "Grand Total" and (
|
||||
discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage
|
||||
):
|
||||
tax_amount_precision = doc.taxes[0].precision("tax_amount")
|
||||
|
||||
for i, tax in enumerate(doc.taxes):
|
||||
if discount_amount_applied:
|
||||
tax.tax_amount_after_discount_amount = flt(
|
||||
tax.tax_amount_after_discount_amount, tax_amount_precision
|
||||
)
|
||||
|
||||
self.round_off_base_values(tax)
|
||||
self.set_cumulative_total(i, tax)
|
||||
self.set_cumulative_total(i, tax)
|
||||
|
||||
self._set_in_company_currency(tax, ["total"])
|
||||
|
||||
# adjust Discount Amount loss in last tax iteration
|
||||
if (
|
||||
i == (len(self.doc.get("taxes")) - 1)
|
||||
and self.discount_amount_applied
|
||||
and self.doc.discount_amount
|
||||
and self.doc.apply_discount_on == "Grand Total"
|
||||
):
|
||||
self.grand_total_diff = flt(
|
||||
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"),
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f" net_amount: {current_net_amount:<20} tax_amount: {current_tax_amount:<20} - {tax.description}"
|
||||
if not discount_amount_applied:
|
||||
self.grand_total_for_distributing_discount = doc.taxes[-1].total
|
||||
else:
|
||||
self.grand_total_diff = flt(
|
||||
self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total,
|
||||
doc.precision("grand_total"),
|
||||
)
|
||||
|
||||
for i, tax in enumerate(doc.taxes):
|
||||
self.round_off_totals(tax)
|
||||
self._set_in_company_currency(
|
||||
tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]
|
||||
)
|
||||
|
||||
self.round_off_base_values(tax)
|
||||
self.set_cumulative_total(i, tax)
|
||||
|
||||
self._set_in_company_currency(tax, ["total"])
|
||||
|
||||
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
|
||||
# if just for valuation, do not add the tax amount in total
|
||||
# if tax/charges is for deduction, multiply by -1
|
||||
@@ -612,16 +615,20 @@ class calculate_taxes_and_totals:
|
||||
|
||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
||||
self.grand_total_diff = diff
|
||||
else:
|
||||
self.grand_total_diff = 0
|
||||
|
||||
def calculate_totals(self):
|
||||
grand_total_diff = getattr(self, "grand_total_diff", 0)
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - self.grand_total_diff,
|
||||
self.doc.grand_total - self.doc.net_total - grand_total_diff,
|
||||
self.doc.precision("total_taxes_and_charges"),
|
||||
)
|
||||
else:
|
||||
@@ -766,7 +773,8 @@ class calculate_taxes_and_totals:
|
||||
self.doc.base_discount_amount = 0
|
||||
|
||||
def get_total_for_discount_amount(self):
|
||||
if self.doc.apply_discount_on == "Net Total":
|
||||
doc = self.doc
|
||||
if doc.apply_discount_on == "Net Total" or not doc.get("taxes"):
|
||||
return self.doc.net_total
|
||||
|
||||
total_actual_tax = 0
|
||||
@@ -786,7 +794,7 @@ class calculate_taxes_and_totals:
|
||||
"cumulative_tax_amount": total_actual_tax,
|
||||
}
|
||||
|
||||
for tax in self.doc.get("taxes"):
|
||||
for tax in doc.taxes:
|
||||
if tax.charge_type in ["Actual", "On Item Quantity"]:
|
||||
update_actual_tax_dict(tax, tax.tax_amount)
|
||||
continue
|
||||
@@ -805,7 +813,7 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
|
||||
|
||||
return self.doc.grand_total - total_actual_tax
|
||||
return getattr(self, "grand_total_for_distributing_discount", doc.grand_total) - total_actual_tax
|
||||
|
||||
def calculate_total_advance(self):
|
||||
if not self.doc.docstatus.is_cancelled():
|
||||
|
||||
@@ -2,6 +2,9 @@ import unittest
|
||||
from functools import partial
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.core.doctype.user_permission.user_permission import add_user_permissions
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.controllers import queries
|
||||
@@ -85,3 +88,54 @@ class TestQueries(IntegrationTestCase):
|
||||
|
||||
def test_default_uoms(self):
|
||||
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
|
||||
|
||||
def test_employee_query_with_user_permissions(self):
|
||||
# party field is a dynamic link field in Payment Entry doctype with ignore_user_permissions=0
|
||||
ps = make_property_setter(
|
||||
doctype="Payment Entry",
|
||||
fieldname="party",
|
||||
property="ignore_user_permissions",
|
||||
value=1,
|
||||
property_type="Check",
|
||||
)
|
||||
ps.save()
|
||||
|
||||
user = create_user("test_employee_query@example.com", ("Accounts User", "HR User"))
|
||||
add_user_permissions(
|
||||
{
|
||||
"user": user.name,
|
||||
"doctype": "Employee",
|
||||
"docname": "_T-Employee-00001",
|
||||
"is_default": 1,
|
||||
"apply_to_all_doctypes": 1,
|
||||
"applicable_doctypes": [],
|
||||
"hide_descendants": 0,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.reload_doc("accounts", "doctype", "payment entry")
|
||||
|
||||
frappe.set_user(user.name)
|
||||
params = {
|
||||
"doctype": "Employee",
|
||||
"txt": "",
|
||||
"searchfield": "name",
|
||||
"start": 0,
|
||||
"page_len": 20,
|
||||
"filters": None,
|
||||
"reference_doctype": "Payment Entry",
|
||||
"ignore_user_permissions": 1,
|
||||
}
|
||||
|
||||
result = queries.employee_query(**params)
|
||||
self.assertGreater(len(result), 1)
|
||||
|
||||
ps.delete(ignore_permissions=1, force=1, delete_permanently=1)
|
||||
frappe.reload_doc("accounts", "doctype", "payment entry")
|
||||
frappe.clear_cache()
|
||||
|
||||
# only one employee should be returned even though ignore_user_permissions is passed as 1
|
||||
result = queries.employee_query(**params)
|
||||
self.assertEqual(len(result), 1)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -147,14 +147,37 @@ def link_open_events(ref_doctype, ref_docname, doc):
|
||||
def get_open_activities(ref_doctype, ref_docname):
|
||||
tasks = get_open_todos(ref_doctype, ref_docname)
|
||||
events = get_open_events(ref_doctype, ref_docname)
|
||||
tasks_history = get_closed_todos(ref_doctype, ref_docname)
|
||||
events_history = get_closed_events(ref_doctype, ref_docname)
|
||||
|
||||
return {"tasks": tasks, "events": events}
|
||||
return {
|
||||
"tasks": tasks,
|
||||
"events": events,
|
||||
"tasks_history": tasks_history,
|
||||
"events_history": events_history,
|
||||
}
|
||||
|
||||
|
||||
def get_closed_todos(ref_doctype, ref_docname):
|
||||
return get_filtered_todos(ref_doctype, ref_docname, status=("!=", "Open"))
|
||||
|
||||
|
||||
def get_open_todos(ref_doctype, ref_docname):
|
||||
return get_filtered_todos(ref_doctype, ref_docname, status="Open")
|
||||
|
||||
|
||||
def get_open_events(ref_doctype, ref_docname):
|
||||
return get_filtered_events(ref_doctype, ref_docname, open=True)
|
||||
|
||||
|
||||
def get_closed_events(ref_doctype, ref_docname):
|
||||
return get_filtered_events(ref_doctype, ref_docname, open=False)
|
||||
|
||||
|
||||
def get_filtered_todos(ref_doctype, ref_docname, status: str | tuple[str, str]):
|
||||
return frappe.get_all(
|
||||
"ToDo",
|
||||
filters={"reference_type": ref_doctype, "reference_name": ref_docname, "status": "Open"},
|
||||
filters={"reference_type": ref_doctype, "reference_name": ref_docname, "status": status},
|
||||
fields=[
|
||||
"name",
|
||||
"description",
|
||||
@@ -164,10 +187,15 @@ def get_open_todos(ref_doctype, ref_docname):
|
||||
)
|
||||
|
||||
|
||||
def get_open_events(ref_doctype, ref_docname):
|
||||
def get_filtered_events(ref_doctype, ref_docname, open: bool):
|
||||
event = frappe.qb.DocType("Event")
|
||||
event_link = frappe.qb.DocType("Event Participants")
|
||||
|
||||
if open:
|
||||
event_status_filter = event.status == "Open"
|
||||
else:
|
||||
event_status_filter = event.status != "Open"
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(event)
|
||||
.join(event_link)
|
||||
@@ -183,7 +211,7 @@ def get_open_events(ref_doctype, ref_docname):
|
||||
.where(
|
||||
(event_link.reference_doctype == ref_doctype)
|
||||
& (event_link.reference_docname == ref_docname)
|
||||
& (event.status == "Open")
|
||||
& (event_status_filter)
|
||||
)
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
2644
erpnext/locale/ar.po
2644
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
2650
erpnext/locale/bs.po
2650
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
2678
erpnext/locale/de.po
2678
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
2646
erpnext/locale/eo.po
2646
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2644
erpnext/locale/es.po
2644
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
2656
erpnext/locale/fa.po
2656
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
2644
erpnext/locale/fr.po
2644
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
2644
erpnext/locale/hr.po
2644
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
2644
erpnext/locale/hu.po
2644
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2644
erpnext/locale/pl.po
2644
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
101024
erpnext/locale/pt.po
101024
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2644
erpnext/locale/ru.po
2644
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
20312
erpnext/locale/sr_CS.po
20312
erpnext/locale/sr_CS.po
File diff suppressed because it is too large
Load Diff
2658
erpnext/locale/sv.po
2658
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
2966
erpnext/locale/th.po
2966
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
2644
erpnext/locale/tr.po
2644
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
2674
erpnext/locale/zh.po
2674
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -334,7 +334,7 @@ frappe.ui.form.on("Production Plan", {
|
||||
|
||||
frm.set_value("consider_minimum_order_qty", 0);
|
||||
|
||||
if (frm.doc.ignore_existing_ordered_qty) {
|
||||
if (!frm.doc.ignore_existing_ordered_qty) {
|
||||
frm.events.get_items_for_material_requests(frm);
|
||||
} else {
|
||||
const title = __("Transfer Materials For Warehouse {0}", [frm.doc.for_warehouse]);
|
||||
|
||||
@@ -230,11 +230,11 @@
|
||||
"label": "Include Subcontracted Items"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.",
|
||||
"default": "1",
|
||||
"description": "If enabled, the system will consider items with a shortfall in quantity. \n<br>\nQty = Reqd Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>",
|
||||
"fieldname": "ignore_existing_ordered_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Available Stock"
|
||||
"label": "Skip Available Raw Materials"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_25",
|
||||
@@ -249,7 +249,7 @@
|
||||
{
|
||||
"fieldname": "get_items_for_mr",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Raw Materials for Purchase"
|
||||
"label": "Get Items for Purchase Only"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_27",
|
||||
@@ -391,9 +391,10 @@
|
||||
"label": "Consolidate Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"description": "If items in stock, proceed with Material Transfer or Purchase.",
|
||||
"fieldname": "transfer_materials",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Raw Materials for Transfer"
|
||||
"label": "Get Items for Purchase / Transfer"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -402,8 +403,8 @@
|
||||
"label": "Preview Required Materials"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If this checkbox is enabled, then the system won\u2019t run the MRP for the available sub-assembly items.",
|
||||
"default": "1",
|
||||
"description": "If enabled, the system will consider items with a shortfall in quantity. \n<br>\nQty = Reqd Qty (BOM) - <a href=\"https://docs.frappe.io/erpnext/user/manual/en/projected-quantity\">Projected Qty</a>",
|
||||
"fieldname": "skip_available_sub_assembly_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Skip Available Sub Assembly Items"
|
||||
@@ -436,11 +437,12 @@
|
||||
"label": "Consider Minimum Order Qty"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-10 17:47:52.207209",
|
||||
"modified": "2025-04-08 17:24:09.394056",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
@@ -461,7 +463,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,8 +950,7 @@ class ProductionPlan(Document):
|
||||
|
||||
if material_request_list:
|
||||
material_request_list = [
|
||||
f"""<a href="/app/Form/Material Request/{m.name}">{m.name}</a>"""
|
||||
for m in material_request_list
|
||||
get_link_to_form("Material Request", m.name) for m in material_request_list
|
||||
]
|
||||
msgprint(_("{0} created").format(comma_and(material_request_list)))
|
||||
else:
|
||||
@@ -1329,7 +1328,7 @@ def get_material_request_items(
|
||||
total_qty = row["qty"]
|
||||
|
||||
required_qty = 0
|
||||
if ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
|
||||
if not ignore_existing_ordered_qty or bin_dict.get("projected_qty", 0) < 0:
|
||||
required_qty = total_qty
|
||||
elif total_qty > bin_dict.get("projected_qty", 0):
|
||||
required_qty = total_qty - bin_dict.get("projected_qty", 0)
|
||||
@@ -1688,7 +1687,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
if items:
|
||||
mr_items.append(items)
|
||||
|
||||
if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
|
||||
if (ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
|
||||
new_mr_items = []
|
||||
for item in mr_items:
|
||||
get_materials_from_other_locations(item, warehouses, new_mr_items, company)
|
||||
|
||||
@@ -180,7 +180,7 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
)
|
||||
|
||||
pln = create_production_plan(
|
||||
item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=0
|
||||
item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=1
|
||||
)
|
||||
self.assertFalse(len(pln.mr_items))
|
||||
|
||||
@@ -725,6 +725,7 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
pln.skip_available_sub_assembly_item = 0
|
||||
pln.get_sub_assembly_items("In House")
|
||||
pln.submit()
|
||||
pln.make_work_order()
|
||||
@@ -1454,6 +1455,7 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
)
|
||||
|
||||
plan.for_warehouse = mrp_warhouse
|
||||
plan.ignore_existing_ordered_qty = 1
|
||||
|
||||
items = get_items_for_material_requests(
|
||||
plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}]
|
||||
@@ -1690,6 +1692,7 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
)
|
||||
|
||||
pln.for_warehouse = rm_warehouse
|
||||
pln.ignore_existing_ordered_qty = 1
|
||||
items = get_items_for_material_requests(pln.as_dict(), warehouses=[{"warehouse": store_warehouse}])
|
||||
|
||||
for row in items:
|
||||
@@ -1891,6 +1894,7 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
},
|
||||
)
|
||||
plan.save()
|
||||
plan.ignore_existing_ordered_qty = 1
|
||||
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
|
||||
@@ -107,7 +107,6 @@ erpnext.patches.v12_0.remove_bank_remittance_custom_fields
|
||||
erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit
|
||||
erpnext.patches.v12_0.add_variant_of_in_item_attribute_table
|
||||
erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account
|
||||
erpnext.patches.v12_0.create_default_energy_point_rules
|
||||
erpnext.patches.v12_0.set_produced_qty_field_in_sales_order_for_work_order
|
||||
erpnext.patches.v12_0.set_cwip_and_delete_asset_settings
|
||||
erpnext.patches.v12_0.set_expense_account_in_landed_cost_voucher_taxes
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.setup.install import create_default_energy_point_rules
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("social", "doctype", "energy_point_rule")
|
||||
create_default_energy_point_rules()
|
||||
@@ -54,6 +54,12 @@ erpnext.buying = {
|
||||
return erpnext.queries.company_address_query(this.frm.doc)
|
||||
});
|
||||
}
|
||||
|
||||
if(this.frm.get_field('dispatch_address')) {
|
||||
this.frm.set_query("dispatch_address", () => {
|
||||
return erpnext.queries.address_query(this.frm.doc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setup_queries(doc, cdt, cdn) {
|
||||
@@ -295,6 +301,12 @@ erpnext.buying = {
|
||||
"shipping_address_display", true);
|
||||
}
|
||||
|
||||
dispatch_address(){
|
||||
var me = this;
|
||||
erpnext.utils.get_address_display(this.frm, "dispatch_address",
|
||||
"dispatch_address_display", true);
|
||||
}
|
||||
|
||||
billing_address() {
|
||||
erpnext.utils.get_address_display(this.frm, "billing_address",
|
||||
"billing_address_display", true);
|
||||
|
||||
@@ -343,12 +343,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
calculate_taxes() {
|
||||
const doc = this.frm.doc;
|
||||
if (!doc.taxes?.length) return;
|
||||
|
||||
var me = this;
|
||||
this.grand_total_diff = 0;
|
||||
var actual_tax_dict = {};
|
||||
|
||||
// maintain actual tax rate based on idx
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
$.each(doc.taxes, function(i, tax) {
|
||||
if (tax.charge_type == "Actual") {
|
||||
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
|
||||
}
|
||||
@@ -356,7 +358,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
$.each(this.frm._items || [], function(n, item) {
|
||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||
$.each(me.frm.doc["taxes"] || [], function(i, tax) {
|
||||
$.each(doc.taxes, function(i, tax) {
|
||||
// tax_amount represents the amount of tax for the current step
|
||||
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
|
||||
if (frappe.flags.round_row_wise_tax) {
|
||||
@@ -401,29 +403,40 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
tax.grand_total_for_current_item =
|
||||
flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount);
|
||||
}
|
||||
|
||||
// set precision in the last item iteration
|
||||
if (n == me.frm._items.length - 1) {
|
||||
me.round_off_totals(tax);
|
||||
me.set_in_company_currency(tax,
|
||||
["tax_amount", "tax_amount_after_discount_amount"]);
|
||||
|
||||
me.round_off_base_values(tax);
|
||||
|
||||
// in tax.total, accumulate grand total for each item
|
||||
me.set_cumulative_total(i, tax);
|
||||
|
||||
me.set_in_company_currency(tax, ["total"]);
|
||||
|
||||
// adjust Discount Amount loss in last tax iteration
|
||||
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
|
||||
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
|
||||
me.grand_total_diff = flt(me.frm.doc.grand_total -
|
||||
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const discount_amount_applied = this.discount_amount_applied;
|
||||
if (doc.apply_discount_on === "Grand Total" && (discount_amount_applied || doc.discount_amount || doc.additional_discount_percentage)) {
|
||||
const tax_amount_precision = precision("tax_amount", doc.taxes[0]);
|
||||
|
||||
for (const [i, tax] of doc.taxes.entries()) {
|
||||
if (discount_amount_applied)
|
||||
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, tax_amount_precision);
|
||||
|
||||
this.set_cumulative_total(i, tax);
|
||||
}
|
||||
|
||||
if (!this.discount_amount_applied) {
|
||||
this.grand_total_for_distributing_discount = doc.taxes[doc.taxes.length - 1].total;
|
||||
} else {
|
||||
this.grand_total_diff = flt(
|
||||
this.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[doc.taxes.length - 1].total, precision("grand_total"));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [i, tax] of doc.taxes.entries()) {
|
||||
me.round_off_totals(tax);
|
||||
me.set_in_company_currency(tax,
|
||||
["tax_amount", "tax_amount_after_discount_amount"]);
|
||||
|
||||
me.round_off_base_values(tax);
|
||||
|
||||
// in tax.total, accumulate grand total for each tax
|
||||
me.set_cumulative_total(i, tax);
|
||||
|
||||
me.set_in_company_currency(tax, ["total"]);
|
||||
}
|
||||
}
|
||||
|
||||
set_cumulative_total(row_idx, tax) {
|
||||
@@ -586,10 +599,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
calculate_totals() {
|
||||
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
|
||||
var me = this;
|
||||
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
||||
const me = this;
|
||||
const tax_count = this.frm.doc.taxes?.length;
|
||||
const grand_total_diff = this.grand_total_diff || 0;
|
||||
|
||||
this.frm.doc.grand_total = flt(tax_count
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff
|
||||
: this.frm.doc.net_total);
|
||||
|
||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
@@ -621,7 +636,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
|
||||
- this.grand_total_diff, precision("total_taxes_and_charges"));
|
||||
- grand_total_diff, precision("total_taxes_and_charges"));
|
||||
|
||||
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
|
||||
|
||||
@@ -744,8 +759,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
get_total_for_discount_amount() {
|
||||
if(this.frm.doc.apply_discount_on == "Net Total")
|
||||
return this.frm.doc.net_total;
|
||||
const doc = this.frm.doc;
|
||||
|
||||
if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length)
|
||||
return doc.net_total;
|
||||
|
||||
let total_actual_tax = 0.0;
|
||||
let actual_taxes_dict = {};
|
||||
@@ -760,7 +777,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
};
|
||||
}
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
doc.taxes.forEach(tax => {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
update_actual_taxes_dict(tax, tax.tax_amount);
|
||||
return;
|
||||
@@ -775,7 +792,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
|
||||
});
|
||||
|
||||
return this.frm.doc.grand_total - total_actual_tax;
|
||||
return (this.grand_total_for_distributing_discount || doc.grand_total) - total_actual_tax;
|
||||
}
|
||||
|
||||
calculate_total_advance(update_paid_amount) {
|
||||
|
||||
@@ -795,6 +795,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.serial_no) {
|
||||
item.use_serial_batch_fields = 1
|
||||
}
|
||||
|
||||
if (item && item.serial_no) {
|
||||
if (!item.item_code) {
|
||||
this.frm.trigger("item_code", cdt, cdn);
|
||||
@@ -1130,13 +1134,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
currency() {
|
||||
// The transaction date be either transaction_date (from orders) or posting_date (from invoices)
|
||||
let transaction_date = this.frm.doc.transaction_date || this.frm.doc.posting_date;
|
||||
let inter_company_reference = this.frm.doc.inter_company_order_reference || this.frm.doc.inter_company_invoice_reference;
|
||||
|
||||
let me = this;
|
||||
this.set_dynamic_labels();
|
||||
let company_currency = this.get_company_currency();
|
||||
// Added `load_after_mapping` to determine if document is loading after mapping from another doc
|
||||
if(this.frm.doc.currency && this.frm.doc.currency !== company_currency
|
||||
&& !this.frm.doc.__onload?.load_after_mapping) {
|
||||
&& (!this.frm.doc.__onload?.load_after_mapping || inter_company_reference)) {
|
||||
|
||||
this.get_exchange_rate(transaction_date, this.frm.doc.currency, company_currency,
|
||||
function(exchange_rate) {
|
||||
@@ -1357,13 +1362,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
batch_no(doc, cdt, cdn) {
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (!this.is_a_mapped_document(item)) {
|
||||
this.apply_price_list(item, true);
|
||||
}
|
||||
}
|
||||
|
||||
toggle_conversion_factor(item) {
|
||||
// toggle read only property for conversion factor field if the uom and stock uom are same
|
||||
if(this.frm.get_field('items').grid.fields_map.conversion_factor) {
|
||||
@@ -1589,7 +1587,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
batch_no(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.use_serial_batch_fields && row.batch_no) {
|
||||
|
||||
if (row.batch_no) {
|
||||
row.use_serial_batch_fields = 1
|
||||
}
|
||||
|
||||
if (row.batch_no) {
|
||||
var params = this._get_args(row);
|
||||
params.batch_no = row.batch_no;
|
||||
params.uom = row.uom;
|
||||
|
||||
@@ -239,13 +239,16 @@ erpnext.setup.fiscal_years = {
|
||||
Afghanistan: ["12-21", "12-20"],
|
||||
Australia: ["07-01", "06-30"],
|
||||
Bangladesh: ["07-01", "06-30"],
|
||||
Canada: ["04-01", "03-31"],
|
||||
"Costa Rica": ["10-01", "09-30"],
|
||||
Egypt: ["07-01", "06-30"],
|
||||
Ethiopia: ["07-08", "07-07"],
|
||||
"Hong Kong": ["04-01", "03-31"],
|
||||
India: ["04-01", "03-31"],
|
||||
Iran: ["06-23", "06-22"],
|
||||
Kenya: ["07-01", "06-30"],
|
||||
Malaysia: ["07-01", "06-30"],
|
||||
Myanmar: ["04-01", "03-31"],
|
||||
Nepal: ["07-16", "07-15"],
|
||||
"New Zealand": ["04-01", "03-31"],
|
||||
Pakistan: ["07-01", "06-30"],
|
||||
Singapore: ["04-01", "03-31"],
|
||||
|
||||
@@ -57,6 +57,47 @@
|
||||
{{ __("No open task") }}
|
||||
</div>
|
||||
{% } %}
|
||||
|
||||
{% if (typeof tasks_history == "object" && tasks_history?.length) { %}
|
||||
<div class="mt-4" style="opacity: 0.8;">
|
||||
<div class="open-section-head">
|
||||
<span>{{ __("Completed Tasks") }}</span>
|
||||
</div>
|
||||
{% for (const t of tasks_history) { %}
|
||||
<div class="single-activity">
|
||||
{% if(t.date || t.allocated_to) { %}
|
||||
<div class="text-muted small flex align-items-center">
|
||||
<div>
|
||||
{% if(t.allocated_to) { %}
|
||||
{%= frappe.avatar(t.allocated_to) %}
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<div class="indicator-pill no-indicator-dot whitespace-nowrap red">
|
||||
{{ __("Done") }}
|
||||
</div>
|
||||
</div>
|
||||
{% if (t.date) { %}
|
||||
<div class="ml-auto">
|
||||
{%= frappe.datetime.global_date_format(t.date) %}
|
||||
</div>
|
||||
{% } %}
|
||||
<div class="checkbox {% if(!t.date) { %}ml-auto{% } else { %}ml-2{% } %}">
|
||||
<input type="checkbox" class="completion-checkbox" checked disabled>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
<div class="pl-2 mb-2 mt-2">
|
||||
<div class="label-area font-md">
|
||||
<a href="/app/todo/{{ t.name }}" title="{{ __('Open Task') }}">
|
||||
{%= t.description %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="open-events pl-1">
|
||||
<div class="open-section-head">
|
||||
@@ -104,6 +145,49 @@
|
||||
{{ __("No open event") }}
|
||||
</div>
|
||||
{% } %}
|
||||
|
||||
{% if (typeof events_history == "object" && events_history?.length) { %}
|
||||
<div class="mt-4" style="opacity: 0.8;">
|
||||
<div class="open-section-head">
|
||||
<span>{{ __("Past Events") }}</span>
|
||||
</div>
|
||||
{% let icon_set = {"Sent/Received Email": "mail", "Call": "call", "Meeting": "share-people"}; %}
|
||||
{% for(const event of events_history) { %}
|
||||
<div class="single-activity">
|
||||
<div class="flex justify-between mb-2">
|
||||
<div class="row label-area font-md ml-1 title">
|
||||
<span class="mr-2">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-{{ icon_set[event.event_category] || 'calendar' }}"></use>
|
||||
</svg>
|
||||
</span>
|
||||
<a href="/app/event/{{ event.name }}" title="{{ __('Open Event') }}">
|
||||
{%= event.subject %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="completion-checkbox" checked disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-muted ml-1">
|
||||
{%= frappe.datetime.global_date_format(event.starts_on) %}
|
||||
|
||||
{% if (event.ends_on) { %}
|
||||
{% if (frappe.datetime.obj_to_user(event.starts_on) != frappe.datetime.obj_to_user(event.ends_on)) %}
|
||||
-
|
||||
{%= frappe.datetime.global_date_format(frappe.datetime.obj_to_user(event.ends_on)) %}
|
||||
{%= frappe.datetime.get_time(event.ends_on) %}
|
||||
{% } else if (event.ends_on) { %}
|
||||
-
|
||||
{%= frappe.datetime.get_time(event.ends_on) %}
|
||||
{% } %}
|
||||
{% } %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,8 @@ erpnext.utils.CRMActivities = class CRMActivities {
|
||||
var activities_html = frappe.render_template("crm_activities", {
|
||||
tasks: r.message.tasks,
|
||||
events: r.message.events,
|
||||
tasks_history: r.message.tasks_history,
|
||||
events_history: r.message.events_history,
|
||||
});
|
||||
|
||||
$(activities_html).appendTo(me.open_activities_wrapper);
|
||||
|
||||
@@ -78,35 +78,34 @@ erpnext.accounts.dimensions = {
|
||||
},
|
||||
|
||||
update_dimension(frm, doctype) {
|
||||
if (this.accounting_dimensions) {
|
||||
this.accounting_dimensions.forEach((dimension) => {
|
||||
if (frm.is_new()) {
|
||||
if (
|
||||
frm.doc.company &&
|
||||
Object.keys(this.default_dimensions || {}).length > 0 &&
|
||||
this.default_dimensions[frm.doc.company]
|
||||
) {
|
||||
let default_dimension =
|
||||
this.default_dimensions[frm.doc.company][dimension["fieldname"]];
|
||||
if (
|
||||
!this.accounting_dimensions ||
|
||||
!frm.is_new() ||
|
||||
!frm.doc.company ||
|
||||
!this.default_dimensions?.[frm.doc.company]
|
||||
)
|
||||
return;
|
||||
|
||||
if (default_dimension) {
|
||||
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
|
||||
frm.set_value(dimension["fieldname"], default_dimension);
|
||||
}
|
||||
|
||||
$.each(frm.doc.items || frm.doc.accounts || [], function (i, row) {
|
||||
frappe.model.set_value(
|
||||
row.doctype,
|
||||
row.name,
|
||||
dimension["fieldname"],
|
||||
default_dimension
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// don't set default dimensions if any of the dimension is already set due to mapping
|
||||
if (frm.doc.__onload?.load_after_mapping) {
|
||||
for (const dimension of this.accounting_dimensions) {
|
||||
if (frm.doc[dimension["fieldname"]]) return;
|
||||
}
|
||||
}
|
||||
|
||||
this.accounting_dimensions.forEach((dimension) => {
|
||||
const default_dimension = this.default_dimensions[frm.doc.company][dimension["fieldname"]];
|
||||
|
||||
if (!default_dimension) return;
|
||||
|
||||
if (frappe.meta.has_field(doctype, dimension["fieldname"])) {
|
||||
frm.set_value(dimension["fieldname"], default_dimension);
|
||||
}
|
||||
|
||||
(frm.doc.items || frm.doc.accounts || []).forEach((row) => {
|
||||
frappe.model.set_value(row.doctype, row.name, dimension["fieldname"], default_dimension);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
copy_dimension_from_first_row(frm, cdt, cdn, fieldname) {
|
||||
|
||||
@@ -71,6 +71,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) {
|
||||
if (!args.shipping_address && frm.doc.shipping_address) {
|
||||
args.shipping_address = frm.doc.shipping_address;
|
||||
}
|
||||
|
||||
if (!args.dispatch_address && frm.doc.dispatch_address) {
|
||||
args.dispatch_address = frm.doc.dispatch_address;
|
||||
}
|
||||
}
|
||||
|
||||
if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
|
||||
@@ -115,6 +115,33 @@ erpnext.sales_common = {
|
||||
this.toggle_editable_price_list_rate();
|
||||
}
|
||||
|
||||
company() {
|
||||
super.company();
|
||||
this.set_default_company_address();
|
||||
}
|
||||
|
||||
set_default_company_address() {
|
||||
if (!frappe.meta.has_field(this.frm.doc.doctype, "company_address")) return;
|
||||
var me = this;
|
||||
if (this.frm.doc.company) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: {
|
||||
name: this.frm.doc.company,
|
||||
existing_address: this.frm.doc.company_address || "",
|
||||
},
|
||||
debounce: 2000,
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
me.frm.set_value("company_address", r.message);
|
||||
} else {
|
||||
me.frm.set_value("company_address", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customer() {
|
||||
var me = this;
|
||||
erpnext.utils.get_party_details(this.frm, null, null, function () {
|
||||
|
||||
@@ -610,6 +610,7 @@ body[data-route="pos"] {
|
||||
.frappe-control[data-fieldname="address_display"] .ql-editor,
|
||||
.frappe-control[data-fieldname="shipping_address_display"] .ql-editor,
|
||||
.frappe-control[data-fieldname="shipping_address"] .ql-editor,
|
||||
.frappe-control[data-fieldname="dispatch_address_display"] .ql-editor,
|
||||
.frappe-control[data-fieldname="dispatch_address"] .ql-editor,
|
||||
.frappe-control[data-fieldname="source_address_display"] .ql-editor,
|
||||
.frappe-control[data-fieldname="target_address_display"] .ql-editor,
|
||||
|
||||
@@ -153,9 +153,11 @@
|
||||
margin-bottom: 0px;
|
||||
min-height: 8rem;
|
||||
height: 8rem;
|
||||
overflow: hidden;
|
||||
|
||||
> img {
|
||||
@extend .image;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,9 +397,11 @@
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-light);
|
||||
margin-right: var(--margin-md);
|
||||
overflow: hidden;
|
||||
|
||||
> img {
|
||||
@extend .image;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,14 +703,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 11rem;
|
||||
height: 11rem;
|
||||
max-width: 11rem;
|
||||
max-height: 11rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-left: var(--margin-md);
|
||||
color: var(--gray-500);
|
||||
overflow: hidden;
|
||||
|
||||
> img {
|
||||
@extend .image;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> .item-abbr {
|
||||
@@ -716,8 +722,8 @@
|
||||
justify-content: center;
|
||||
border-radius: var(--border-radius-md);
|
||||
font-size: var(--text-3xl);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 11rem;
|
||||
height: 11rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,6 +753,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: var(--padding-md);
|
||||
row-gap: var(--padding-md);
|
||||
|
||||
> .auto-fetch-btn {
|
||||
@extend .pointer-no-select;
|
||||
@@ -1167,6 +1174,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.point-of-sale-app {
|
||||
> .items-selector {
|
||||
> .items-container {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 620px) {
|
||||
.point-of-sale-app {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
|
||||
@@ -378,6 +378,7 @@ def make_custom_fields(update=True):
|
||||
),
|
||||
],
|
||||
"Purchase Invoice Item": invoice_item_fields,
|
||||
"POS Invoice Item": invoice_item_fields,
|
||||
"Sales Order Item": invoice_item_fields,
|
||||
"Delivery Note Item": invoice_item_fields,
|
||||
"Sales Invoice Item": invoice_item_fields + customer_po_fields,
|
||||
|
||||
@@ -78,7 +78,8 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
onload(doc, dt, dn) {
|
||||
super.onload(doc, dt, dn);
|
||||
|
||||
this.frm.trigger("disable_customer_if_creating_from_opportunity");
|
||||
// TODO: think of better way to do this
|
||||
// this.frm.trigger("disable_customer_if_creating_from_opportunity");
|
||||
}
|
||||
party_name() {
|
||||
var me = this;
|
||||
|
||||
@@ -175,27 +175,6 @@ frappe.ui.form.on("Sales Order", {
|
||||
);
|
||||
},
|
||||
|
||||
// When multiple companies are set up. in case company name is changed set default company address
|
||||
company: function (frm) {
|
||||
if (frm.doc.company) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.get_default_company_address",
|
||||
args: {
|
||||
name: frm.doc.company,
|
||||
existing_address: frm.doc.company_address || "",
|
||||
},
|
||||
debounce: 2000,
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("company_address", r.message);
|
||||
} else {
|
||||
frm.set_value("company_address", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.transaction_date) {
|
||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, get_datetime
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
|
||||
@@ -328,25 +328,59 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
invoice_list = []
|
||||
|
||||
if search_term and status:
|
||||
invoices_by_customer = frappe.db.get_list(
|
||||
pos_invoices_by_customer = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||
filters=get_invoice_filters("POS Invoice", status, customer=search_term),
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
invoices_by_name = frappe.db.get_list(
|
||||
pos_invoices_by_name = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
||||
filters=get_invoice_filters("POS Invoice", status, name=search_term),
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
|
||||
invoice_list = invoices_by_customer + invoices_by_name
|
||||
elif status:
|
||||
invoice_list = frappe.db.get_list(
|
||||
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
|
||||
pos_invoice_list = add_doctype_to_results(
|
||||
"POS Invoice", pos_invoices_by_customer + pos_invoices_by_name
|
||||
)
|
||||
|
||||
sales_invoices_by_customer = frappe.db.get_list(
|
||||
"Sales Invoice",
|
||||
filters=get_invoice_filters("Sales Invoice", status, customer=search_term),
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
sales_invoices_by_name = frappe.db.get_list(
|
||||
"Sales Invoice",
|
||||
filters=get_invoice_filters("Sales Invoice", status, name=search_term),
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
|
||||
sales_invoice_list = add_doctype_to_results(
|
||||
"Sales Invoice", sales_invoices_by_customer + sales_invoices_by_name
|
||||
)
|
||||
|
||||
elif status:
|
||||
pos_invoice_list = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters=get_invoice_filters("POS Invoice", status),
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
pos_invoice_list = add_doctype_to_results("POS Invoice", pos_invoice_list)
|
||||
|
||||
sales_invoice_list = frappe.db.get_list(
|
||||
"Sales Invoice",
|
||||
filters=get_invoice_filters("Sales Invoice", status),
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
sales_invoice_list = add_doctype_to_results("Sales Invoice", sales_invoice_list)
|
||||
|
||||
invoice_list = order_results_by_posting_date([*pos_invoice_list, *sales_invoice_list])
|
||||
|
||||
return invoice_list
|
||||
|
||||
|
||||
@@ -402,3 +436,68 @@ def get_pos_profile_data(pos_profile):
|
||||
|
||||
pos_profile.customer_groups = _customer_groups_with_children
|
||||
return pos_profile
|
||||
|
||||
|
||||
def add_doctype_to_results(doctype, results):
|
||||
for result in results:
|
||||
result["doctype"] = doctype
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def order_results_by_posting_date(results):
|
||||
return sorted(
|
||||
results,
|
||||
key=lambda x: get_datetime(f"{x.get('posting_date')} {x.get('posting_time')}"),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
|
||||
def get_invoice_filters(doctype, status, name=None, customer=None):
|
||||
filters = {}
|
||||
|
||||
if name:
|
||||
filters["name"] = ["like", f"%{name}%"]
|
||||
if customer:
|
||||
filters["customer"] = ["like", f"%{customer}%"]
|
||||
|
||||
if doctype == "POS Invoice":
|
||||
filters["status"] = status
|
||||
return filters
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
filters["is_created_using_pos"] = 1
|
||||
filters["is_consolidated"] = 0
|
||||
|
||||
if status == "Draft":
|
||||
filters["docstatus"] = 0
|
||||
else:
|
||||
filters["docstatus"] = 1
|
||||
if status == "Paid":
|
||||
filters["is_return"] = 0
|
||||
if status == "Return":
|
||||
filters["is_return"] = 1
|
||||
|
||||
filters["pos_closing_entry"] = ["is", "set"] if status == "Consolidated" else ["is", "not set"]
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_customer_recent_transactions(customer):
|
||||
sales_invoices = frappe.db.get_list(
|
||||
"Sales Invoice",
|
||||
filters={"customer": customer, "docstatus": 1, "is_pos": 1, "is_consolidated": 0},
|
||||
fields=["name", "grand_total", "status", "posting_date", "posting_time", "currency"],
|
||||
page_length=20,
|
||||
)
|
||||
|
||||
pos_invoices = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"customer": customer, "docstatus": 1},
|
||||
fields=["name", "grand_total", "status", "posting_date", "posting_time", "currency"],
|
||||
page_length=20,
|
||||
)
|
||||
|
||||
invoices = order_results_by_posting_date(sales_invoices + pos_invoices)
|
||||
return invoices
|
||||
|
||||
@@ -139,6 +139,11 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.allow_negative_stock = flt(message.allow_negative_stock) || false;
|
||||
});
|
||||
|
||||
const use_sales_invoice_in_pos = await frappe.db.get_single_value(
|
||||
"Accounts Settings",
|
||||
"use_sales_invoice_in_pos"
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
|
||||
args: { pos_profile: this.pos_profile },
|
||||
@@ -146,6 +151,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
const profile = res.message;
|
||||
Object.assign(this.settings, profile);
|
||||
this.settings.customer_groups = profile.customer_groups.map((group) => group.name);
|
||||
this.settings.frm_doctype = use_sales_invoice_in_pos ? "Sales Invoice" : "POS Invoice";
|
||||
this.make_app();
|
||||
},
|
||||
});
|
||||
@@ -376,7 +382,6 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
highlight_cart_item: (item) => {
|
||||
const cart_item = this.cart.get_cart_item(item);
|
||||
this.cart.toggle_item_highlight(cart_item);
|
||||
},
|
||||
|
||||
item_field_focused: (fieldname) => {
|
||||
@@ -456,8 +461,9 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.recent_order_list = new erpnext.PointOfSale.PastOrderList({
|
||||
wrapper: this.$components_wrapper,
|
||||
events: {
|
||||
open_invoice_data: (name) => {
|
||||
frappe.db.get_doc("POS Invoice", name).then((doc) => {
|
||||
open_invoice_data: (doctype, name) => {
|
||||
if (!["POS Invoice", "Sales Invoice"].includes(doctype)) return;
|
||||
frappe.db.get_doc(doctype, name).then((doc) => {
|
||||
this.order_summary.load_summary_of(doc);
|
||||
});
|
||||
},
|
||||
@@ -473,21 +479,26 @@ erpnext.PointOfSale.Controller = class {
|
||||
events: {
|
||||
get_frm: () => this.frm,
|
||||
|
||||
process_return: (name) => {
|
||||
process_return: (doctype, name) => {
|
||||
this.recent_order_list.toggle_component(false);
|
||||
frappe.db.get_doc("POS Invoice", name).then((doc) => {
|
||||
frappe.db.get_doc(doctype, name).then((doc) => {
|
||||
frappe.run_serially([
|
||||
() => frappe.dom.freeze(),
|
||||
() => this.make_invoice_frm(doc.doctype),
|
||||
() => this.make_return_invoice(doc),
|
||||
() => this.cart.load_invoice(),
|
||||
() => this.item_selector.toggle_component(true),
|
||||
() => this.item_selector.resize_selector(false),
|
||||
() => this.item_details.toggle_component(false),
|
||||
() => frappe.dom.unfreeze(),
|
||||
]);
|
||||
});
|
||||
},
|
||||
edit_order: (name) => {
|
||||
edit_order: (doctype, name) => {
|
||||
this.recent_order_list.toggle_component(false);
|
||||
frappe.run_serially([
|
||||
() => this.make_invoice_frm(doctype),
|
||||
() => this.sync_draft_invoice_to_frm(doctype, name),
|
||||
() => this.frm.refresh(name),
|
||||
() => this.frm.call("reset_mode_of_payments"),
|
||||
() => this.cart.load_invoice(),
|
||||
@@ -496,9 +507,11 @@ erpnext.PointOfSale.Controller = class {
|
||||
() => this.item_details.toggle_component(false),
|
||||
]);
|
||||
},
|
||||
delete_order: (name) => {
|
||||
frappe.model.delete_doc(this.frm.doc.doctype, name, () => {
|
||||
this.recent_order_list.refresh_list();
|
||||
delete_order: (doctype, name) => {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
frappe.model.delete_doc(doctype, name, () => {
|
||||
this.recent_order_list.refresh_list();
|
||||
});
|
||||
});
|
||||
},
|
||||
new_order: () => {
|
||||
@@ -530,7 +543,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
make_new_invoice() {
|
||||
return frappe.run_serially([
|
||||
() => frappe.dom.freeze(),
|
||||
() => this.make_sales_invoice_frm(),
|
||||
() => this.make_invoice_frm(this.settings.frm_doctype),
|
||||
() => this.set_pos_profile_data(),
|
||||
() => this.set_pos_profile_status(),
|
||||
() => this.cart.load_invoice(),
|
||||
@@ -538,27 +551,27 @@ erpnext.PointOfSale.Controller = class {
|
||||
]);
|
||||
}
|
||||
|
||||
make_sales_invoice_frm() {
|
||||
const doctype = "POS Invoice";
|
||||
make_invoice_frm(doctype) {
|
||||
return new Promise((resolve) => {
|
||||
if (this.frm) {
|
||||
this.frm = this.get_new_frm(this.frm);
|
||||
if (this.frm && this.frm.doctype == doctype) {
|
||||
this.frm = this.get_new_frm(this.frm, doctype);
|
||||
this.frm.doc.items = [];
|
||||
this.frm.doc.is_pos = 1;
|
||||
if (doctype == "Sales Invoice") this.frm.doc.is_created_using_pos = 1;
|
||||
resolve();
|
||||
} else {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
this.frm = this.get_new_frm();
|
||||
this.frm = this.get_new_frm(undefined, doctype);
|
||||
this.frm.doc.items = [];
|
||||
this.frm.doc.is_pos = 1;
|
||||
if (doctype == "Sales Invoice") this.frm.doc.is_created_using_pos = 1;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_new_frm(_frm) {
|
||||
const doctype = "POS Invoice";
|
||||
get_new_frm(_frm, doctype = this.settings.frm_doctype) {
|
||||
const page = $("<div>");
|
||||
const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
|
||||
const name = frappe.model.make_new_doc_and_get_name(doctype, true);
|
||||
@@ -567,12 +580,18 @@ erpnext.PointOfSale.Controller = class {
|
||||
return frm;
|
||||
}
|
||||
|
||||
sync_draft_invoice_to_frm(doctype, invoice) {
|
||||
return frappe.db.get_doc(doctype, invoice).then((doc) => {
|
||||
frappe.model.sync(doc);
|
||||
});
|
||||
}
|
||||
|
||||
async make_return_invoice(doc) {
|
||||
frappe.dom.freeze();
|
||||
this.frm = this.get_new_frm(this.frm);
|
||||
this.frm.doc.items = [];
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
||||
method:
|
||||
doc.doctype == "POS Invoice"
|
||||
? "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return"
|
||||
: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_sales_return",
|
||||
args: {
|
||||
source_name: doc.name,
|
||||
target_doc: this.frm.doc,
|
||||
@@ -580,9 +599,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
callback: (r) => {
|
||||
frappe.model.sync(r.message);
|
||||
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
|
||||
this.set_pos_profile_data().then(() => {
|
||||
frappe.dom.unfreeze();
|
||||
});
|
||||
this.set_pos_profile_data();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
await me.events.checkout();
|
||||
me.toggle_checkout_btn(false);
|
||||
me.disable_customer_selection();
|
||||
|
||||
me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
|
||||
});
|
||||
@@ -195,6 +196,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
this.$totals_section.on("click", ".edit-cart-btn", () => {
|
||||
this.events.edit_cart();
|
||||
this.toggle_checkout_btn(true);
|
||||
me.enable_customer_selection();
|
||||
});
|
||||
|
||||
this.$component.on("click", ".add-discount-wrapper", () => {
|
||||
@@ -207,6 +209,11 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
// called when discount is applied
|
||||
this.update_totals_section(frm);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", "paid_amount", (frm) => {
|
||||
// called when discount is applied
|
||||
this.update_totals_section(frm);
|
||||
});
|
||||
}
|
||||
|
||||
attach_shortcuts() {
|
||||
@@ -278,7 +285,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
toggle_item_highlight(item) {
|
||||
const $cart_item = $(item);
|
||||
const item_is_highlighted = $cart_item.attr("style") == "background-color:var(--gray-50);";
|
||||
const item_is_highlighted = $cart_item.attr("style") == "background-color: var(--control-bg);";
|
||||
|
||||
if (!item || item_is_highlighted) {
|
||||
this.item_is_selected = false;
|
||||
@@ -698,6 +705,25 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
}
|
||||
|
||||
disable_customer_selection() {
|
||||
this.$customer_section.find(".reset-customer-btn").css("visibility", "hidden");
|
||||
this.$customer_section.off("click", ".customer-display");
|
||||
this.$customer_section.off("click", ".reset-customer-btn");
|
||||
}
|
||||
|
||||
enable_customer_selection() {
|
||||
this.$customer_section.find(".reset-customer-btn").css("visibility", "visible");
|
||||
this.$customer_section.on("click", ".customer-display", (e) => {
|
||||
if ($(e.target).closest(".reset-customer-btn").length) return;
|
||||
|
||||
const show = this.$cart_container.is(":visible");
|
||||
this.toggle_customer_info(show);
|
||||
});
|
||||
this.$customer_section.on("click", ".reset-customer-btn", () => {
|
||||
this.reset_customer_selector();
|
||||
});
|
||||
}
|
||||
|
||||
highlight_checkout_btn(toggle) {
|
||||
if (toggle) {
|
||||
this.$add_discount_elem.css("display", "flex");
|
||||
@@ -968,13 +994,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
|
||||
fetch_customer_transactions() {
|
||||
frappe.db
|
||||
.get_list("POS Invoice", {
|
||||
filters: { customer: this.customer_info.customer, docstatus: 1 },
|
||||
fields: ["name", "grand_total", "status", "posting_date", "posting_time", "currency"],
|
||||
limit: 20,
|
||||
frappe
|
||||
.call({
|
||||
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_customer_recent_transactions",
|
||||
args: { customer: this.customer_info.customer },
|
||||
})
|
||||
.then((res) => {
|
||||
res = res.message;
|
||||
const transaction_container = this.$customer_section.find(".customer-transactions");
|
||||
|
||||
if (!res.length) {
|
||||
@@ -998,6 +1024,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
Draft: "red",
|
||||
Return: "gray",
|
||||
Consolidated: "blue",
|
||||
"Credit Note Issued": "gray",
|
||||
};
|
||||
|
||||
transaction_container.append(
|
||||
|
||||
@@ -6,6 +6,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
this.allow_rate_change = settings.allow_rate_change;
|
||||
this.allow_discount_change = settings.allow_discount_change;
|
||||
this.current_item = {};
|
||||
this.frm_doctype = settings.frm_doctype;
|
||||
|
||||
this.init_component();
|
||||
}
|
||||
@@ -323,7 +324,9 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
};
|
||||
}
|
||||
|
||||
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
|
||||
const frm_doctype = this.events.get_frm().doc.doctype;
|
||||
|
||||
frappe.model.on(`${frm_doctype} Item`, "*", (fieldname, value, item_row) => {
|
||||
const field_control = this[`${fieldname}_control`];
|
||||
const item_row_is_being_edited = this.compare_with_current_item(item_row);
|
||||
if (
|
||||
@@ -423,7 +426,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
warehouse: this.warehouse_control.get_value() || "",
|
||||
batch_nos: this.current_item.batch_no || "",
|
||||
posting_date: expiry_date,
|
||||
for_doctype: "POS Invoice",
|
||||
for_doctype: this.frm_doctype,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -104,11 +104,11 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
return `<div class="item-qty-pill">
|
||||
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center border-b-grey text-6xl text-grey-100" style="height:8rem; min-height:8rem">
|
||||
<div class="item-display">
|
||||
<img
|
||||
onerror="cur_pos.item_selector.handle_broken_image(this)"
|
||||
class="h-full item-img" src="${item_image}"
|
||||
alt="${frappe.get_abbr(item.item_name)}"
|
||||
class="item-img" src="${item_image}"
|
||||
alt="${item.item_name}"
|
||||
>
|
||||
</div>`;
|
||||
} else {
|
||||
|
||||
@@ -38,9 +38,10 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
});
|
||||
const me = this;
|
||||
this.$invoices_container.on("click", ".invoice-wrapper", function () {
|
||||
const invoice_doctype = $(this).attr("data-invoice-doctype");
|
||||
const invoice_name = unescape($(this).attr("data-invoice-name"));
|
||||
|
||||
me.events.open_invoice_data(invoice_name);
|
||||
me.events.open_invoice_data(invoice_doctype, invoice_name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,7 +100,9 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
const posting_datetime = frappe.datetime.str_to_user(
|
||||
invoice.posting_date + " " + invoice.posting_time
|
||||
);
|
||||
return `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
|
||||
return `<div class="invoice-wrapper" data-invoice-doctype="${
|
||||
invoice.doctype
|
||||
}" data-invoice-name="${escape(invoice.name)}">
|
||||
<div class="invoice-name-date">
|
||||
<div class="invoice-name">${invoice.name}</div>
|
||||
<div class="invoice-date">
|
||||
|
||||
@@ -117,9 +117,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
|
||||
async function get_returned_qty() {
|
||||
const r = await frappe.call({
|
||||
method: "erpnext.controllers.sales_and_purchase_return.get_pos_invoice_item_returned_qty",
|
||||
method: "erpnext.controllers.sales_and_purchase_return.get_invoice_item_returned_qty",
|
||||
args: {
|
||||
pos_invoice: doc.name,
|
||||
doctype: doc.doctype,
|
||||
invoice: doc.name,
|
||||
customer: doc.customer,
|
||||
item_row_name: item_data.name,
|
||||
},
|
||||
@@ -192,7 +193,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
|
||||
bind_events() {
|
||||
this.$summary_container.on("click", ".return-btn", async () => {
|
||||
const r = await this.is_pos_invoice_returnable(this.doc.name);
|
||||
const r = await this.is_invoice_returnable(this.doc.doctype, this.doc.name);
|
||||
if (!r) {
|
||||
frappe.msgprint({
|
||||
title: __("Invalid Return"),
|
||||
@@ -201,21 +202,21 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.events.process_return(this.doc.name);
|
||||
this.events.process_return(this.doc.doctype, this.doc.name);
|
||||
this.toggle_component(false);
|
||||
this.$component.find(".no-summary-placeholder").css("display", "flex");
|
||||
this.$summary_wrapper.css("display", "none");
|
||||
});
|
||||
|
||||
this.$summary_container.on("click", ".edit-btn", () => {
|
||||
this.events.edit_order(this.doc.name);
|
||||
this.events.edit_order(this.doc.doctype, this.doc.name);
|
||||
this.toggle_component(false);
|
||||
this.$component.find(".no-summary-placeholder").css("display", "flex");
|
||||
this.$summary_wrapper.css("display", "none");
|
||||
});
|
||||
|
||||
this.$summary_container.on("click", ".delete-btn", () => {
|
||||
this.events.delete_order(this.doc.name);
|
||||
this.events.delete_order(this.doc.doctype, this.doc.name);
|
||||
this.show_summary_placeholder();
|
||||
});
|
||||
|
||||
@@ -461,11 +462,12 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
|
||||
}
|
||||
|
||||
async is_pos_invoice_returnable(invoice) {
|
||||
async is_invoice_returnable(doctype, invoice) {
|
||||
const r = await frappe.call({
|
||||
method: "erpnext.controllers.sales_and_purchase_return.is_pos_invoice_returnable",
|
||||
method: "erpnext.controllers.sales_and_purchase_return.is_invoice_returnable",
|
||||
args: {
|
||||
pos_invoice: invoice,
|
||||
doctype: doctype,
|
||||
invoice: invoice,
|
||||
},
|
||||
});
|
||||
return r.message;
|
||||
|
||||
@@ -164,36 +164,12 @@ erpnext.PointOfSale.Payment = class {
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => {
|
||||
const contact = frm.doc.contact_mobile;
|
||||
const request_button = $(this.request_for_payment_field?.$input[0]);
|
||||
if (contact) {
|
||||
request_button.removeClass("btn-default").addClass("btn-primary");
|
||||
} else {
|
||||
request_button.removeClass("btn-primary").addClass("btn-default");
|
||||
}
|
||||
frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => {
|
||||
this.bind_coupon_code_event(frm);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => {
|
||||
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
|
||||
if (!frm.doc.ignore_pricing_rule) {
|
||||
frm.applying_pos_coupon_code = true;
|
||||
frappe.run_serially([
|
||||
() => (frm.doc.ignore_pricing_rule = 1),
|
||||
() => frm.trigger("ignore_pricing_rule"),
|
||||
() => (frm.doc.ignore_pricing_rule = 0),
|
||||
() => frm.trigger("apply_pricing_rule"),
|
||||
() => frm.save(),
|
||||
() => this.update_totals_section(frm.doc),
|
||||
() => (frm.applying_pos_coupon_code = false),
|
||||
]);
|
||||
} else if (frm.doc.ignore_pricing_rule) {
|
||||
frappe.show_alert({
|
||||
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
}
|
||||
frappe.ui.form.on("Sales Invoice", "coupon_code", (frm) => {
|
||||
this.bind_coupon_code_event(frm);
|
||||
});
|
||||
|
||||
this.setup_listener_for_payments();
|
||||
@@ -225,19 +201,19 @@ erpnext.PointOfSale.Payment = class {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Invoice", "paid_amount", (frm) => {
|
||||
this.update_totals_section(frm.doc);
|
||||
|
||||
// need to re calculate cash shortcuts after discount is applied
|
||||
const is_cash_shortcuts_invisible = !this.$payment_modes.find(".cash-shortcuts").is(":visible");
|
||||
this.attach_cash_shortcuts(frm.doc);
|
||||
!is_cash_shortcuts_invisible &&
|
||||
this.$payment_modes.find(".cash-shortcuts").css("display", "grid");
|
||||
this.render_payment_mode_dom();
|
||||
this.bind_paid_amount_event(frm);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Invoice", "loyalty_amount", (frm) => {
|
||||
const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
|
||||
this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency);
|
||||
this.bind_loyalty_amount_event(frm);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", "paid_amount", (frm) => {
|
||||
this.bind_paid_amount_event(frm);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", "loyalty_amount", (frm) => {
|
||||
this.bind_loyalty_amount_event(frm);
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => {
|
||||
@@ -250,6 +226,43 @@ erpnext.PointOfSale.Payment = class {
|
||||
});
|
||||
}
|
||||
|
||||
bind_coupon_code_event(frm) {
|
||||
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
|
||||
if (!frm.doc.ignore_pricing_rule) {
|
||||
frm.applying_pos_coupon_code = true;
|
||||
frappe.run_serially([
|
||||
() => (frm.doc.ignore_pricing_rule = 1),
|
||||
() => frm.trigger("ignore_pricing_rule"),
|
||||
() => (frm.doc.ignore_pricing_rule = 0),
|
||||
() => frm.trigger("apply_pricing_rule"),
|
||||
() => frm.save(),
|
||||
() => this.update_totals_section(frm.doc),
|
||||
() => (frm.applying_pos_coupon_code = false),
|
||||
]);
|
||||
} else if (frm.doc.ignore_pricing_rule) {
|
||||
frappe.show_alert({
|
||||
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bind_paid_amount_event(frm) {
|
||||
this.update_totals_section(frm.doc);
|
||||
|
||||
// need to re calculate cash shortcuts after discount is applied
|
||||
const is_cash_shortcuts_invisible = !this.$payment_modes.find(".cash-shortcuts").is(":visible");
|
||||
this.attach_cash_shortcuts(frm.doc);
|
||||
!is_cash_shortcuts_invisible && this.$payment_modes.find(".cash-shortcuts").css("display", "grid");
|
||||
this.render_payment_mode_dom();
|
||||
}
|
||||
|
||||
bind_loyalty_amount_event(frm) {
|
||||
const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
|
||||
this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency);
|
||||
}
|
||||
|
||||
setup_listener_for_payments() {
|
||||
frappe.realtime.on("process_phone_payment", (data) => {
|
||||
const doc = this.events.get_frm().doc;
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
field_map = {
|
||||
"Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
|
||||
"Contact": ["name", "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"],
|
||||
"Address": [
|
||||
"name",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"pincode",
|
||||
"city",
|
||||
"state",
|
||||
"pincode",
|
||||
"country",
|
||||
"is_primary_address",
|
||||
],
|
||||
@@ -29,6 +31,12 @@ def get_columns(filters):
|
||||
columns = [
|
||||
f"{party_type}:Link/{party_type}",
|
||||
f"{frappe.unscrub(str(party_type_value))}::150",
|
||||
{
|
||||
"label": _("Address"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Address",
|
||||
"hidden": 1,
|
||||
},
|
||||
"Address Line 1",
|
||||
"Address Line 2",
|
||||
"Postal Code",
|
||||
@@ -36,6 +44,7 @@ def get_columns(filters):
|
||||
"State",
|
||||
"Country",
|
||||
"Is Primary Address:Check",
|
||||
{"label": _("Contact"), "fieldtype": "Link", "options": "Contact", "hidden": 1},
|
||||
"First Name",
|
||||
"Last Name",
|
||||
"Phone",
|
||||
@@ -43,14 +52,10 @@ def get_columns(filters):
|
||||
"Email Id",
|
||||
"Is Primary Contact:Check",
|
||||
]
|
||||
if filters.get("party_type") == "Supplier" and frappe.db.get_single_value(
|
||||
"Buying Settings", "supp_master_name"
|
||||
) == ["Naming Series", "Auto Name"]:
|
||||
columns.insert(1, "Supplier Name:Data:150")
|
||||
if filters.get("party_type") == "Customer" and frappe.db.get_single_value(
|
||||
"Selling Settings", "cust_master_name"
|
||||
) == ["Naming Series", "Auto Name"]:
|
||||
columns.insert(1, "Customer Name:Data:150")
|
||||
|
||||
if should_add_party_name(party_type):
|
||||
columns.insert(2, f"{party_type} Name:Data:150")
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
@@ -72,6 +77,7 @@ def get_party_addresses_and_contact(party_type, party, party_group, filters):
|
||||
|
||||
if party:
|
||||
query_filters = {"name": party}
|
||||
|
||||
if filters.get("party_type") in ["Customer", "Supplier"]:
|
||||
field = filters.get("party_type").lower() + "_name"
|
||||
else:
|
||||
@@ -93,14 +99,18 @@ def get_party_addresses_and_contact(party_type, party, party_group, filters):
|
||||
party_details = get_party_details(party_type, party_list, "Address", party_details)
|
||||
party_details = get_party_details(party_type, party_list, "Contact", party_details)
|
||||
|
||||
add_party_name = should_add_party_name(party_type)
|
||||
|
||||
for party, details in party_details.items():
|
||||
addresses = details.get("address", [])
|
||||
contacts = details.get("contact", [])
|
||||
if not any([addresses, contacts]):
|
||||
result = [party]
|
||||
result.append(party_groups[party])
|
||||
if filters.get("party_type") in ["Customer", "Supplier"]:
|
||||
|
||||
if add_party_name:
|
||||
result.append(party_name_map[party])
|
||||
|
||||
result.extend(add_blank_columns_for("Contact"))
|
||||
result.extend(add_blank_columns_for("Address"))
|
||||
data.append(result)
|
||||
@@ -112,8 +122,10 @@ def get_party_addresses_and_contact(party_type, party, party_group, filters):
|
||||
for idx in range(0, max_length):
|
||||
result = [party]
|
||||
result.append(party_groups[party])
|
||||
if filters.get("party_type") in ["Customer", "Supplier"]:
|
||||
|
||||
if add_party_name:
|
||||
result.append(party_name_map[party])
|
||||
|
||||
address = addresses[idx] if idx < len(addresses) else add_blank_columns_for("Address")
|
||||
contact = contacts[idx] if idx < len(contacts) else add_blank_columns_for("Contact")
|
||||
result.extend(address)
|
||||
@@ -130,9 +142,11 @@ def get_party_details(party_type, party_list, doctype, party_details):
|
||||
fields = ["`tabDynamic Link`.link_name", *field_map.get(doctype, [])]
|
||||
|
||||
records = frappe.get_list(doctype, filters=filters, fields=fields, as_list=True)
|
||||
|
||||
for d in records:
|
||||
details = party_details.get(d[0])
|
||||
details.setdefault(frappe.scrub(doctype), []).append(d[1:])
|
||||
|
||||
return party_details
|
||||
|
||||
|
||||
@@ -151,3 +165,16 @@ def get_party_group(party_type):
|
||||
}
|
||||
|
||||
return group[party_type]
|
||||
|
||||
|
||||
def should_add_party_name(party_type):
|
||||
settings_map = {
|
||||
"Supplier": ("Buying Settings", "supp_master_name"),
|
||||
"Customer": ("Selling Settings", "cust_master_name"),
|
||||
}
|
||||
|
||||
if party_type in settings_map:
|
||||
doctype, fieldname = settings_map.get(party_type)
|
||||
return frappe.db.get_single_value(doctype, fieldname) in ["Naming Series", "Auto Name"]
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
from frappe import _
|
||||
|
||||
doctype_rule_map = {
|
||||
"Item": {"points": 5, "for_doc_event": "New"},
|
||||
"Customer": {"points": 5, "for_doc_event": "New"},
|
||||
"Supplier": {"points": 5, "for_doc_event": "New"},
|
||||
"Lead": {"points": 2, "for_doc_event": "New"},
|
||||
"Opportunity": {
|
||||
"points": 10,
|
||||
"for_doc_event": "Custom",
|
||||
"condition": 'doc.status=="Converted"',
|
||||
"rule_name": _("On Converting Opportunity"),
|
||||
"user_field": "converted_by",
|
||||
},
|
||||
"Sales Order": {
|
||||
"points": 10,
|
||||
"for_doc_event": "Submit",
|
||||
"rule_name": _("On Sales Order Submission"),
|
||||
"user_field": "modified_by",
|
||||
},
|
||||
"Purchase Order": {
|
||||
"points": 10,
|
||||
"for_doc_event": "Submit",
|
||||
"rule_name": _("On Purchase Order Submission"),
|
||||
"user_field": "modified_by",
|
||||
},
|
||||
"Task": {
|
||||
"points": 5,
|
||||
"condition": 'doc.status == "Completed"',
|
||||
"rule_name": _("On Task Completion"),
|
||||
"user_field": "completed_by",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_default_energy_point_rules():
|
||||
return [
|
||||
{
|
||||
"doctype": "Energy Point Rule",
|
||||
"reference_doctype": doctype,
|
||||
"for_doc_event": rule.get("for_doc_event") or "Custom",
|
||||
"condition": rule.get("condition"),
|
||||
"rule_name": rule.get("rule_name") or _("On {0} Creation").format(doctype),
|
||||
"points": rule.get("points"),
|
||||
"user_field": rule.get("user_field") or "owner",
|
||||
}
|
||||
for doctype, rule in doctype_rule_map.items()
|
||||
]
|
||||
@@ -281,6 +281,7 @@ class Company(NestedSet):
|
||||
frappe.clear_cache()
|
||||
|
||||
def create_default_warehouses(self):
|
||||
parent_warehouse = None
|
||||
for wh_detail in [
|
||||
{"warehouse_name": _("All Warehouses"), "is_group": 1},
|
||||
{"warehouse_name": _("Stores"), "is_group": 0},
|
||||
@@ -288,22 +289,31 @@ class Company(NestedSet):
|
||||
{"warehouse_name": _("Finished Goods"), "is_group": 0},
|
||||
{"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"},
|
||||
]:
|
||||
if not frappe.db.exists("Warehouse", "{} - {}".format(wh_detail["warehouse_name"], self.abbr)):
|
||||
warehouse = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Warehouse",
|
||||
"warehouse_name": wh_detail["warehouse_name"],
|
||||
"is_group": wh_detail["is_group"],
|
||||
"company": self.name,
|
||||
"parent_warehouse": "{} - {}".format(_("All Warehouses"), self.abbr)
|
||||
if not wh_detail["is_group"]
|
||||
else "",
|
||||
"warehouse_type": wh_detail.get("warehouse_type"),
|
||||
}
|
||||
)
|
||||
warehouse.flags.ignore_permissions = True
|
||||
warehouse.flags.ignore_mandatory = True
|
||||
warehouse.insert()
|
||||
if frappe.db.exists(
|
||||
"Warehouse",
|
||||
{
|
||||
"warehouse_name": wh_detail["warehouse_name"],
|
||||
"company": self.name,
|
||||
},
|
||||
):
|
||||
continue
|
||||
|
||||
warehouse = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Warehouse",
|
||||
"warehouse_name": wh_detail["warehouse_name"],
|
||||
"is_group": wh_detail["is_group"],
|
||||
"company": self.name,
|
||||
"parent_warehouse": parent_warehouse,
|
||||
"warehouse_type": wh_detail.get("warehouse_type"),
|
||||
}
|
||||
)
|
||||
warehouse.flags.ignore_permissions = True
|
||||
warehouse.flags.ignore_mandatory = True
|
||||
warehouse.insert()
|
||||
|
||||
if wh_detail["is_group"]:
|
||||
parent_warehouse = warehouse.name
|
||||
|
||||
def create_default_accounts(self):
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
|
||||
|
||||
@@ -8,7 +8,6 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
|
||||
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
|
||||
|
||||
from .default_success_action import get_default_success_action
|
||||
@@ -26,7 +25,6 @@ def after_install():
|
||||
create_marketgin_campagin_custom_fields()
|
||||
add_all_roles_to("Administrator")
|
||||
create_default_success_action()
|
||||
create_default_energy_point_rules()
|
||||
create_incoterms()
|
||||
create_default_role_profiles()
|
||||
add_company_to_session_defaults()
|
||||
@@ -147,18 +145,6 @@ def create_default_success_action():
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def create_default_energy_point_rules():
|
||||
for rule in get_default_energy_point_rules():
|
||||
# check if any rule for ref. doctype exists
|
||||
rule_exists = frappe.db.exists(
|
||||
"Energy Point Rule", {"reference_doctype": rule.get("reference_doctype")}
|
||||
)
|
||||
if rule_exists:
|
||||
continue
|
||||
doc = frappe.get_doc(rule)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def add_company_to_session_defaults():
|
||||
settings = frappe.get_single("Session Default Settings")
|
||||
settings.append("session_defaults", {"ref_doctype": "Company"})
|
||||
|
||||
@@ -2,5 +2,20 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bin", {
|
||||
refresh: function (frm) {},
|
||||
refresh(frm) {
|
||||
frm.trigger("recalculate_bin_quantity");
|
||||
},
|
||||
|
||||
recalculate_bin_quantity(frm) {
|
||||
frm.add_custom_button(__("Recalculate Bin Qty"), () => {
|
||||
frappe.call({
|
||||
method: "recalculate_qty",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Bin Qty Recalculated"), 2);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user