mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-31 10:49:09 +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_tab",
|
||||||
"pos_setting_section",
|
"pos_setting_section",
|
||||||
"post_change_gl_entries",
|
"post_change_gl_entries",
|
||||||
|
"column_break_xrnd",
|
||||||
|
"use_sales_invoice_in_pos",
|
||||||
"assets_tab",
|
"assets_tab",
|
||||||
"asset_settings_section",
|
"asset_settings_section",
|
||||||
"calculate_depr_using_total_days",
|
"calculate_depr_using_total_days",
|
||||||
@@ -532,14 +534,26 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||||
"options": "Invoice\nPayment\nReconciliation Date"
|
"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",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-01-23 13:15:44.077853",
|
"modified": "2025-03-30 20:47:17.954736",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
@@ -564,8 +578,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class AccountsSettings(Document):
|
|||||||
submit_journal_entries: DF.Check
|
submit_journal_entries: DF.Check
|
||||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||||
|
use_sales_invoice_in_pos: DF.Check
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -92,6 +93,9 @@ class AccountsSettings(Document):
|
|||||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||||
self.validate_pending_reposts()
|
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:
|
if clear_cache:
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
||||||
@@ -135,3 +139,15 @@ class AccountsSettings(Document):
|
|||||||
if self.has_value_changed("reconciliation_queue_size"):
|
if self.has_value_changed("reconciliation_queue_size"):
|
||||||
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
||||||
frappe.throw(_("Queue Size should be between 5 and 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
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("POS Closing Entry", {
|
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.ignore_doctypes_on_cancel_all = ["POS Invoice Merge Log"];
|
||||||
frm.set_query("pos_profile", function (doc) {
|
frm.set_query("pos_profile", function (doc) {
|
||||||
return {
|
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);
|
set_html_data(frm);
|
||||||
|
|
||||||
if (frm.doc.docstatus == 1) {
|
if (frm.doc.docstatus == 1) {
|
||||||
@@ -83,6 +92,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
|||||||
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
|
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
|
||||||
() => frm.trigger("set_opening_amounts"),
|
() => frm.trigger("set_opening_amounts"),
|
||||||
() => frm.trigger("get_pos_invoices"),
|
() => frm.trigger("get_pos_invoices"),
|
||||||
|
() => frm.trigger("get_sales_invoices"),
|
||||||
() => frappe.dom.unfreeze(),
|
() => frappe.dom.unfreeze(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -113,7 +123,25 @@ frappe.ui.form.on("POS Closing Entry", {
|
|||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
let pos_docs = r.message;
|
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);
|
refresh_fields(frm);
|
||||||
set_html_data(frm);
|
set_html_data(frm);
|
||||||
},
|
},
|
||||||
@@ -132,9 +160,40 @@ frappe.ui.form.on("POS Closing Entry", {
|
|||||||
row.expected_amount = row.opening_amount;
|
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([
|
await Promise.all([
|
||||||
frappe.call({
|
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: {
|
args: {
|
||||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||||
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_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,
|
user: frm.doc.user,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
let pos_invoices = r.message;
|
let sales_invoices = r.message;
|
||||||
for (let doc of pos_invoices) {
|
for (let doc of sales_invoices) {
|
||||||
frm.doc.grand_total += flt(doc.grand_total);
|
frm.doc.grand_total += flt(doc.grand_total);
|
||||||
frm.doc.net_total += flt(doc.net_total);
|
frm.doc.net_total += flt(doc.net_total);
|
||||||
frm.doc.total_quantity += flt(doc.total_qty);
|
frm.doc.total_quantity += flt(doc.total_qty);
|
||||||
@@ -155,6 +214,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
frappe.dom.unfreeze();
|
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) => {
|
data.forEach((d) => {
|
||||||
add_to_pos_transaction(d, frm);
|
add_to_pos_transaction(d, frm);
|
||||||
frm.doc.grand_total += flt(d.grand_total);
|
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) {
|
function add_to_pos_transaction(d, frm) {
|
||||||
frm.add_child("pos_transactions", {
|
frm.add_child("pos_transactions", {
|
||||||
pos_invoice: d.name,
|
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) {
|
function refresh_payments(d, frm, is_new) {
|
||||||
d.payments.forEach((p) => {
|
d.payments.forEach((p) => {
|
||||||
const payment = frm.doc.payment_reconciliation.find(
|
const payment = frm.doc.payment_reconciliation.find(
|
||||||
@@ -226,6 +306,7 @@ function refresh_taxes(d, frm) {
|
|||||||
|
|
||||||
function reset_values(frm) {
|
function reset_values(frm) {
|
||||||
frm.set_value("pos_transactions", []);
|
frm.set_value("pos_transactions", []);
|
||||||
|
frm.set_value("sales_invoice_transactions", []);
|
||||||
frm.set_value("payment_reconciliation", []);
|
frm.set_value("payment_reconciliation", []);
|
||||||
frm.set_value("taxes", []);
|
frm.set_value("taxes", []);
|
||||||
frm.set_value("grand_total", 0);
|
frm.set_value("grand_total", 0);
|
||||||
@@ -235,6 +316,7 @@ function reset_values(frm) {
|
|||||||
|
|
||||||
function refresh_fields(frm) {
|
function refresh_fields(frm) {
|
||||||
frm.refresh_field("pos_transactions");
|
frm.refresh_field("pos_transactions");
|
||||||
|
frm.refresh_field("sales_invoice_transactions");
|
||||||
frm.refresh_field("payment_reconciliation");
|
frm.refresh_field("payment_reconciliation");
|
||||||
frm.refresh_field("taxes");
|
frm.refresh_field("taxes");
|
||||||
frm.refresh_field("grand_total");
|
frm.refresh_field("grand_total");
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"user",
|
"user",
|
||||||
"section_break_12",
|
"section_break_12",
|
||||||
"pos_transactions",
|
"pos_transactions",
|
||||||
|
"sales_invoice_transactions",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
"payment_reconciliation_details",
|
"payment_reconciliation_details",
|
||||||
"section_break_11",
|
"section_break_11",
|
||||||
@@ -227,8 +228,15 @@
|
|||||||
"label": "Posting Time",
|
"label": "Posting Time",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_invoice_transactions",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Sales Invoice Transactions",
|
||||||
|
"options": "Sales Invoice Reference"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
@@ -236,7 +244,7 @@
|
|||||||
"link_fieldname": "pos_closing_entry"
|
"link_fieldname": "pos_closing_entry"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-03-27 13:10:14.073467",
|
"modified": "2025-03-19 19:49:58.845697",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Closing Entry",
|
"name": "POS Closing Entry",
|
||||||
@@ -285,8 +293,9 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
from erpnext.accounts.doctype.pos_closing_entry_taxes.pos_closing_entry_taxes import (
|
from erpnext.accounts.doctype.pos_closing_entry_taxes.pos_closing_entry_taxes import (
|
||||||
POSClosingEntryTaxes,
|
POSClosingEntryTaxes,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
|
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
|
||||||
POSInvoiceReference,
|
from erpnext.accounts.doctype.sales_invoice_reference.sales_invoice_reference import (
|
||||||
|
SalesInvoiceReference,
|
||||||
)
|
)
|
||||||
|
|
||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
@@ -45,6 +46,7 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
pos_transactions: DF.Table[POSInvoiceReference]
|
pos_transactions: DF.Table[POSInvoiceReference]
|
||||||
posting_date: DF.Date
|
posting_date: DF.Date
|
||||||
posting_time: DF.Time
|
posting_time: DF.Time
|
||||||
|
sales_invoice_transactions: DF.Table[SalesInvoiceReference]
|
||||||
status: DF.Literal["Draft", "Submitted", "Queued", "Failed", "Cancelled"]
|
status: DF.Literal["Draft", "Submitted", "Queued", "Failed", "Cancelled"]
|
||||||
taxes: DF.Table[POSClosingEntryTaxes]
|
taxes: DF.Table[POSClosingEntryTaxes]
|
||||||
total_quantity: DF.Float
|
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":
|
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"))
|
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||||
|
|
||||||
self.validate_duplicate_pos_invoices()
|
self.is_pos_using_sales_invoice = frappe.db.get_single_value(
|
||||||
self.validate_pos_invoices()
|
"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):
|
def validate_duplicate_pos_invoices(self):
|
||||||
pos_occurences = {}
|
pos_occurences = {}
|
||||||
@@ -114,6 +128,71 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
|
|
||||||
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
|
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()
|
@frappe.whitelist()
|
||||||
def get_payment_reconciliation_details(self):
|
def get_payment_reconciliation_details(self):
|
||||||
currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
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}",
|
docname=f"POS Opening Entry/{self.pos_opening_entry}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.update_sales_invoices_closing_entry()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
unconsolidate_pos_invoices(closing_entry=self)
|
unconsolidate_pos_invoices(closing_entry=self)
|
||||||
|
|
||||||
|
self.update_sales_invoices_closing_entry(cancel=True)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def retry(self):
|
def retry(self):
|
||||||
consolidate_pos_invoices(closing_entry=self)
|
consolidate_pos_invoices(closing_entry=self)
|
||||||
@@ -143,6 +226,12 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
opening_entry.set_status()
|
opening_entry.set_status()
|
||||||
opening_entry.save()
|
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.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
@@ -173,6 +262,33 @@ def get_pos_invoices(start, end, pos_profile, user):
|
|||||||
return data
|
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):
|
def make_closing_entry_from_opening(opening_entry):
|
||||||
closing_entry = frappe.new_doc("POS Closing Entry")
|
closing_entry = frappe.new_doc("POS Closing Entry")
|
||||||
closing_entry.pos_opening_entry = opening_entry.name
|
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.net_total = 0
|
||||||
closing_entry.total_quantity = 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_start_date,
|
||||||
closing_entry.period_end_date,
|
closing_entry.period_end_date,
|
||||||
closing_entry.pos_profile,
|
closing_entry.pos_profile,
|
||||||
@@ -193,6 +322,7 @@ def make_closing_entry_from_opening(opening_entry):
|
|||||||
)
|
)
|
||||||
|
|
||||||
pos_transactions = []
|
pos_transactions = []
|
||||||
|
sales_invoice_transactions = []
|
||||||
taxes = []
|
taxes = []
|
||||||
payments = []
|
payments = []
|
||||||
for detail in opening_entry.balance_details:
|
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(
|
pos_transactions.append(
|
||||||
frappe._dict(
|
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.grand_total += flt(d.grand_total)
|
||||||
closing_entry.net_total += flt(d.net_total)
|
closing_entry.net_total += flt(d.net_total)
|
||||||
closing_entry.total_quantity += flt(d.total_qty)
|
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("pos_transactions", pos_transactions)
|
||||||
|
closing_entry.set("sales_invoice_transactions", sales_invoice_transactions)
|
||||||
closing_entry.set("payment_reconciliation", payments)
|
closing_entry.set("payment_reconciliation", payments)
|
||||||
closing_entry.set("taxes", taxes)
|
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)
|
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
|
||||||
self.assertEqual(batch_qty_with_pos, 10.0)
|
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):
|
def init_user_and_profile(**args):
|
||||||
user = "test@example.com"
|
user = "test@example.com"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
|
from frappe.model.mapper import map_child_doc, map_doc
|
||||||
from frappe.query_builder.functions import IfNull, Sum
|
from frappe.query_builder.functions import IfNull, Sum
|
||||||
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
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.accounts.party import get_due_date, get_party_account
|
||||||
from erpnext.controllers.queries import item_query as _item_query
|
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
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
|
||||||
class PartialPaymentValidationError(frappe.ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class POSInvoice(SalesInvoice):
|
class POSInvoice(SalesInvoice):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
# This code is auto-generated. Do not modify anything in this block.
|
# 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
|
# run on validate method of selling controller
|
||||||
super(SalesInvoice, self).validate()
|
super(SalesInvoice, self).validate()
|
||||||
self.validate_pos_opening_entry()
|
self.validate_pos_opening_entry()
|
||||||
|
self.validate_is_pos_using_sales_invoice()
|
||||||
self.validate_auto_set_posting_time()
|
self.validate_auto_set_posting_time()
|
||||||
self.validate_mode_of_payment()
|
self.validate_mode_of_payment()
|
||||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
@@ -244,6 +243,9 @@ class POSInvoice(SalesInvoice):
|
|||||||
update_coupon_code_count(self.coupon_code, "used")
|
update_coupon_code_count(self.coupon_code, "used")
|
||||||
self.clear_unallocated_mode_of_payments()
|
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):
|
def before_cancel(self):
|
||||||
if (
|
if (
|
||||||
self.consolidated_invoice
|
self.consolidated_invoice
|
||||||
@@ -287,6 +289,47 @@ class POSInvoice(SalesInvoice):
|
|||||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||||
frappe.qb.from_(sip).delete().where(sip.parent == self.name).where(sip.amount == 0).run()
|
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):
|
def delink_serial_and_batch_bundle(self):
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
if row.serial_and_batch_bundle:
|
if row.serial_and_batch_bundle:
|
||||||
@@ -378,6 +421,13 @@ class POSInvoice(SalesInvoice):
|
|||||||
title=_("Item Unavailable"),
|
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):
|
def validate_serialised_or_batched_item(self):
|
||||||
error_msg = []
|
error_msg = []
|
||||||
for d in self.get("items"):
|
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:
|
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
|
||||||
validate_loyalty_points(self, 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):
|
def set_status(self, update=False, status=None, update_modified=True):
|
||||||
if self.is_new():
|
if self.is_new():
|
||||||
if self.get("amended_from"):
|
if self.get("amended_from"):
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.tests import IntegrationTestCase
|
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.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.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.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import map_child_doc, map_doc
|
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 import cint, flt, get_time, getdate, nowdate, nowtime
|
||||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||||
from frappe.utils.scheduler import is_scheduler_inactive
|
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 (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_checks_for_pl_and_bs_accounts,
|
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
|
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 = doc.name
|
||||||
si_item.pos_invoice_item = item.name
|
si_item.pos_invoice_item = item.name
|
||||||
if doc.is_return:
|
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
|
doc.return_against, item.pos_invoice_item
|
||||||
)
|
)
|
||||||
if item.serial_and_batch_bundle:
|
if item.serial_and_batch_bundle:
|
||||||
@@ -633,26 +633,3 @@ def get_error_message(message) -> str:
|
|||||||
return message["message"]
|
return message["message"]
|
||||||
except Exception:
|
except Exception:
|
||||||
return str(message)
|
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"
|
"options": "Project"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 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);
|
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() {
|
make_invoice_discounting() {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@
|
|||||||
"update_billed_amount_in_delivery_note",
|
"update_billed_amount_in_delivery_note",
|
||||||
"is_debit_note",
|
"is_debit_note",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
|
"is_created_using_pos",
|
||||||
|
"pos_closing_entry",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"dimension_col_break",
|
"dimension_col_break",
|
||||||
@@ -2199,6 +2201,23 @@
|
|||||||
"label": "Company Contact Person",
|
"label": "Company Contact Person",
|
||||||
"options": "Contact",
|
"options": "Contact",
|
||||||
"print_hide": 1
|
"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,
|
"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"}
|
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||||
|
|
||||||
|
|
||||||
|
class PartialPaymentValidationError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SalesInvoice(SellingController):
|
class SalesInvoice(SellingController):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
# This code is auto-generated. Do not modify anything in this block.
|
# 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
|
inter_company_invoice_reference: DF.Link | None
|
||||||
is_cash_or_non_trade_discount: DF.Check
|
is_cash_or_non_trade_discount: DF.Check
|
||||||
is_consolidated: DF.Check
|
is_consolidated: DF.Check
|
||||||
|
is_created_using_pos: DF.Check
|
||||||
is_debit_note: DF.Check
|
is_debit_note: DF.Check
|
||||||
is_discounted: DF.Check
|
is_discounted: DF.Check
|
||||||
is_internal_customer: DF.Check
|
is_internal_customer: DF.Check
|
||||||
@@ -162,6 +167,7 @@ class SalesInvoice(SellingController):
|
|||||||
plc_conversion_rate: DF.Float
|
plc_conversion_rate: DF.Float
|
||||||
po_date: DF.Date | None
|
po_date: DF.Date | None
|
||||||
po_no: DF.Data | None
|
po_no: DF.Data | None
|
||||||
|
pos_closing_entry: DF.Link | None
|
||||||
pos_profile: DF.Link | None
|
pos_profile: DF.Link | None
|
||||||
posting_date: DF.Date
|
posting_date: DF.Date
|
||||||
posting_time: DF.Time | None
|
posting_time: DF.Time | None
|
||||||
@@ -306,6 +312,10 @@ class SalesInvoice(SellingController):
|
|||||||
if cint(self.is_pos):
|
if cint(self.is_pos):
|
||||||
self.validate_pos()
|
self.validate_pos()
|
||||||
|
|
||||||
|
if cint(self.is_created_using_pos):
|
||||||
|
self.validate_created_using_pos()
|
||||||
|
self.validate_full_payment()
|
||||||
|
|
||||||
self.validate_dropship_item()
|
self.validate_dropship_item()
|
||||||
|
|
||||||
if cint(self.update_stock):
|
if cint(self.update_stock):
|
||||||
@@ -528,7 +538,22 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
frappe.throw(msg, title=_("Not Allowed"))
|
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):
|
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()
|
self.check_if_consolidated_invoice()
|
||||||
|
|
||||||
super().before_cancel()
|
super().before_cancel()
|
||||||
@@ -598,6 +623,15 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
self.delete_auto_created_batches()
|
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):
|
def update_status_updater_args(self):
|
||||||
if not cint(self.update_stock):
|
if not cint(self.update_stock):
|
||||||
return
|
return
|
||||||
@@ -669,6 +703,15 @@ class SalesInvoice(SellingController):
|
|||||||
timesheet.flags.ignore_validate_update_after_submit = True
|
timesheet.flags.ignore_validate_update_after_submit = True
|
||||||
timesheet.db_update_all()
|
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()
|
@frappe.whitelist()
|
||||||
def set_missing_values(self, for_validate=False):
|
def set_missing_values(self, for_validate=False):
|
||||||
pos = self.set_pos_fields(for_validate)
|
pos = self.set_pos_fields(for_validate)
|
||||||
@@ -704,6 +747,13 @@ class SalesInvoice(SellingController):
|
|||||||
"allow_print_before_pay": pos.get("allow_print_before_pay"),
|
"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):
|
def update_time_sheet(self, sales_invoice):
|
||||||
for d in self.timesheets:
|
for d in self.timesheets:
|
||||||
if d.time_sheet:
|
if d.time_sheet:
|
||||||
@@ -1025,6 +1075,32 @@ class SalesInvoice(SellingController):
|
|||||||
) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
|
) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)):
|
||||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
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):
|
def validate_warehouse(self):
|
||||||
super().validate_warehouse()
|
super().validate_warehouse()
|
||||||
|
|
||||||
|
|||||||
@@ -4386,6 +4386,27 @@ class TestSalesInvoice(IntegrationTestCase):
|
|||||||
|
|
||||||
self.assertRaises(StockOverReturnError, return_doc.save)
|
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):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
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
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
|
from frappe.query_builder import DocType
|
||||||
from frappe.utils import cint, flt, format_datetime, get_datetime
|
from frappe.utils import cint, flt, format_datetime, get_datetime
|
||||||
|
|
||||||
import erpnext
|
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.get("is_return"):
|
||||||
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
|
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
|
||||||
doc.consolidated_invoice = ""
|
doc.consolidated_invoice = ""
|
||||||
|
if doc.doctype == "Sales Invoice":
|
||||||
|
doc.pos_closing_entry = ""
|
||||||
# no copy enabled for party_account_currency
|
# no copy enabled for party_account_currency
|
||||||
doc.party_account_currency = source.party_account_currency
|
doc.party_account_currency = source.party_account_currency
|
||||||
doc.set("payments", [])
|
doc.set("payments", [])
|
||||||
@@ -1179,26 +1182,49 @@ def get_payment_data(invoice):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pos_invoice_item_returned_qty(pos_invoice, customer, item_row_name):
|
def get_invoice_item_returned_qty(doctype, invoice, customer, item_row_name):
|
||||||
is_return, docstatus = frappe.db.get_value("POS Invoice", pos_invoice, ["is_return", "docstatus"])
|
is_return, docstatus = frappe.db.get_value(doctype, invoice, ["is_return", "docstatus"])
|
||||||
if not is_return and docstatus == 1:
|
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()
|
@frappe.whitelist()
|
||||||
def is_pos_invoice_returnable(pos_invoice):
|
def is_invoice_returnable(doctype, invoice):
|
||||||
is_return, docstatus, customer = frappe.db.get_value(
|
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:
|
if is_return or docstatus == 0:
|
||||||
return False
|
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
|
already_full_returned = 0
|
||||||
for d in invoice_item_qty:
|
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:
|
if returned_qty.qty == d.qty:
|
||||||
already_full_returned += 1
|
already_full_returned += 1
|
||||||
|
|
||||||
return len(invoice_item_qty) != already_full_returned
|
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 json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint, get_datetime
|
||||||
from frappe.utils.nestedset import get_root_of
|
from frappe.utils.nestedset import get_root_of
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability
|
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 = []
|
invoice_list = []
|
||||||
|
|
||||||
if search_term and status:
|
if search_term and status:
|
||||||
invoices_by_customer = frappe.db.get_list(
|
pos_invoices_by_customer = frappe.db.get_list(
|
||||||
"POS Invoice",
|
"POS Invoice",
|
||||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
filters=get_invoice_filters("POS Invoice", status, customer=search_term),
|
||||||
fields=fields,
|
fields=fields,
|
||||||
page_length=limit,
|
page_length=limit,
|
||||||
)
|
)
|
||||||
invoices_by_name = frappe.db.get_list(
|
pos_invoices_by_name = frappe.db.get_list(
|
||||||
"POS Invoice",
|
"POS Invoice",
|
||||||
filters={"name": ["like", f"%{search_term}%"], "status": status},
|
filters=get_invoice_filters("POS Invoice", status, name=search_term),
|
||||||
fields=fields,
|
fields=fields,
|
||||||
page_length=limit,
|
page_length=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
invoice_list = invoices_by_customer + invoices_by_name
|
pos_invoice_list = add_doctype_to_results(
|
||||||
elif status:
|
"POS Invoice", pos_invoices_by_customer + pos_invoices_by_name
|
||||||
invoice_list = frappe.db.get_list(
|
|
||||||
"POS Invoice", filters={"status": status}, fields=fields, page_length=limit
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
return invoice_list
|
||||||
|
|
||||||
|
|
||||||
@@ -402,3 +436,68 @@ def get_pos_profile_data(pos_profile):
|
|||||||
|
|
||||||
pos_profile.customer_groups = _customer_groups_with_children
|
pos_profile.customer_groups = _customer_groups_with_children
|
||||||
return pos_profile
|
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;
|
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({
|
frappe.call({
|
||||||
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
|
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
|
||||||
args: { pos_profile: this.pos_profile },
|
args: { pos_profile: this.pos_profile },
|
||||||
@@ -146,6 +151,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
const profile = res.message;
|
const profile = res.message;
|
||||||
Object.assign(this.settings, profile);
|
Object.assign(this.settings, profile);
|
||||||
this.settings.customer_groups = profile.customer_groups.map((group) => group.name);
|
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();
|
this.make_app();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -455,8 +461,9 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
this.recent_order_list = new erpnext.PointOfSale.PastOrderList({
|
this.recent_order_list = new erpnext.PointOfSale.PastOrderList({
|
||||||
wrapper: this.$components_wrapper,
|
wrapper: this.$components_wrapper,
|
||||||
events: {
|
events: {
|
||||||
open_invoice_data: (name) => {
|
open_invoice_data: (doctype, name) => {
|
||||||
frappe.db.get_doc("POS Invoice", name).then((doc) => {
|
if (!["POS Invoice", "Sales Invoice"].includes(doctype)) return;
|
||||||
|
frappe.db.get_doc(doctype, name).then((doc) => {
|
||||||
this.order_summary.load_summary_of(doc);
|
this.order_summary.load_summary_of(doc);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -472,21 +479,26 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
events: {
|
events: {
|
||||||
get_frm: () => this.frm,
|
get_frm: () => this.frm,
|
||||||
|
|
||||||
process_return: (name) => {
|
process_return: (doctype, name) => {
|
||||||
this.recent_order_list.toggle_component(false);
|
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.run_serially([
|
||||||
|
() => frappe.dom.freeze(),
|
||||||
|
() => this.make_invoice_frm(doc.doctype),
|
||||||
() => this.make_return_invoice(doc),
|
() => this.make_return_invoice(doc),
|
||||||
() => this.cart.load_invoice(),
|
() => this.cart.load_invoice(),
|
||||||
() => this.item_selector.toggle_component(true),
|
() => this.item_selector.toggle_component(true),
|
||||||
() => this.item_selector.resize_selector(false),
|
() => this.item_selector.resize_selector(false),
|
||||||
() => this.item_details.toggle_component(false),
|
() => this.item_details.toggle_component(false),
|
||||||
|
() => frappe.dom.unfreeze(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
edit_order: (name) => {
|
edit_order: (doctype, name) => {
|
||||||
this.recent_order_list.toggle_component(false);
|
this.recent_order_list.toggle_component(false);
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
|
() => this.make_invoice_frm(doctype),
|
||||||
|
() => this.sync_draft_invoice_to_frm(doctype, name),
|
||||||
() => this.frm.refresh(name),
|
() => this.frm.refresh(name),
|
||||||
() => this.frm.call("reset_mode_of_payments"),
|
() => this.frm.call("reset_mode_of_payments"),
|
||||||
() => this.cart.load_invoice(),
|
() => this.cart.load_invoice(),
|
||||||
@@ -495,9 +507,11 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
() => this.item_details.toggle_component(false),
|
() => this.item_details.toggle_component(false),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
delete_order: (name) => {
|
delete_order: (doctype, name) => {
|
||||||
frappe.model.delete_doc(this.frm.doc.doctype, name, () => {
|
frappe.model.with_doctype(doctype, () => {
|
||||||
this.recent_order_list.refresh_list();
|
frappe.model.delete_doc(doctype, name, () => {
|
||||||
|
this.recent_order_list.refresh_list();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
new_order: () => {
|
new_order: () => {
|
||||||
@@ -529,7 +543,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
make_new_invoice() {
|
make_new_invoice() {
|
||||||
return frappe.run_serially([
|
return frappe.run_serially([
|
||||||
() => frappe.dom.freeze(),
|
() => 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_data(),
|
||||||
() => this.set_pos_profile_status(),
|
() => this.set_pos_profile_status(),
|
||||||
() => this.cart.load_invoice(),
|
() => this.cart.load_invoice(),
|
||||||
@@ -537,27 +551,27 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
make_sales_invoice_frm() {
|
make_invoice_frm(doctype) {
|
||||||
const doctype = "POS Invoice";
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.frm) {
|
if (this.frm && this.frm.doctype == doctype) {
|
||||||
this.frm = this.get_new_frm(this.frm);
|
this.frm = this.get_new_frm(this.frm, doctype);
|
||||||
this.frm.doc.items = [];
|
this.frm.doc.items = [];
|
||||||
this.frm.doc.is_pos = 1;
|
this.frm.doc.is_pos = 1;
|
||||||
|
if (doctype == "Sales Invoice") this.frm.doc.is_created_using_pos = 1;
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
frappe.model.with_doctype(doctype, () => {
|
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.items = [];
|
||||||
this.frm.doc.is_pos = 1;
|
this.frm.doc.is_pos = 1;
|
||||||
|
if (doctype == "Sales Invoice") this.frm.doc.is_created_using_pos = 1;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get_new_frm(_frm) {
|
get_new_frm(_frm, doctype = this.settings.frm_doctype) {
|
||||||
const doctype = "POS Invoice";
|
|
||||||
const page = $("<div>");
|
const page = $("<div>");
|
||||||
const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
|
const frm = _frm || new frappe.ui.form.Form(doctype, page, false);
|
||||||
const name = frappe.model.make_new_doc_and_get_name(doctype, true);
|
const name = frappe.model.make_new_doc_and_get_name(doctype, true);
|
||||||
@@ -566,12 +580,18 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
return frm;
|
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) {
|
async make_return_invoice(doc) {
|
||||||
frappe.dom.freeze();
|
|
||||||
this.frm = this.get_new_frm(this.frm);
|
|
||||||
this.frm.doc.items = [];
|
|
||||||
return frappe.call({
|
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: {
|
args: {
|
||||||
source_name: doc.name,
|
source_name: doc.name,
|
||||||
target_doc: this.frm.doc,
|
target_doc: this.frm.doc,
|
||||||
@@ -579,9 +599,7 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
frappe.model.sync(r.message);
|
frappe.model.sync(r.message);
|
||||||
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
|
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false;
|
||||||
this.set_pos_profile_data().then(() => {
|
this.set_pos_profile_data();
|
||||||
frappe.dom.unfreeze();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,11 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
// called when discount is applied
|
// called when discount is applied
|
||||||
this.update_totals_section(frm);
|
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() {
|
attach_shortcuts() {
|
||||||
@@ -989,13 +994,13 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetch_customer_transactions() {
|
fetch_customer_transactions() {
|
||||||
frappe.db
|
frappe
|
||||||
.get_list("POS Invoice", {
|
.call({
|
||||||
filters: { customer: this.customer_info.customer, docstatus: 1 },
|
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_customer_recent_transactions",
|
||||||
fields: ["name", "grand_total", "status", "posting_date", "posting_time", "currency"],
|
args: { customer: this.customer_info.customer },
|
||||||
limit: 20,
|
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
res = res.message;
|
||||||
const transaction_container = this.$customer_section.find(".customer-transactions");
|
const transaction_container = this.$customer_section.find(".customer-transactions");
|
||||||
|
|
||||||
if (!res.length) {
|
if (!res.length) {
|
||||||
@@ -1019,6 +1024,7 @@ erpnext.PointOfSale.ItemCart = class {
|
|||||||
Draft: "red",
|
Draft: "red",
|
||||||
Return: "gray",
|
Return: "gray",
|
||||||
Consolidated: "blue",
|
Consolidated: "blue",
|
||||||
|
"Credit Note Issued": "gray",
|
||||||
};
|
};
|
||||||
|
|
||||||
transaction_container.append(
|
transaction_container.append(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
this.allow_rate_change = settings.allow_rate_change;
|
this.allow_rate_change = settings.allow_rate_change;
|
||||||
this.allow_discount_change = settings.allow_discount_change;
|
this.allow_discount_change = settings.allow_discount_change;
|
||||||
this.current_item = {};
|
this.current_item = {};
|
||||||
|
this.frm_doctype = settings.frm_doctype;
|
||||||
|
|
||||||
this.init_component();
|
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 field_control = this[`${fieldname}_control`];
|
||||||
const item_row_is_being_edited = this.compare_with_current_item(item_row);
|
const item_row_is_being_edited = this.compare_with_current_item(item_row);
|
||||||
if (
|
if (
|
||||||
@@ -423,7 +426,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
|||||||
warehouse: this.warehouse_control.get_value() || "",
|
warehouse: this.warehouse_control.get_value() || "",
|
||||||
batch_nos: this.current_item.batch_no || "",
|
batch_nos: this.current_item.batch_no || "",
|
||||||
posting_date: expiry_date,
|
posting_date: expiry_date,
|
||||||
for_doctype: "POS Invoice",
|
for_doctype: this.frm_doctype,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ erpnext.PointOfSale.PastOrderList = class {
|
|||||||
});
|
});
|
||||||
const me = this;
|
const me = this;
|
||||||
this.$invoices_container.on("click", ".invoice-wrapper", function () {
|
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"));
|
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(
|
const posting_datetime = frappe.datetime.str_to_user(
|
||||||
invoice.posting_date + " " + invoice.posting_time
|
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-date">
|
||||||
<div class="invoice-name">${invoice.name}</div>
|
<div class="invoice-name">${invoice.name}</div>
|
||||||
<div class="invoice-date">
|
<div class="invoice-date">
|
||||||
|
|||||||
@@ -117,9 +117,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
|
|
||||||
async function get_returned_qty() {
|
async function get_returned_qty() {
|
||||||
const r = await frappe.call({
|
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: {
|
args: {
|
||||||
pos_invoice: doc.name,
|
doctype: doc.doctype,
|
||||||
|
invoice: doc.name,
|
||||||
customer: doc.customer,
|
customer: doc.customer,
|
||||||
item_row_name: item_data.name,
|
item_row_name: item_data.name,
|
||||||
},
|
},
|
||||||
@@ -192,7 +193,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
|
|
||||||
bind_events() {
|
bind_events() {
|
||||||
this.$summary_container.on("click", ".return-btn", async () => {
|
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) {
|
if (!r) {
|
||||||
frappe.msgprint({
|
frappe.msgprint({
|
||||||
title: __("Invalid Return"),
|
title: __("Invalid Return"),
|
||||||
@@ -201,21 +202,21 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.events.process_return(this.doc.name);
|
this.events.process_return(this.doc.doctype, this.doc.name);
|
||||||
this.toggle_component(false);
|
this.toggle_component(false);
|
||||||
this.$component.find(".no-summary-placeholder").css("display", "flex");
|
this.$component.find(".no-summary-placeholder").css("display", "flex");
|
||||||
this.$summary_wrapper.css("display", "none");
|
this.$summary_wrapper.css("display", "none");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$summary_container.on("click", ".edit-btn", () => {
|
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.toggle_component(false);
|
||||||
this.$component.find(".no-summary-placeholder").css("display", "flex");
|
this.$component.find(".no-summary-placeholder").css("display", "flex");
|
||||||
this.$summary_wrapper.css("display", "none");
|
this.$summary_wrapper.css("display", "none");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$summary_container.on("click", ".delete-btn", () => {
|
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();
|
this.show_summary_placeholder();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -461,11 +462,12 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
|||||||
show ? this.$component.css("display", "flex") : this.$component.css("display", "none");
|
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({
|
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: {
|
args: {
|
||||||
pos_invoice: invoice,
|
doctype: doctype,
|
||||||
|
invoice: invoice,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return r.message;
|
return r.message;
|
||||||
|
|||||||
@@ -164,36 +164,12 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => {
|
frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => {
|
||||||
const contact = frm.doc.contact_mobile;
|
this.bind_coupon_code_event(frm);
|
||||||
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) => {
|
frappe.ui.form.on("Sales Invoice", "coupon_code", (frm) => {
|
||||||
if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) {
|
this.bind_coupon_code_event(frm);
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setup_listener_for_payments();
|
this.setup_listener_for_payments();
|
||||||
@@ -225,19 +201,19 @@ erpnext.PointOfSale.Payment = class {
|
|||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("POS Invoice", "paid_amount", (frm) => {
|
frappe.ui.form.on("POS Invoice", "paid_amount", (frm) => {
|
||||||
this.update_totals_section(frm.doc);
|
this.bind_paid_amount_event(frm);
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("POS Invoice", "loyalty_amount", (frm) => {
|
frappe.ui.form.on("POS Invoice", "loyalty_amount", (frm) => {
|
||||||
const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
|
this.bind_loyalty_amount_event(frm);
|
||||||
this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency);
|
});
|
||||||
|
|
||||||
|
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) => {
|
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() {
|
setup_listener_for_payments() {
|
||||||
frappe.realtime.on("process_phone_payment", (data) => {
|
frappe.realtime.on("process_phone_payment", (data) => {
|
||||||
const doc = this.events.get_frm().doc;
|
const doc = this.events.get_frm().doc;
|
||||||
|
|||||||
Reference in New Issue
Block a user