mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-28 19:18:32 +00:00
feat: sales invoice integration with pos (#46485)
* feat: pos configuration to activate real time update of gl and stock ledger * feat: sales invoice on pos order list * fix: syntax * feat: past order list with sales invoice * feat: customer recent transaction with sales invoice * fix: real_time_update validation added a validation to restrict switching between sales invoice and pos invoice in case there's already a pos opening entry * fix: use sales invoice on accounts settings moved the check to use sales invoice instead of pos invoice from pos profile to accounts settings. * fix: using accounts settings to get frm doctype * fix: added support for sales invoice in process return * feat: event listeners for sales invoice on pos * fix: create and load return invoice * fix: edit order * refactor: function rename * fix: sales invoice generation using pos added fields to distinguish sales invoice generated using pos * feat: credit note in pos invoice during sales invoice mode * feat: pos closing entry support for sales invoices * refactor: resolving linter issues * refactor: fix linter issue * fix: filters for sales invoice in toggle recent orders * feat: disable partial payments on sales invoice transactions made using pos * fix: resolve import error * fix: recent order list and pos invoice returns during sales invoice mode * fix: reset pos_closing_entry on return sales invoice * fix: filtering out consolidated return sales invoice for pos invoice return * fix: pos delete order * fix: added missing reference to consolidated sales invoice item * fix: added check to restrict sales invoice creation * refactor: variable name * fix: integrating sales_invoice in make_closing_entry_from_opening method * test: test for sales invoice integration in pos * fix: issue with accounting dimension on sales invoice * refactor: moved invoice switching mode validation in backend * test: removed test case Removed Test Case for Full Payment of Sales Invoice created using POS as planning to add feature to accept Partial Payment from POS. * test: fixed the failing tests * test: remove explicit use of frappe.db.commit() * test: fixing pos invoice test * test: removed test
This commit is contained in:
@@ -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"),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 +15,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 +238,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:
|
||||
@@ -633,26 +633,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
|
||||
|
||||
@@ -417,6 +417,7 @@
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 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() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -387,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", [])
|
||||
@@ -1179,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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -455,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);
|
||||
});
|
||||
},
|
||||
@@ -472,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(),
|
||||
@@ -495,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: () => {
|
||||
@@ -529,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(),
|
||||
@@ -537,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);
|
||||
@@ -566,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,
|
||||
@@ -579,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();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,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() {
|
||||
@@ -989,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) {
|
||||
@@ -1019,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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user