Merge branch 'develop' into education-dashboard
This commit is contained in:
commit
fdcaecb58c
@ -147,6 +147,11 @@
|
|||||||
"link_to": "Trial Balance",
|
"link_to": "Trial Balance",
|
||||||
"type": "Report"
|
"type": "Report"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Point of Sale",
|
||||||
|
"link_to": "point-of-sale",
|
||||||
|
"type": "Page"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Dashboard",
|
"label": "Dashboard",
|
||||||
"link_to": "Accounts",
|
"link_to": "Accounts",
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
"bank_name",
|
"bank_name",
|
||||||
"swift_number",
|
"swift_number",
|
||||||
"column_break_1",
|
"column_break_1",
|
||||||
"branch_code",
|
|
||||||
"website",
|
"website",
|
||||||
"address_and_contact",
|
"address_and_contact",
|
||||||
"address_html",
|
"address_html",
|
||||||
@ -51,15 +50,6 @@
|
|||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_in_quick_entry": 1,
|
|
||||||
"fieldname": "branch_code",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Branch Code",
|
|
||||||
"unique": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "address_and_contact",
|
"fieldname": "address_and_contact",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -111,7 +101,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-03-25 21:22:33.496264",
|
"modified": "2020-07-17 14:00:13.105433",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank",
|
"name": "Bank",
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
"account_details_section",
|
"account_details_section",
|
||||||
"iban",
|
"iban",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
|
"branch_code",
|
||||||
"bank_account_no",
|
"bank_account_no",
|
||||||
"address_and_contact",
|
"address_and_contact",
|
||||||
"address_html",
|
"address_html",
|
||||||
@ -197,10 +198,16 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Mask",
|
"label": "Mask",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "branch_code",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_global_search": 1,
|
||||||
|
"label": "Branch Code"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-04-06 21:00:45.379804",
|
"modified": "2020-07-17 13:59:50.795412",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
cur_frm.add_fetch('bank_account','account','account');
|
cur_frm.add_fetch('bank_account','account','account');
|
||||||
cur_frm.add_fetch('bank_account','bank_account_no','bank_account_no');
|
cur_frm.add_fetch('bank_account','bank_account_no','bank_account_no');
|
||||||
cur_frm.add_fetch('bank_account','iban','iban');
|
cur_frm.add_fetch('bank_account','iban','iban');
|
||||||
cur_frm.add_fetch('bank','branch_code','branch_code');
|
cur_frm.add_fetch('bank_account','branch_code','branch_code');
|
||||||
cur_frm.add_fetch('bank','swift_number','swift_number');
|
cur_frm.add_fetch('bank','swift_number','swift_number');
|
||||||
|
|
||||||
frappe.ui.form.on('Bank Guarantee', {
|
frappe.ui.form.on('Bank Guarantee', {
|
||||||
|
|||||||
149
erpnext/accounts/doctype/dunning/dunning.js
Normal file
149
erpnext/accounts/doctype/dunning/dunning.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on("Dunning", {
|
||||||
|
setup: function (frm) {
|
||||||
|
frm.set_query("sales_invoice", () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
docstatus: 1,
|
||||||
|
company: frm.doc.company,
|
||||||
|
outstanding_amount: [">", 0],
|
||||||
|
status: "Overdue"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
frm.set_query("income_account", () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
root_type: "Income",
|
||||||
|
is_group: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
refresh: function (frm) {
|
||||||
|
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
|
||||||
|
frm.set_df_property(
|
||||||
|
"sales_invoice",
|
||||||
|
"read_only",
|
||||||
|
frm.doc.__islocal ? 0 : 1
|
||||||
|
);
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
|
||||||
|
frm.add_custom_button(__("Resolve"), () => {
|
||||||
|
frm.set_value("status", "Resolved");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.status !== "Resolved") {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Payment"),
|
||||||
|
function () {
|
||||||
|
frm.events.make_payment_entry(frm);
|
||||||
|
},__("Create")
|
||||||
|
);
|
||||||
|
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overdue_days: function (frm) {
|
||||||
|
frappe.db.get_value(
|
||||||
|
"Dunning Type",
|
||||||
|
{
|
||||||
|
start_day: ["<", frm.doc.overdue_days],
|
||||||
|
end_day: [">=", frm.doc.overdue_days],
|
||||||
|
},
|
||||||
|
"dunning_type",
|
||||||
|
(r) => {
|
||||||
|
if (r) {
|
||||||
|
frm.set_value("dunning_type", r.dunning_type);
|
||||||
|
} else {
|
||||||
|
frm.set_value("dunning_type", "");
|
||||||
|
frm.set_value("rate_of_interest", "");
|
||||||
|
frm.set_value("dunning_fee", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
dunning_type: function (frm) {
|
||||||
|
frm.trigger("get_dunning_letter_text");
|
||||||
|
},
|
||||||
|
language: function (frm) {
|
||||||
|
frm.trigger("get_dunning_letter_text");
|
||||||
|
},
|
||||||
|
get_dunning_letter_text: function (frm) {
|
||||||
|
if (frm.doc.dunning_type) {
|
||||||
|
frappe.call({
|
||||||
|
method:
|
||||||
|
"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
|
||||||
|
args: {
|
||||||
|
dunning_type: frm.doc.dunning_type,
|
||||||
|
language: frm.doc.language,
|
||||||
|
doc: frm.doc,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value("body_text", r.message.body_text);
|
||||||
|
frm.set_value("closing_text", r.message.closing_text);
|
||||||
|
frm.set_value("language", r.message.language);
|
||||||
|
} else {
|
||||||
|
frm.set_value("body_text", "");
|
||||||
|
frm.set_value("closing_text", "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
due_date: function (frm) {
|
||||||
|
frm.trigger("calculate_overdue_days");
|
||||||
|
},
|
||||||
|
posting_date: function (frm) {
|
||||||
|
frm.trigger("calculate_overdue_days");
|
||||||
|
},
|
||||||
|
rate_of_interest: function (frm) {
|
||||||
|
frm.trigger("calculate_interest_and_amount");
|
||||||
|
},
|
||||||
|
outstanding_amount: function (frm) {
|
||||||
|
frm.trigger("calculate_interest_and_amount");
|
||||||
|
},
|
||||||
|
interest_amount: function (frm) {
|
||||||
|
frm.trigger("calculate_interest_and_amount");
|
||||||
|
},
|
||||||
|
dunning_fee: function (frm) {
|
||||||
|
frm.trigger("calculate_interest_and_amount");
|
||||||
|
},
|
||||||
|
sales_invoice: function (frm) {
|
||||||
|
frm.trigger("calculate_overdue_days");
|
||||||
|
},
|
||||||
|
calculate_overdue_days: function (frm) {
|
||||||
|
if (frm.doc.posting_date && frm.doc.due_date) {
|
||||||
|
const overdue_days = moment(frm.doc.posting_date).diff(
|
||||||
|
frm.doc.due_date,
|
||||||
|
"days"
|
||||||
|
);
|
||||||
|
frm.set_value("overdue_days", overdue_days);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calculate_interest_and_amount: function (frm) {
|
||||||
|
const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
|
||||||
|
const interest_amount = interest_per_year / 365 * frm.doc.overdue_days || 0;
|
||||||
|
const dunning_amount = interest_amount + frm.doc.dunning_fee;
|
||||||
|
const grand_total = frm.doc.outstanding_amount + dunning_amount;
|
||||||
|
frm.set_value("interest_amount", interest_amount);
|
||||||
|
frm.set_value("dunning_amount", dunning_amount);
|
||||||
|
frm.set_value("grand_total", grand_total);
|
||||||
|
},
|
||||||
|
make_payment_entry: function (frm) {
|
||||||
|
return frappe.call({
|
||||||
|
method:
|
||||||
|
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
|
||||||
|
args: {
|
||||||
|
dt: frm.doc.doctype,
|
||||||
|
dn: frm.doc.name,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
370
erpnext/accounts/doctype/dunning/dunning.json
Normal file
370
erpnext/accounts/doctype/dunning/dunning.json
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_events_in_timeline": 1,
|
||||||
|
"autoname": "naming_series:",
|
||||||
|
"creation": "2019-07-05 16:34:31.013238",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"title",
|
||||||
|
"naming_series",
|
||||||
|
"sales_invoice",
|
||||||
|
"customer",
|
||||||
|
"customer_name",
|
||||||
|
"outstanding_amount",
|
||||||
|
"currency",
|
||||||
|
"conversion_rate",
|
||||||
|
"column_break_3",
|
||||||
|
"company",
|
||||||
|
"posting_date",
|
||||||
|
"posting_time",
|
||||||
|
"due_date",
|
||||||
|
"overdue_days",
|
||||||
|
"address_and_contact_section",
|
||||||
|
"address_display",
|
||||||
|
"contact_display",
|
||||||
|
"contact_mobile",
|
||||||
|
"contact_email",
|
||||||
|
"column_break_18",
|
||||||
|
"company_address_display",
|
||||||
|
"section_break_6",
|
||||||
|
"dunning_type",
|
||||||
|
"interest_amount",
|
||||||
|
"column_break_8",
|
||||||
|
"rate_of_interest",
|
||||||
|
"dunning_fee",
|
||||||
|
"section_break_12",
|
||||||
|
"dunning_amount",
|
||||||
|
"grand_total",
|
||||||
|
"income_account",
|
||||||
|
"column_break_17",
|
||||||
|
"status",
|
||||||
|
"printing_setting_section",
|
||||||
|
"language",
|
||||||
|
"body_text",
|
||||||
|
"column_break_22",
|
||||||
|
"letter_head",
|
||||||
|
"closing_text",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "DUNN-.MM.-.YY.-",
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Series",
|
||||||
|
"options": "DUNN-.MM.-.YY.-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_invoice",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Sales Invoice",
|
||||||
|
"options": "Sales Invoice",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.customer_name",
|
||||||
|
"fieldname": "customer_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Customer Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.outstanding_amount",
|
||||||
|
"fieldname": "outstanding_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Outstanding Amount",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "overdue_days",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Overdue Days",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_6",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dunning_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Dunning Type",
|
||||||
|
"options": "Dunning Type",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "interest_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Interest Amount",
|
||||||
|
"precision": "2",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_8",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fetch_from": "dunning_type.dunning_fee",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "dunning_fee",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Dunning Fee",
|
||||||
|
"precision": "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_12",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_17",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "printing_setting_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Printing Setting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "language",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Print Language",
|
||||||
|
"options": "Language"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "letter_head",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Letter Head",
|
||||||
|
"options": "Letter Head"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_22",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.currency",
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Dunning",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"default": "{customer_name}",
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "body_text",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Body Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "closing_text",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Closing Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.due_date",
|
||||||
|
"fieldname": "due_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Due Date",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"label": "Posting Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fetch_from": "dunning_type.interest_rate",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "rate_of_interest",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Rate of Interest (%) Yearly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "address_and_contact_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Address and Contact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.address_display",
|
||||||
|
"fieldname": "address_display",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Address",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.contact_display",
|
||||||
|
"fieldname": "contact_display",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Contact",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.contact_mobile",
|
||||||
|
"fieldname": "contact_mobile",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Mobile No",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_18",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.company_address_display",
|
||||||
|
"fieldname": "company_address_display",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Company Address",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.contact_email",
|
||||||
|
"fieldname": "contact_email",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Contact Email",
|
||||||
|
"options": "Email",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.customer",
|
||||||
|
"fieldname": "customer",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Customer",
|
||||||
|
"options": "Customer",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Grand Total",
|
||||||
|
"precision": "2",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"default": "Unresolved",
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Draft\nResolved\nUnresolved\nCancelled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dunning_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Dunning Amount",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "income_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Income Account",
|
||||||
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_invoice.conversion_rate",
|
||||||
|
"fieldname": "conversion_rate",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Conversion Rate",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-07-21 18:20:23.512151",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Dunning",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "ASC",
|
||||||
|
"title_field": "customer_name",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
119
erpnext/accounts/doctype/dunning/dunning.py
Normal file
119
erpnext/accounts/doctype/dunning/dunning.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
import json
|
||||||
|
from six import string_types
|
||||||
|
from frappe.utils import getdate, get_datetime, rounded, flt
|
||||||
|
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
|
||||||
|
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||||
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
|
|
||||||
|
|
||||||
|
class Dunning(AccountsController):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_overdue_days()
|
||||||
|
self.validate_amount()
|
||||||
|
if not self.income_account:
|
||||||
|
self.income_account = frappe.db.get_value('Company', self.company, 'default_income_account')
|
||||||
|
|
||||||
|
def validate_overdue_days(self):
|
||||||
|
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
|
||||||
|
|
||||||
|
def validate_amount(self):
|
||||||
|
amounts = calculate_interest_and_amount(
|
||||||
|
self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
|
||||||
|
if self.interest_amount != amounts.get('interest_amount'):
|
||||||
|
self.interest_amount = amounts.get('interest_amount')
|
||||||
|
if self.dunning_amount != amounts.get('dunning_amount'):
|
||||||
|
self.dunning_amount = amounts.get('dunning_amount')
|
||||||
|
if self.grand_total != amounts.get('grand_total'):
|
||||||
|
self.grand_total = amounts.get('grand_total')
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
self.make_gl_entries()
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
if self.dunning_amount:
|
||||||
|
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
|
||||||
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
|
||||||
|
def make_gl_entries(self):
|
||||||
|
if not self.dunning_amount:
|
||||||
|
return
|
||||||
|
gl_entries = []
|
||||||
|
invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"]
|
||||||
|
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
|
||||||
|
accounting_dimensions = get_accounting_dimensions()
|
||||||
|
invoice_fields.extend(accounting_dimensions)
|
||||||
|
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
|
||||||
|
default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
|
||||||
|
gl_entries.append(
|
||||||
|
self.get_gl_dict({
|
||||||
|
"account": inv.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"due_date": self.due_date,
|
||||||
|
"against": self.income_account,
|
||||||
|
"debit": dunning_in_company_currency,
|
||||||
|
"debit_in_account_currency": self.dunning_amount,
|
||||||
|
"against_voucher": self.name,
|
||||||
|
"against_voucher_type": "Dunning",
|
||||||
|
"cost_center": inv.cost_center or default_cost_center,
|
||||||
|
"project": inv.project
|
||||||
|
}, inv.party_account_currency, item=inv)
|
||||||
|
)
|
||||||
|
gl_entries.append(
|
||||||
|
self.get_gl_dict({
|
||||||
|
"account": self.income_account,
|
||||||
|
"against": self.customer,
|
||||||
|
"credit": dunning_in_company_currency,
|
||||||
|
"cost_center": inv.cost_center or default_cost_center,
|
||||||
|
"credit_in_account_currency": self.dunning_amount,
|
||||||
|
"project": inv.project
|
||||||
|
}, item=inv)
|
||||||
|
)
|
||||||
|
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_dunning(doc, state):
|
||||||
|
for reference in doc.references:
|
||||||
|
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
|
||||||
|
dunnings = frappe.get_list('Dunning', filters={
|
||||||
|
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')})
|
||||||
|
|
||||||
|
for dunning in dunnings:
|
||||||
|
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
|
||||||
|
|
||||||
|
def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
|
||||||
|
interest_amount = 0
|
||||||
|
if rate_of_interest:
|
||||||
|
interest_per_year = rounded(flt(outstanding_amount) * flt(rate_of_interest))/100
|
||||||
|
interest_amount = (
|
||||||
|
interest_per_year / days_in_year(get_datetime(posting_date).year)) * int(overdue_days)
|
||||||
|
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
|
||||||
|
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||||
|
return {
|
||||||
|
'interest_amount': interest_amount,
|
||||||
|
'grand_total': grand_total,
|
||||||
|
'dunning_amount': dunning_amount}
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_dunning_letter_text(dunning_type, doc, language=None):
|
||||||
|
if isinstance(doc, string_types):
|
||||||
|
doc = json.loads(doc)
|
||||||
|
if language:
|
||||||
|
filters = {'parent': dunning_type, 'language': language}
|
||||||
|
else:
|
||||||
|
filters = {'parent': dunning_type, 'is_default_language': 1}
|
||||||
|
letter_text = frappe.db.get_value('Dunning Letter Text', filters,
|
||||||
|
['body_text', 'closing_text', 'language'], as_dict=1)
|
||||||
|
if letter_text:
|
||||||
|
return {
|
||||||
|
'body_text': frappe.render_template(letter_text.body_text, doc),
|
||||||
|
'closing_text': frappe.render_template(letter_text.closing_text, doc),
|
||||||
|
'language': letter_text.language
|
||||||
|
}
|
||||||
9
erpnext/accounts/doctype/dunning/dunning_list.js
Normal file
9
erpnext/accounts/doctype/dunning/dunning_list.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
frappe.listview_settings["Dunning"] = {
|
||||||
|
get_indicator: function (doc) {
|
||||||
|
if (doc.status === "Resolved") {
|
||||||
|
return [__("Resolved"), "green", "status,=,Resolved"];
|
||||||
|
} else {
|
||||||
|
return [__("Unresolved"), "red", "status,=,Unresolved"];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
100
erpnext/accounts/doctype/dunning/test_dunning.py
Normal file
100
erpnext/accounts/doctype/dunning/test_dunning.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
from frappe.utils import add_days, today, nowdate
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice_against_cost_center
|
||||||
|
from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
|
|
||||||
|
class TestDunning(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
create_dunning_type()
|
||||||
|
unlink_payment_on_cancel_of_invoice()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(self):
|
||||||
|
unlink_payment_on_cancel_of_invoice(0)
|
||||||
|
|
||||||
|
def test_dunning(self):
|
||||||
|
dunning = create_dunning()
|
||||||
|
amounts = calculate_interest_and_amount(
|
||||||
|
dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
|
||||||
|
self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
|
||||||
|
self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
|
||||||
|
self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
|
||||||
|
|
||||||
|
def test_gl_entries(self):
|
||||||
|
dunning = create_dunning()
|
||||||
|
dunning.submit()
|
||||||
|
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||||
|
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
|
||||||
|
order by account asc""", dunning.name, as_dict=1)
|
||||||
|
self.assertTrue(gl_entries)
|
||||||
|
expected_values = dict((d[0], d) for d in [
|
||||||
|
['Debtors - _TC', 20.44, 0.0],
|
||||||
|
['Sales - _TC', 0.0, 20.44]
|
||||||
|
])
|
||||||
|
for gle in gl_entries:
|
||||||
|
self.assertEquals(expected_values[gle.account][0], gle.account)
|
||||||
|
self.assertEquals(expected_values[gle.account][1], gle.debit)
|
||||||
|
self.assertEquals(expected_values[gle.account][2], gle.credit)
|
||||||
|
|
||||||
|
def test_payment_entry(self):
|
||||||
|
dunning = create_dunning()
|
||||||
|
dunning.submit()
|
||||||
|
pe = get_payment_entry("Dunning", dunning.name)
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
pe.paid_from_account_currency = dunning.currency
|
||||||
|
pe.paid_to_account_currency = dunning.currency
|
||||||
|
pe.source_exchange_rate = 1
|
||||||
|
pe.target_exchange_rate = 1
|
||||||
|
pe.insert()
|
||||||
|
pe.submit()
|
||||||
|
si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice)
|
||||||
|
self.assertEqual(si_doc.outstanding_amount, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def create_dunning():
|
||||||
|
posting_date = add_days(today(), -20)
|
||||||
|
due_date = add_days(today(), -15)
|
||||||
|
sales_invoice = create_sales_invoice_against_cost_center(
|
||||||
|
posting_date=posting_date, due_date=due_date, status='Overdue')
|
||||||
|
dunning_type = frappe.get_doc("Dunning Type", 'First Notice')
|
||||||
|
dunning = frappe.new_doc("Dunning")
|
||||||
|
dunning.sales_invoice = sales_invoice.name
|
||||||
|
dunning.customer_name = sales_invoice.customer_name
|
||||||
|
dunning.outstanding_amount = sales_invoice.outstanding_amount
|
||||||
|
dunning.debit_to = sales_invoice.debit_to
|
||||||
|
dunning.currency = sales_invoice.currency
|
||||||
|
dunning.company = sales_invoice.company
|
||||||
|
dunning.posting_date = nowdate()
|
||||||
|
dunning.due_date = sales_invoice.due_date
|
||||||
|
dunning.dunning_type = 'First Notice'
|
||||||
|
dunning.rate_of_interest = dunning_type.rate_of_interest
|
||||||
|
dunning.dunning_fee = dunning_type.dunning_fee
|
||||||
|
dunning.save()
|
||||||
|
return dunning
|
||||||
|
|
||||||
|
def create_dunning_type():
|
||||||
|
dunning_type = frappe.new_doc("Dunning Type")
|
||||||
|
dunning_type.dunning_type = 'First Notice'
|
||||||
|
dunning_type.start_day = 10
|
||||||
|
dunning_type.end_day = 20
|
||||||
|
dunning_type.dunning_fee = 20
|
||||||
|
dunning_type.rate_of_interest = 8
|
||||||
|
dunning_type.append(
|
||||||
|
"dunning_letter_text", {
|
||||||
|
'language': 'en',
|
||||||
|
'body_text': 'We have still not received payment for our invoice ',
|
||||||
|
'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
dunning_type.save()
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2019-12-06 04:25:40.215625",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"language",
|
||||||
|
"is_default_language",
|
||||||
|
"section_break_4",
|
||||||
|
"body_text",
|
||||||
|
"closing_text",
|
||||||
|
"section_break_7",
|
||||||
|
"body_and_closing_text_help"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "language",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Language",
|
||||||
|
"options": "Language"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_default_language",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Default Language"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_4",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Letter or Email Body Text",
|
||||||
|
"fieldname": "body_text",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Body Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Letter or Email Closing Text",
|
||||||
|
"fieldname": "closing_text",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Closing Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_7",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "body_and_closing_text_help",
|
||||||
|
"fieldtype": "HTML",
|
||||||
|
"label": "Body and Closing Text Help",
|
||||||
|
"options": "<h4>Body Text and Closing Text Example</h4>\n\n<div>We have noticed that you have not yet paid invoice {{sales_invoice}} for {{frappe.db.get_value(\"Currency\", currency, \"symbol\")}} {{outstanding_amount}}. This is a friendly reminder that the invoice was due on {{due_date}}. Please pay the amount due immediately to avoid any further dunning cost.</div>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your template are the fields in the document. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-07-14 18:02:35.988958",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Dunning Letter Text",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class DunningLetterText(Document):
|
||||||
|
pass
|
||||||
8
erpnext/accounts/doctype/dunning_type/dunning_type.js
Normal file
8
erpnext/accounts/doctype/dunning_type/dunning_type.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Dunning Type', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
||||||
129
erpnext/accounts/doctype/dunning_type/dunning_type.json
Normal file
129
erpnext/accounts/doctype/dunning_type/dunning_type.json
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "field:dunning_type",
|
||||||
|
"creation": "2019-12-04 04:59:08.003664",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"dunning_type",
|
||||||
|
"overdue_interval_section",
|
||||||
|
"start_day",
|
||||||
|
"column_break_4",
|
||||||
|
"end_day",
|
||||||
|
"section_break_6",
|
||||||
|
"dunning_fee",
|
||||||
|
"column_break_8",
|
||||||
|
"rate_of_interest",
|
||||||
|
"text_block_section",
|
||||||
|
"dunning_letter_text"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "dunning_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Dunning Type",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dunning_fee",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Dunning Fee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This section allows the user to set the Body and Closing text of the Dunning Letter for the Dunning Type based on language, which can be used in Print.",
|
||||||
|
"fieldname": "text_block_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Dunning Letter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dunning_letter_text",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"options": "Dunning Letter Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_6",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_8",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "overdue_interval_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Overdue Interval"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "start_day",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Start Day"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "end_day",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "End Day"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "rate_of_interest",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Rate of Interest (%) Yearly"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-07-15 17:14:17.835074",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Dunning Type",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Administrator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
10
erpnext/accounts/doctype/dunning_type/dunning_type.py
Normal file
10
erpnext/accounts/doctype/dunning_type/dunning_type.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class DunningType(Document):
|
||||||
|
pass
|
||||||
10
erpnext/accounts/doctype/dunning_type/test_dunning_type.py
Normal file
10
erpnext/accounts/doctype/dunning_type/test_dunning_type.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestDunningType(unittest.TestCase):
|
||||||
|
pass
|
||||||
@ -1,426 +1,123 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"autoname": "",
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-01-23 05:40:18.117583",
|
"creation": "2018-01-23 05:40:18.117583",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"loyalty_program",
|
||||||
|
"loyalty_program_tier",
|
||||||
|
"customer",
|
||||||
|
"invoice_type",
|
||||||
|
"invoice",
|
||||||
|
"redeem_against",
|
||||||
|
"loyalty_points",
|
||||||
|
"purchase_amount",
|
||||||
|
"expiry_date",
|
||||||
|
"posting_date",
|
||||||
|
"company"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "loyalty_program",
|
"fieldname": "loyalty_program",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Loyalty Program",
|
"label": "Loyalty Program",
|
||||||
"length": 0,
|
"options": "Loyalty Program"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Loyalty Program",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "loyalty_program_tier",
|
"fieldname": "loyalty_program_tier",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
"label": "Loyalty Program Tier"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Loyalty Program Tier",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "customer",
|
"fieldname": "customer",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Customer",
|
"label": "Customer",
|
||||||
"length": 0,
|
"options": "Customer"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Customer",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "sales_invoice",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Sales Invoice",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Sales Invoice",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "redeem_against",
|
"fieldname": "redeem_against",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Redeem Against",
|
"label": "Redeem Against",
|
||||||
"length": 0,
|
"options": "Loyalty Point Entry"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Loyalty Point Entry",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "loyalty_points",
|
"fieldname": "loyalty_points",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Loyalty Points"
|
||||||
"label": "Loyalty Points",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "purchase_amount",
|
"fieldname": "purchase_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
"label": "Purchase Amount"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Purchase Amount",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "expiry_date",
|
"fieldname": "expiry_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Expiry Date"
|
||||||
"label": "Expiry Date",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "posting_date",
|
"fieldname": "posting_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
"label": "Posting Date"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Posting Date",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"length": 0,
|
"options": "Company"
|
||||||
"no_copy": 0,
|
},
|
||||||
"options": "Company",
|
{
|
||||||
"permlevel": 0,
|
"fieldname": "invoice_type",
|
||||||
"precision": "",
|
"fieldtype": "Link",
|
||||||
"print_hide": 0,
|
"label": "Invoice Type",
|
||||||
"print_hide_if_no_value": 0,
|
"options": "DocType"
|
||||||
"read_only": 0,
|
},
|
||||||
"remember_last_selected_value": 0,
|
{
|
||||||
"report_hide": 0,
|
"fieldname": "invoice",
|
||||||
"reqd": 0,
|
"fieldtype": "Dynamic Link",
|
||||||
"search_index": 0,
|
"in_list_view": 1,
|
||||||
"set_only_once": 0,
|
"label": "Invoice",
|
||||||
"translatable": 0,
|
"options": "invoice_type"
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"is_submittable": 0,
|
"modified": "2020-01-30 17:27:55.964242",
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2018-08-29 16:05:22.810347",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Loyalty Point Entry",
|
"name": "Loyalty Point Entry",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Auditor",
|
"role": "Auditor"
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Accounts Manager",
|
"role": "Accounts Manager"
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Accounts User",
|
"role": "Accounts User"
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "customer",
|
"title_field": "customer",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No
|
|||||||
date = today()
|
date = today()
|
||||||
|
|
||||||
return frappe.db.sql('''
|
return frappe.db.sql('''
|
||||||
select name, loyalty_points, expiry_date, loyalty_program_tier, sales_invoice
|
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
|
||||||
from `tabLoyalty Point Entry`
|
from `tabLoyalty Point Entry`
|
||||||
where customer=%s and loyalty_program=%s
|
where customer=%s and loyalty_program=%s
|
||||||
and expiry_date>=%s and loyalty_points>0 and company=%s
|
and expiry_date>=%s and loyalty_points>0 and company=%s
|
||||||
|
|||||||
@ -36,7 +36,8 @@ def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=Non
|
|||||||
return {"loyalty_points": 0, "total_spent": 0}
|
return {"loyalty_points": 0, "total_spent": 0}
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False, current_transaction_amount=0):
|
def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, \
|
||||||
|
silent=False, include_expired_entry=False, current_transaction_amount=0):
|
||||||
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
|
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
|
||||||
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
|
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
|
||||||
lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry))
|
lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry))
|
||||||
@ -59,10 +60,10 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None
|
|||||||
if not loyalty_program:
|
if not loyalty_program:
|
||||||
loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program")
|
loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program")
|
||||||
|
|
||||||
if not (loyalty_program or silent):
|
if not loyalty_program and not silent:
|
||||||
frappe.throw(_("Customer isn't enrolled in any Loyalty Program"))
|
frappe.throw(_("Customer isn't enrolled in any Loyalty Program"))
|
||||||
elif silent and not loyalty_program:
|
elif silent and not loyalty_program:
|
||||||
return frappe._dict({"loyalty_program": None})
|
return frappe._dict({"loyalty_programs": None})
|
||||||
|
|
||||||
if not company:
|
if not company:
|
||||||
company = frappe.db.get_default("company") or frappe.get_all("Company")[0].name
|
company = frappe.db.get_default("company") or frappe.get_all("Company")[0].name
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
|||||||
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
|
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
|
||||||
earned_points = get_points_earned(si_original)
|
earned_points = get_points_earned(si_original)
|
||||||
|
|
||||||
lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
|
lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
|
||||||
|
|
||||||
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
|
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
|
||||||
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
|
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
|
||||||
@ -42,8 +42,8 @@ class TestLoyaltyProgram(unittest.TestCase):
|
|||||||
|
|
||||||
earned_after_redemption = get_points_earned(si_redeem)
|
earned_after_redemption = get_points_earned(si_redeem)
|
||||||
|
|
||||||
lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name})
|
lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
|
||||||
lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
|
lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
|
||||||
|
|
||||||
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
|
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
|
||||||
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
|
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
|
||||||
@ -66,7 +66,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
|||||||
|
|
||||||
earned_points = get_points_earned(si_original)
|
earned_points = get_points_earned(si_original)
|
||||||
|
|
||||||
lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
|
lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
|
||||||
|
|
||||||
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
|
self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
|
||||||
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
|
self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
|
||||||
@ -82,8 +82,8 @@ class TestLoyaltyProgram(unittest.TestCase):
|
|||||||
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
|
customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
|
||||||
earned_after_redemption = get_points_earned(si_redeem)
|
earned_after_redemption = get_points_earned(si_redeem)
|
||||||
|
|
||||||
lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name})
|
lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
|
||||||
lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
|
lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
|
||||||
|
|
||||||
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
|
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
|
||||||
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
|
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
|
||||||
@ -101,7 +101,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
|||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si.name, 'customer': si.customer})
|
lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si.name, 'customer': si.customer})
|
||||||
self.assertEqual(True, not (lpe is None))
|
self.assertEqual(True, not (lpe is None))
|
||||||
|
|
||||||
# cancelling sales invoice
|
# cancelling sales invoice
|
||||||
@ -118,7 +118,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
|||||||
si_original.submit()
|
si_original.submit()
|
||||||
|
|
||||||
earned_points = get_points_earned(si_original)
|
earned_points = get_points_earned(si_original)
|
||||||
lpe_original = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
|
lpe_original = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
|
||||||
self.assertEqual(lpe_original.loyalty_points, earned_points)
|
self.assertEqual(lpe_original.loyalty_points, earned_points)
|
||||||
|
|
||||||
# create sales invoice return
|
# create sales invoice return
|
||||||
@ -130,10 +130,10 @@ class TestLoyaltyProgram(unittest.TestCase):
|
|||||||
si_return.submit()
|
si_return.submit()
|
||||||
|
|
||||||
# fetch original invoice again as its status would have been updated
|
# fetch original invoice again as its status would have been updated
|
||||||
si_original = frappe.get_doc('Sales Invoice', lpe_original.sales_invoice)
|
si_original = frappe.get_doc('Sales Invoice', lpe_original.invoice)
|
||||||
|
|
||||||
earned_points = get_points_earned(si_original)
|
earned_points = get_points_earned(si_original)
|
||||||
lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer})
|
lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
|
||||||
self.assertEqual(lpe_after_return.loyalty_points, earned_points)
|
self.assertEqual(lpe_after_return.loyalty_points, earned_points)
|
||||||
self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points))
|
self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points))
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
|
|
||||||
frm.set_query("reference_doctype", "references", function() {
|
frm.set_query("reference_doctype", "references", function() {
|
||||||
if (frm.doc.party_type=="Customer") {
|
if (frm.doc.party_type=="Customer") {
|
||||||
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry"];
|
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
|
||||||
} else if (frm.doc.party_type=="Supplier") {
|
} else if (frm.doc.party_type=="Supplier") {
|
||||||
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
|
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
|
||||||
} else if (frm.doc.party_type=="Employee") {
|
} else if (frm.doc.party_type=="Employee") {
|
||||||
@ -125,7 +125,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
const child = locals[cdt][cdn];
|
const child = locals[cdt][cdn];
|
||||||
const filters = {"docstatus": 1, "company": doc.company};
|
const filters = {"docstatus": 1, "company": doc.company};
|
||||||
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
|
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
|
||||||
'Purchase Order', 'Expense Claim', 'Fees'];
|
'Purchase Order', 'Expense Claim', 'Fees', 'Dunning'];
|
||||||
|
|
||||||
if (in_list(party_type_doctypes, child.reference_doctype)) {
|
if (in_list(party_type_doctypes, child.reference_doctype)) {
|
||||||
filters[doc.party_type.toLowerCase()] = doc.party;
|
filters[doc.party_type.toLowerCase()] = doc.party;
|
||||||
@ -863,10 +863,10 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(frm.doc.party_type=="Customer" &&
|
if(frm.doc.party_type=="Customer" &&
|
||||||
!in_list(["Sales Order", "Sales Invoice", "Journal Entry"], row.reference_doctype)
|
!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
|
||||||
) {
|
) {
|
||||||
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
|
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
|
||||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice or Journal Entry", [row.idx]));
|
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning", [row.idx]));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -199,8 +199,8 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
def validate_account_type(self, account, account_types):
|
def validate_account_type(self, account, account_types):
|
||||||
account_type = frappe.db.get_value("Account", account, "account_type")
|
account_type = frappe.db.get_value("Account", account, "account_type")
|
||||||
if account_type not in account_types:
|
# if account_type not in account_types:
|
||||||
frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
|
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
|
||||||
|
|
||||||
def set_exchange_rate(self):
|
def set_exchange_rate(self):
|
||||||
if self.paid_from and not self.source_exchange_rate:
|
if self.paid_from and not self.source_exchange_rate:
|
||||||
@ -223,7 +223,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.party_type == "Student":
|
if self.party_type == "Student":
|
||||||
valid_reference_doctypes = ("Fees")
|
valid_reference_doctypes = ("Fees")
|
||||||
elif self.party_type == "Customer":
|
elif self.party_type == "Customer":
|
||||||
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry")
|
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
|
||||||
elif self.party_type == "Supplier":
|
elif self.party_type == "Supplier":
|
||||||
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
|
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
|
||||||
elif self.party_type == "Employee":
|
elif self.party_type == "Employee":
|
||||||
@ -897,6 +897,10 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
total_amount = ref_doc.get("grand_total")
|
total_amount = ref_doc.get("grand_total")
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||||
|
if reference_doctype == "Dunning":
|
||||||
|
total_amount = ref_doc.get("dunning_amount")
|
||||||
|
exchange_rate = 1
|
||||||
|
outstanding_amount = ref_doc.get("dunning_amount")
|
||||||
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
||||||
total_amount = ref_doc.get("total_amount")
|
total_amount = ref_doc.get("total_amount")
|
||||||
if ref_doc.multi_currency:
|
if ref_doc.multi_currency:
|
||||||
@ -907,7 +911,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
elif reference_doctype != "Journal Entry":
|
elif reference_doctype != "Journal Entry":
|
||||||
if party_account_currency == company_currency:
|
if party_account_currency == company_currency:
|
||||||
if ref_doc.doctype == "Expense Claim":
|
if ref_doc.doctype == "Expense Claim":
|
||||||
total_amount = ref_doc.total_sanctioned_amount
|
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
|
||||||
elif ref_doc.doctype == "Employee Advance":
|
elif ref_doc.doctype == "Employee Advance":
|
||||||
total_amount = ref_doc.advance_amount
|
total_amount = ref_doc.advance_amount
|
||||||
else:
|
else:
|
||||||
@ -925,8 +929,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||||
bill_no = ref_doc.get("bill_no")
|
bill_no = ref_doc.get("bill_no")
|
||||||
elif reference_doctype == "Expense Claim":
|
elif reference_doctype == "Expense Claim":
|
||||||
outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) \
|
outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
|
||||||
- flt(ref_doc.get("total_amount+reimbursed")) - flt(ref_doc.get("total_advance_amount"))
|
- flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
|
||||||
elif reference_doctype == "Employee Advance":
|
elif reference_doctype == "Employee Advance":
|
||||||
outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount)
|
outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount)
|
||||||
else:
|
else:
|
||||||
@ -951,7 +955,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
|||||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
|
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
|
||||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||||
|
|
||||||
if dt in ("Sales Invoice", "Sales Order"):
|
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
|
||||||
party_type = "Customer"
|
party_type = "Customer"
|
||||||
elif dt in ("Purchase Invoice", "Purchase Order"):
|
elif dt in ("Purchase Invoice", "Purchase Order"):
|
||||||
party_type = "Supplier"
|
party_type = "Supplier"
|
||||||
@ -980,7 +984,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
|||||||
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
|
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
|
||||||
|
|
||||||
# payment type
|
# payment type
|
||||||
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees") and doc.outstanding_amount > 0)) \
|
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
|
||||||
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
|
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
|
||||||
payment_type = "Receive"
|
payment_type = "Receive"
|
||||||
else:
|
else:
|
||||||
@ -1006,6 +1010,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
|||||||
elif dt == "Fees":
|
elif dt == "Fees":
|
||||||
grand_total = doc.grand_total
|
grand_total = doc.grand_total
|
||||||
outstanding_amount = doc.outstanding_amount
|
outstanding_amount = doc.outstanding_amount
|
||||||
|
elif dt == "Dunning":
|
||||||
|
grand_total = doc.grand_total
|
||||||
|
outstanding_amount = doc.grand_total
|
||||||
else:
|
else:
|
||||||
if party_account_currency == doc.company_currency:
|
if party_account_currency == doc.company_currency:
|
||||||
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||||
@ -1074,6 +1081,26 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
|||||||
|
|
||||||
for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
|
for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
|
||||||
pe.append('references', reference)
|
pe.append('references', reference)
|
||||||
|
else:
|
||||||
|
if dt == "Dunning":
|
||||||
|
pe.append("references", {
|
||||||
|
'reference_doctype': 'Sales Invoice',
|
||||||
|
'reference_name': doc.get('sales_invoice'),
|
||||||
|
"bill_no": doc.get("bill_no"),
|
||||||
|
"due_date": doc.get("due_date"),
|
||||||
|
'total_amount': doc.get('outstanding_amount'),
|
||||||
|
'outstanding_amount': doc.get('outstanding_amount'),
|
||||||
|
'allocated_amount': doc.get('outstanding_amount')
|
||||||
|
})
|
||||||
|
pe.append("references", {
|
||||||
|
'reference_doctype': dt,
|
||||||
|
'reference_name': dn,
|
||||||
|
"bill_no": doc.get("bill_no"),
|
||||||
|
"due_date": doc.get("due_date"),
|
||||||
|
'total_amount': doc.get('dunning_amount'),
|
||||||
|
'outstanding_amount': doc.get('dunning_amount'),
|
||||||
|
'allocated_amount': doc.get('dunning_amount')
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
pe.append("references", {
|
pe.append("references", {
|
||||||
'reference_doctype': dt,
|
'reference_doctype': dt,
|
||||||
|
|||||||
@ -73,6 +73,10 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.frm.set_value('party_type', '');
|
||||||
|
this.frm.set_value('party', '');
|
||||||
|
this.frm.set_value('receivable_payable_account', '');
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function() {
|
refresh: function() {
|
||||||
|
|||||||
@ -48,7 +48,8 @@ class PaymentReconciliation(Document):
|
|||||||
select
|
select
|
||||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||||
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
||||||
{dr_or_cr} as amount, t2.is_advance
|
{dr_or_cr} as amount, t2.is_advance,
|
||||||
|
t2.account_currency as currency
|
||||||
from
|
from
|
||||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||||
where
|
where
|
||||||
@ -88,7 +89,8 @@ class PaymentReconciliation(Document):
|
|||||||
if self.party_type == 'Customer' else "Purchase Invoice")
|
if self.party_type == 'Customer' else "Purchase Invoice")
|
||||||
|
|
||||||
return frappe.db.sql(""" SELECT `tab{doc}`.name as reference_name, %(voucher_type)s as reference_type,
|
return frappe.db.sql(""" SELECT `tab{doc}`.name as reference_name, %(voucher_type)s as reference_type,
|
||||||
(sum(`tabGL Entry`.{dr_or_cr}) - sum(`tabGL Entry`.{reconciled_dr_or_cr})) as amount
|
(sum(`tabGL Entry`.{dr_or_cr}) - sum(`tabGL Entry`.{reconciled_dr_or_cr})) as amount,
|
||||||
|
account_currency as currency
|
||||||
FROM `tab{doc}`, `tabGL Entry`
|
FROM `tab{doc}`, `tabGL Entry`
|
||||||
WHERE
|
WHERE
|
||||||
(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no)
|
(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no)
|
||||||
@ -141,6 +143,7 @@ class PaymentReconciliation(Document):
|
|||||||
ent.invoice_number = e.get('voucher_no')
|
ent.invoice_number = e.get('voucher_no')
|
||||||
ent.invoice_date = e.get('posting_date')
|
ent.invoice_date = e.get('posting_date')
|
||||||
ent.amount = flt(e.get('invoice_amount'))
|
ent.amount = flt(e.get('invoice_amount'))
|
||||||
|
ent.currency = e.get('currency')
|
||||||
ent.outstanding_amount = e.get('outstanding_amount')
|
ent.outstanding_amount = e.get('outstanding_amount')
|
||||||
|
|
||||||
def reconcile(self, args):
|
def reconcile(self, args):
|
||||||
@ -269,11 +272,14 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
reconcile_dr_or_cr = ('debit_in_account_currency'
|
reconcile_dr_or_cr = ('debit_in_account_currency'
|
||||||
if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
|
if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
|
||||||
|
|
||||||
|
company_currency = erpnext.get_company_currency(company)
|
||||||
|
|
||||||
jv = frappe.get_doc({
|
jv = frappe.get_doc({
|
||||||
"doctype": "Journal Entry",
|
"doctype": "Journal Entry",
|
||||||
"voucher_type": voucher_type,
|
"voucher_type": voucher_type,
|
||||||
"posting_date": today(),
|
"posting_date": today(),
|
||||||
"company": company,
|
"company": company,
|
||||||
|
"multi_currency": 1 if d.currency != company_currency else 0,
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
'account': d.account,
|
'account': d.account,
|
||||||
|
|||||||
@ -1,183 +1,80 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2014-07-09 16:14:23.672922",
|
"creation": "2014-07-09 16:14:23.672922",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"invoice_type",
|
||||||
|
"invoice_number",
|
||||||
|
"invoice_date",
|
||||||
|
"col_break1",
|
||||||
|
"amount",
|
||||||
|
"outstanding_amount",
|
||||||
|
"currency"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"fieldname": "invoice_type",
|
"fieldname": "invoice_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Invoice Type",
|
"label": "Invoice Type",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Sales Invoice\nPurchase Invoice\nJournal Entry",
|
"options": "Sales Invoice\nPurchase Invoice\nJournal Entry",
|
||||||
"permlevel": 0,
|
"read_only": 1
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"fieldname": "invoice_number",
|
"fieldname": "invoice_number",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Dynamic Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Invoice Number",
|
"label": "Invoice Number",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "invoice_type",
|
"options": "invoice_type",
|
||||||
"permlevel": 0,
|
"read_only": 1
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"fieldname": "invoice_date",
|
"fieldname": "invoice_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Invoice Date",
|
"label": "Invoice Date",
|
||||||
"length": 0,
|
"read_only": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"fieldname": "col_break1",
|
"fieldname": "col_break1",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"label": "",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"length": 0,
|
"options": "currency",
|
||||||
"no_copy": 0,
|
"read_only": 1
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"fieldname": "outstanding_amount",
|
"fieldname": "outstanding_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Outstanding Amount",
|
"label": "Outstanding Amount",
|
||||||
"length": 0,
|
"options": "currency",
|
||||||
"no_copy": 0,
|
"read_only": 1
|
||||||
"permlevel": 0,
|
},
|
||||||
"print_hide": 0,
|
{
|
||||||
"print_hide_if_no_value": 0,
|
"fieldname": "currency",
|
||||||
"read_only": 1,
|
"fieldtype": "Link",
|
||||||
"report_hide": 0,
|
"hidden": 1,
|
||||||
"reqd": 0,
|
"label": "Currency",
|
||||||
"search_index": 0,
|
"options": "Currency"
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2016-07-11 03:28:03.588476",
|
"modified": "2020-07-19 18:12:27.964073",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Invoice",
|
"name": "Payment Reconciliation Invoice",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_seen": 0
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"creation": "2014-07-09 16:13:35.452759",
|
"creation": "2014-07-09 16:13:35.452759",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"reference_type",
|
"reference_type",
|
||||||
"reference_name",
|
"reference_name",
|
||||||
@ -16,7 +18,8 @@
|
|||||||
"difference_account",
|
"difference_account",
|
||||||
"difference_amount",
|
"difference_amount",
|
||||||
"sec_break1",
|
"sec_break1",
|
||||||
"remark"
|
"remark",
|
||||||
|
"currency"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -73,6 +76,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -81,6 +85,7 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Allocated amount",
|
"label": "Allocated amount",
|
||||||
|
"options": "currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -106,16 +111,25 @@
|
|||||||
"fieldname": "difference_amount",
|
"fieldname": "difference_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Difference Amount",
|
"label": "Difference Amount",
|
||||||
|
"options": "currency",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_10",
|
"fieldname": "section_break_10",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"modified": "2019-06-24 00:08:11.150796",
|
"links": [],
|
||||||
|
"modified": "2020-07-19 18:12:41.682347",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Payment",
|
"name": "Payment Reconciliation Payment",
|
||||||
|
|||||||
@ -211,7 +211,7 @@
|
|||||||
"label": "IBAN"
|
"label": "IBAN"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "bank.branch_code",
|
"fetch_from": "bank_account.branch_code",
|
||||||
"fetch_if_empty": 1,
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "branch_code",
|
"fieldname": "branch_code",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
@ -352,7 +352,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-29 17:38:49.392713",
|
"modified": "2020-07-17 14:06:42.185763",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Request",
|
"name": "Payment Request",
|
||||||
|
|||||||
@ -140,9 +140,6 @@ class PaymentRequest(Document):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def set_as_paid(self):
|
def set_as_paid(self):
|
||||||
if frappe.session.user == "Guest":
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
|
|
||||||
payment_entry = self.create_payment_entry()
|
payment_entry = self.create_payment_entry()
|
||||||
self.make_invoice()
|
self.make_invoice()
|
||||||
|
|
||||||
@ -254,7 +251,7 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
if status in ["Authorized", "Completed"]:
|
if status in ["Authorized", "Completed"]:
|
||||||
redirect_to = None
|
redirect_to = None
|
||||||
self.run_method("set_as_paid")
|
self.set_as_paid()
|
||||||
|
|
||||||
# if shopping cart enabled and in session
|
# if shopping cart enabled and in session
|
||||||
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
|
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
|
||||||
|
|||||||
@ -12,15 +12,15 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-left">{{ _('Grand Total') }}</td>
|
<td class="text-left font-bold">{{ _('Grand Total') }}</td>
|
||||||
<td class='text-right'>{{ data.grand_total or '' }} {{ currency.symbol }}</td>
|
<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-left">{{ _('Net Total') }}</td>
|
<td class="text-left font-bold">{{ _('Net Total') }}</td>
|
||||||
<td class='text-right'>{{ data.net_total or '' }} {{ currency.symbol }}</td>
|
<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-left">{{ _('Total Quantity') }}</td>
|
<td class="text-left font-bold">{{ _('Total Quantity') }}</td>
|
||||||
<td class='text-right'>{{ data.total_quantity or '' }}</td>
|
<td class='text-right'>{{ data.total_quantity or '' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@ -45,7 +45,7 @@
|
|||||||
{% for d in data.payment_reconciliation %}
|
{% for d in data.payment_reconciliation %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-left">{{ d.mode_of_payment }}</td>
|
<td class="text-left">{{ d.mode_of_payment }}</td>
|
||||||
<td class='text-right'>{{ d.expected_amount }} {{ currency.symbol }}</td>
|
<td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -55,12 +55,14 @@
|
|||||||
<!-- Section end -->
|
<!-- Section end -->
|
||||||
|
|
||||||
<!-- Taxes section -->
|
<!-- Taxes section -->
|
||||||
|
{% if data.taxes %}
|
||||||
<div>
|
<div>
|
||||||
<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Taxes") }}</h6>
|
<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Taxes") }}</h6>
|
||||||
<div class="tax-break-up" style="overflow-x: auto;">
|
<div class="tax-break-up" style="overflow-x: auto;">
|
||||||
<table class="table table-bordered table-hover">
|
<table class="table table-bordered table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="text-left">{{ _("Account") }}</th>
|
||||||
<th class="text-left">{{ _("Rate") }}</th>
|
<th class="text-left">{{ _("Rate") }}</th>
|
||||||
<th class="text-right">{{ _("Amount") }}</th>
|
<th class="text-right">{{ _("Amount") }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -68,14 +70,16 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for d in data.taxes %}
|
{% for d in data.taxes %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="text-left">{{ d.account_head }}</td>
|
||||||
<td class="text-left">{{ d.rate }} %</td>
|
<td class="text-left">{{ d.rate }} %</td>
|
||||||
<td class='text-right'>{{ d.amount }} {{ currency.symbol }}</td>
|
<td class='text-right'> {{ frappe.utils.fmt_money(d.amount, currency=currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<!-- Section end -->
|
<!-- Section end -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
149
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
Normal file
149
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('POS Closing Entry', {
|
||||||
|
onload: function(frm) {
|
||||||
|
frm.set_query("pos_profile", function(doc) {
|
||||||
|
return {
|
||||||
|
filters: { 'user': doc.user }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("user", function(doc) {
|
||||||
|
return {
|
||||||
|
query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers",
|
||||||
|
filters: { 'parent': doc.pos_profile }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("pos_opening_entry", function(doc) {
|
||||||
|
return { filters: { 'status': 'Open', 'docstatus': 1 } };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime());
|
||||||
|
if (frm.doc.docstatus === 1) set_html_data(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
pos_opening_entry(frm) {
|
||||||
|
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
|
||||||
|
reset_values(frm);
|
||||||
|
frm.trigger("set_opening_amounts");
|
||||||
|
frm.trigger("get_pos_invoices");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set_opening_amounts(frm) {
|
||||||
|
frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
|
||||||
|
.then(({ balance_details }) => {
|
||||||
|
balance_details.forEach(detail => {
|
||||||
|
frm.add_child("payment_reconciliation", {
|
||||||
|
mode_of_payment: detail.mode_of_payment,
|
||||||
|
opening_amount: detail.opening_amount,
|
||||||
|
expected_amount: detail.opening_amount
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_pos_invoices(frm) {
|
||||||
|
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),
|
||||||
|
user: frm.doc.user
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
let pos_docs = r.message;
|
||||||
|
set_form_data(pos_docs, frm)
|
||||||
|
refresh_fields(frm)
|
||||||
|
set_html_data(frm)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frappe.ui.form.on('POS Closing Entry Detail', {
|
||||||
|
closing_amount: (frm, cdt, cdn) => {
|
||||||
|
const row = locals[cdt][cdn];
|
||||||
|
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function set_form_data(data, frm) {
|
||||||
|
data.forEach(d => {
|
||||||
|
add_to_pos_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);
|
||||||
|
add_to_payments(d, frm);
|
||||||
|
add_to_taxes(d, frm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_to_pos_transaction(d, frm) {
|
||||||
|
frm.add_child("pos_transactions", {
|
||||||
|
pos_invoice: d.name,
|
||||||
|
posting_date: d.posting_date,
|
||||||
|
grand_total: d.grand_total,
|
||||||
|
customer: d.customer
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_to_payments(d, frm) {
|
||||||
|
d.payments.forEach(p => {
|
||||||
|
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
|
||||||
|
if (payment) {
|
||||||
|
payment.expected_amount += flt(p.amount);
|
||||||
|
} else {
|
||||||
|
frm.add_child("payment_reconciliation", {
|
||||||
|
mode_of_payment: p.mode_of_payment,
|
||||||
|
opening_amount: 0,
|
||||||
|
expected_amount: p.amount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_to_taxes(d, frm) {
|
||||||
|
d.taxes.forEach(t => {
|
||||||
|
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
|
||||||
|
if (tax) {
|
||||||
|
tax.amount += flt(t.tax_amount);
|
||||||
|
} else {
|
||||||
|
frm.add_child("taxes", {
|
||||||
|
account_head: t.account_head,
|
||||||
|
rate: t.rate,
|
||||||
|
amount: t.tax_amount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset_values(frm) {
|
||||||
|
frm.set_value("pos_transactions", []);
|
||||||
|
frm.set_value("payment_reconciliation", []);
|
||||||
|
frm.set_value("taxes", []);
|
||||||
|
frm.set_value("grand_total", 0);
|
||||||
|
frm.set_value("net_total", 0);
|
||||||
|
frm.set_value("total_quantity", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh_fields(frm) {
|
||||||
|
frm.refresh_field("pos_transactions");
|
||||||
|
frm.refresh_field("payment_reconciliation");
|
||||||
|
frm.refresh_field("taxes");
|
||||||
|
frm.refresh_field("grand_total");
|
||||||
|
frm.refresh_field("net_total");
|
||||||
|
frm.refresh_field("total_quantity");
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_html_data(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "get_payment_reconciliation_details",
|
||||||
|
doc: frm.doc,
|
||||||
|
callback: (r) => {
|
||||||
|
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "POS-CLO-.YYYY.-.#####",
|
||||||
|
"creation": "2018-05-28 19:06:40.830043",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"period_start_date",
|
||||||
|
"period_end_date",
|
||||||
|
"column_break_3",
|
||||||
|
"posting_date",
|
||||||
|
"pos_opening_entry",
|
||||||
|
"section_break_5",
|
||||||
|
"company",
|
||||||
|
"column_break_7",
|
||||||
|
"pos_profile",
|
||||||
|
"user",
|
||||||
|
"section_break_12",
|
||||||
|
"pos_transactions",
|
||||||
|
"section_break_9",
|
||||||
|
"payment_reconciliation_details",
|
||||||
|
"section_break_11",
|
||||||
|
"payment_reconciliation",
|
||||||
|
"section_break_13",
|
||||||
|
"grand_total",
|
||||||
|
"net_total",
|
||||||
|
"total_quantity",
|
||||||
|
"column_break_16",
|
||||||
|
"taxes",
|
||||||
|
"section_break_14",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fetch_from": "pos_opening_entry.period_start_date",
|
||||||
|
"fieldname": "period_start_date",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Period Start Date",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "period_end_date",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Period End Date",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Posting Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_5",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_7",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "pos_opening_entry.pos_profile",
|
||||||
|
"fieldname": "pos_profile",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "POS Profile",
|
||||||
|
"options": "POS Profile",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "pos_opening_entry.user",
|
||||||
|
"fieldname": "user",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cashier",
|
||||||
|
"options": "User",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_9",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.docstatus==1",
|
||||||
|
"fieldname": "payment_reconciliation_details",
|
||||||
|
"fieldtype": "HTML"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_11",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Modes of Payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_reconciliation",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Payment Reconciliation",
|
||||||
|
"options": "POS Closing Entry Detail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "eval:doc.docstatus==0",
|
||||||
|
"fieldname": "section_break_13",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Grand Total",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "net_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Total",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total_quantity",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Total Quantity",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_16",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "taxes",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Taxes",
|
||||||
|
"options": "POS Closing Entry Taxes",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_12",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Linked Invoices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_14",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "POS Closing Entry",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pos_transactions",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "POS Transactions",
|
||||||
|
"options": "POS Invoice Reference",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pos_opening_entry",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "POS Opening Entry",
|
||||||
|
"options": "POS Opening Entry",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:03:22.226113",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Closing Entry",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Administrator",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
127
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
Normal file
127
erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
import json
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import getdate, get_datetime, flt
|
||||||
|
from collections import defaultdict
|
||||||
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||||
|
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
|
||||||
|
|
||||||
|
class POSClosingEntry(Document):
|
||||||
|
def validate(self):
|
||||||
|
user = frappe.get_all('POS Closing Entry',
|
||||||
|
filters = { 'user': self.user, 'docstatus': 1 },
|
||||||
|
or_filters = {
|
||||||
|
'period_start_date': ('between', [self.period_start_date, self.period_end_date]),
|
||||||
|
'period_end_date': ('between', [self.period_start_date, self.period_end_date])
|
||||||
|
})
|
||||||
|
|
||||||
|
if user:
|
||||||
|
frappe.throw(_("POS Closing Entry {} against {} between selected period"
|
||||||
|
.format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period"))
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
merge_pos_invoices(self.pos_transactions)
|
||||||
|
opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
|
||||||
|
opening_entry.pos_closing_entry = self.name
|
||||||
|
opening_entry.set_status()
|
||||||
|
opening_entry.save()
|
||||||
|
|
||||||
|
def get_payment_reconciliation_details(self):
|
||||||
|
currency = frappe.get_cached_value('Company', self.company, "default_currency")
|
||||||
|
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
|
||||||
|
{"data": self, "currency": currency})
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'])
|
||||||
|
return [c['user'] for c in cashiers_list]
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_pos_invoices(start, end, user):
|
||||||
|
data = frappe.db.sql("""
|
||||||
|
select
|
||||||
|
name, timestamp(posting_date, posting_time) as "timestamp"
|
||||||
|
from
|
||||||
|
`tabPOS Invoice`
|
||||||
|
where
|
||||||
|
owner = %s and docstatus = 1 and
|
||||||
|
(consolidated_invoice is NULL or consolidated_invoice = '')
|
||||||
|
""", (user), as_dict=1)
|
||||||
|
|
||||||
|
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
|
||||||
|
# need to get taxes and payments so can't avoid get_doc
|
||||||
|
data = [frappe.get_doc("POS 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
|
||||||
|
closing_entry.period_start_date = opening_entry.period_start_date
|
||||||
|
closing_entry.period_end_date = frappe.utils.get_datetime()
|
||||||
|
closing_entry.pos_profile = opening_entry.pos_profile
|
||||||
|
closing_entry.user = opening_entry.user
|
||||||
|
closing_entry.company = opening_entry.company
|
||||||
|
closing_entry.grand_total = 0
|
||||||
|
closing_entry.net_total = 0
|
||||||
|
closing_entry.total_quantity = 0
|
||||||
|
|
||||||
|
invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user)
|
||||||
|
|
||||||
|
pos_transactions = []
|
||||||
|
taxes = []
|
||||||
|
payments = []
|
||||||
|
for detail in opening_entry.balance_details:
|
||||||
|
payments.append(frappe._dict({
|
||||||
|
'mode_of_payment': detail.mode_of_payment,
|
||||||
|
'opening_amount': detail.opening_amount,
|
||||||
|
'expected_amount': detail.opening_amount
|
||||||
|
}))
|
||||||
|
|
||||||
|
for d in invoices:
|
||||||
|
pos_transactions.append(frappe._dict({
|
||||||
|
'pos_invoice': d.name,
|
||||||
|
'posting_date': d.posting_date,
|
||||||
|
'grand_total': d.grand_total,
|
||||||
|
'customer': d.customer
|
||||||
|
}))
|
||||||
|
closing_entry.grand_total += flt(d.grand_total)
|
||||||
|
closing_entry.net_total += flt(d.net_total)
|
||||||
|
closing_entry.total_quantity += flt(d.total_qty)
|
||||||
|
|
||||||
|
for t in d.taxes:
|
||||||
|
existing_tax = [tx for tx in taxes if tx.account_head == t.account_head and tx.rate == t.rate]
|
||||||
|
if existing_tax:
|
||||||
|
existing_tax[0].amount += flt(t.tax_amount);
|
||||||
|
else:
|
||||||
|
taxes.append(frappe._dict({
|
||||||
|
'account_head': t.account_head,
|
||||||
|
'rate': t.rate,
|
||||||
|
'amount': t.tax_amount
|
||||||
|
}))
|
||||||
|
|
||||||
|
for p in d.payments:
|
||||||
|
existing_pay = [pay for pay in payments if pay.mode_of_payment == p.mode_of_payment]
|
||||||
|
if existing_pay:
|
||||||
|
existing_pay[0].expected_amount += flt(p.amount);
|
||||||
|
else:
|
||||||
|
payments.append(frappe._dict({
|
||||||
|
'mode_of_payment': p.mode_of_payment,
|
||||||
|
'opening_amount': 0,
|
||||||
|
'expected_amount': p.amount
|
||||||
|
}))
|
||||||
|
|
||||||
|
closing_entry.set("pos_transactions", pos_transactions)
|
||||||
|
closing_entry.set("payment_reconciliation", payments)
|
||||||
|
closing_entry.set("taxes", taxes)
|
||||||
|
|
||||||
|
return closing_entry
|
||||||
@ -2,15 +2,15 @@
|
|||||||
// rename this file from _test_[name] to test_[name] to activate
|
// rename this file from _test_[name] to test_[name] to activate
|
||||||
// and remove above this line
|
// and remove above this line
|
||||||
|
|
||||||
QUnit.test("test: POS Closing Voucher", function (assert) {
|
QUnit.test("test: POS Closing Entry", function (assert) {
|
||||||
let done = assert.async();
|
let done = assert.async();
|
||||||
|
|
||||||
// number of asserts
|
// number of asserts
|
||||||
assert.expect(1);
|
assert.expect(1);
|
||||||
|
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
// insert a new POS Closing Voucher
|
// insert a new POS Closing Entry
|
||||||
() => frappe.tests.make('POS Closing Voucher', [
|
() => frappe.tests.make('POS Closing Entry', [
|
||||||
// values to be set
|
// values to be set
|
||||||
{key: 'value'}
|
{key: 'value'}
|
||||||
]),
|
]),
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||||
|
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening
|
||||||
|
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||||
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
|
|
||||||
|
class TestPOSClosingEntry(unittest.TestCase):
|
||||||
|
def test_pos_closing_entry(self):
|
||||||
|
test_user, pos_profile = init_user_and_profile()
|
||||||
|
|
||||||
|
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||||
|
|
||||||
|
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
|
||||||
|
pos_inv1.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
|
||||||
|
})
|
||||||
|
pos_inv1.submit()
|
||||||
|
|
||||||
|
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||||
|
pos_inv2.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||||
|
})
|
||||||
|
pos_inv2.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 = 6700
|
||||||
|
|
||||||
|
pcv_doc.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pcv_doc.total_quantity, 2)
|
||||||
|
self.assertEqual(pcv_doc.net_total, 6700)
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
|
||||||
|
def init_user_and_profile():
|
||||||
|
user = 'test@example.com'
|
||||||
|
test_user = frappe.get_doc('User', user)
|
||||||
|
|
||||||
|
roles = ("Accounts Manager", "Accounts User", "Sales Manager")
|
||||||
|
test_user.add_roles(*roles)
|
||||||
|
frappe.set_user(user)
|
||||||
|
|
||||||
|
pos_profile = make_pos_profile()
|
||||||
|
pos_profile.append('applicable_for_users', {
|
||||||
|
'default': 1,
|
||||||
|
'user': user
|
||||||
|
})
|
||||||
|
|
||||||
|
pos_profile.save()
|
||||||
|
|
||||||
|
return test_user, pos_profile
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2018-05-28 19:10:47.580174",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"mode_of_payment",
|
||||||
|
"opening_amount",
|
||||||
|
"closing_amount",
|
||||||
|
"expected_amount",
|
||||||
|
"difference"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "mode_of_payment",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Mode of Payment",
|
||||||
|
"options": "Mode of Payment",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "expected_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Expected Amount",
|
||||||
|
"options": "company:company_currency",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "difference",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Difference",
|
||||||
|
"options": "company:company_currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "opening_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Opening Amount",
|
||||||
|
"options": "company:company_currency",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "closing_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Closing Amount",
|
||||||
|
"options": "company:company_currency",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:03:34.533607",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Closing Entry Detail",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@ -5,5 +5,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class POSClosingVoucherTaxes(Document):
|
class POSClosingEntryDetail(Document):
|
||||||
pass
|
pass
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2018-05-30 09:11:22.535470",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"account_head",
|
||||||
|
"rate",
|
||||||
|
"amount"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "rate",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Rate",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Amount",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_head",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Account Head",
|
||||||
|
"options": "Account",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:03:39.872884",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Closing Entry Taxes",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@ -5,5 +5,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class POSClosingVoucherDetails(Document):
|
class POSClosingEntryTaxes(Document):
|
||||||
pass
|
pass
|
||||||
205
erpnext/accounts/doctype/pos_invoice/pos_invoice.js
Normal file
205
erpnext/accounts/doctype/pos_invoice/pos_invoice.js
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
{% include 'erpnext/selling/sales_common.js' %};
|
||||||
|
|
||||||
|
erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({
|
||||||
|
setup(doc) {
|
||||||
|
this.setup_posting_date_time_check();
|
||||||
|
this._super(doc);
|
||||||
|
},
|
||||||
|
|
||||||
|
onload() {
|
||||||
|
this._super();
|
||||||
|
if(this.frm.doc.__islocal && this.frm.doc.is_pos) {
|
||||||
|
//Load pos profile data on the invoice if the default value of Is POS is 1
|
||||||
|
|
||||||
|
me.frm.script_manager.trigger("is_pos");
|
||||||
|
me.frm.refresh_fields();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh(doc) {
|
||||||
|
this._super();
|
||||||
|
if (doc.docstatus == 1 && !doc.is_return) {
|
||||||
|
if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||||
|
cur_frm.add_custom_button(__('Return'),
|
||||||
|
this.make_sales_return, __('Create'));
|
||||||
|
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.frm.doc.is_return) {
|
||||||
|
this.frm.return_print_format = "Sales Invoice Return";
|
||||||
|
cur_frm.set_value('consolidated_invoice', '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
is_pos: function(frm){
|
||||||
|
this.set_pos_data();
|
||||||
|
},
|
||||||
|
|
||||||
|
set_pos_data: function() {
|
||||||
|
if(this.frm.doc.is_pos) {
|
||||||
|
this.frm.set_value("allocate_advances_automatically", 0);
|
||||||
|
if(!this.frm.doc.company) {
|
||||||
|
this.frm.set_value("is_pos", 0);
|
||||||
|
frappe.msgprint(__("Please specify Company to proceed"));
|
||||||
|
} else {
|
||||||
|
var me = this;
|
||||||
|
return this.frm.call({
|
||||||
|
doc: me.frm.doc,
|
||||||
|
method: "set_missing_values",
|
||||||
|
callback: function(r) {
|
||||||
|
if(!r.exc) {
|
||||||
|
if(r.message) {
|
||||||
|
me.frm.pos_print_format = r.message.print_format || "";
|
||||||
|
me.frm.meta.default_print_format = r.message.print_format || "";
|
||||||
|
me.frm.allow_edit_rate = r.message.allow_edit_rate;
|
||||||
|
me.frm.allow_edit_discount = r.message.allow_edit_discount;
|
||||||
|
me.frm.doc.campaign = r.message.campaign;
|
||||||
|
me.frm.allow_print_before_pay = r.message.allow_print_before_pay;
|
||||||
|
}
|
||||||
|
me.frm.script_manager.trigger("update_stock");
|
||||||
|
me.calculate_taxes_and_totals();
|
||||||
|
if(me.frm.doc.taxes_and_charges) {
|
||||||
|
me.frm.script_manager.trigger("taxes_and_charges");
|
||||||
|
}
|
||||||
|
frappe.model.set_default_values(me.frm.doc);
|
||||||
|
me.set_dynamic_labels();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else this.frm.trigger("refresh");
|
||||||
|
},
|
||||||
|
|
||||||
|
customer() {
|
||||||
|
if (!this.frm.doc.customer) return
|
||||||
|
|
||||||
|
if (this.frm.doc.is_pos){
|
||||||
|
var pos_profile = this.frm.doc.pos_profile;
|
||||||
|
}
|
||||||
|
var me = this;
|
||||||
|
if(this.frm.updating_party_details) return;
|
||||||
|
erpnext.utils.get_party_details(this.frm,
|
||||||
|
"erpnext.accounts.party.get_party_details", {
|
||||||
|
posting_date: this.frm.doc.posting_date,
|
||||||
|
party: this.frm.doc.customer,
|
||||||
|
party_type: "Customer",
|
||||||
|
account: this.frm.doc.debit_to,
|
||||||
|
price_list: this.frm.doc.selling_price_list,
|
||||||
|
pos_profile: pos_profile
|
||||||
|
}, function() {
|
||||||
|
me.apply_pricing_rule();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
amount: function(){
|
||||||
|
this.write_off_outstanding_amount_automatically()
|
||||||
|
},
|
||||||
|
|
||||||
|
change_amount: function(){
|
||||||
|
if(this.frm.doc.paid_amount > this.frm.doc.grand_total){
|
||||||
|
this.calculate_write_off_amount();
|
||||||
|
}else {
|
||||||
|
this.frm.set_value("change_amount", 0.0);
|
||||||
|
this.frm.set_value("base_change_amount", 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frm.refresh_fields();
|
||||||
|
},
|
||||||
|
|
||||||
|
loyalty_amount: function(){
|
||||||
|
this.calculate_outstanding_amount();
|
||||||
|
this.frm.refresh_field("outstanding_amount");
|
||||||
|
this.frm.refresh_field("paid_amount");
|
||||||
|
this.frm.refresh_field("base_paid_amount");
|
||||||
|
},
|
||||||
|
|
||||||
|
write_off_outstanding_amount_automatically: function() {
|
||||||
|
if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) {
|
||||||
|
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]);
|
||||||
|
// this will make outstanding amount 0
|
||||||
|
this.frm.set_value("write_off_amount",
|
||||||
|
flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount"))
|
||||||
|
);
|
||||||
|
this.frm.toggle_enable("write_off_amount", false);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.frm.toggle_enable("write_off_amount", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calculate_outstanding_amount(false);
|
||||||
|
this.frm.refresh_fields();
|
||||||
|
},
|
||||||
|
|
||||||
|
make_sales_return: function() {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return",
|
||||||
|
frm: cur_frm
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
$.extend(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm }))
|
||||||
|
|
||||||
|
frappe.ui.form.on('POS Invoice', {
|
||||||
|
redeem_loyalty_points: function(frm) {
|
||||||
|
frm.events.get_loyalty_details(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
loyalty_points: function(frm) {
|
||||||
|
if (frm.redemption_conversion_factor) {
|
||||||
|
frm.events.set_loyalty_points(frm);
|
||||||
|
} else {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor",
|
||||||
|
args: {
|
||||||
|
"loyalty_program": frm.doc.loyalty_program
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r) {
|
||||||
|
frm.redemption_conversion_factor = r.message;
|
||||||
|
frm.events.set_loyalty_points(frm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get_loyalty_details: function(frm) {
|
||||||
|
if (frm.doc.customer && frm.doc.redeem_loyalty_points) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details",
|
||||||
|
args: {
|
||||||
|
"customer": frm.doc.customer,
|
||||||
|
"loyalty_program": frm.doc.loyalty_program,
|
||||||
|
"expiry_date": frm.doc.posting_date,
|
||||||
|
"company": frm.doc.company
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r) {
|
||||||
|
frm.set_value("loyalty_redemption_account", r.message.expense_account);
|
||||||
|
frm.set_value("loyalty_redemption_cost_center", r.message.cost_center);
|
||||||
|
frm.redemption_conversion_factor = r.message.conversion_factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set_loyalty_points: function(frm) {
|
||||||
|
if (frm.redemption_conversion_factor) {
|
||||||
|
let loyalty_amount = flt(frm.redemption_conversion_factor*flt(frm.doc.loyalty_points), precision("loyalty_amount"));
|
||||||
|
var remaining_amount = flt(frm.doc.grand_total) - flt(frm.doc.total_advance) - flt(frm.doc.write_off_amount);
|
||||||
|
if (frm.doc.grand_total && (remaining_amount < loyalty_amount)) {
|
||||||
|
let redeemable_points = parseInt(remaining_amount/frm.redemption_conversion_factor);
|
||||||
|
frappe.throw(__("You can only redeem max {0} points in this order.",[redeemable_points]));
|
||||||
|
}
|
||||||
|
frm.set_value("loyalty_amount", loyalty_amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
1637
erpnext/accounts/doctype/pos_invoice/pos_invoice.json
Normal file
1637
erpnext/accounts/doctype/pos_invoice/pos_invoice.json
Normal file
File diff suppressed because it is too large
Load Diff
374
erpnext/accounts/doctype/pos_invoice/pos_invoice.py
Normal file
374
erpnext/accounts/doctype/pos_invoice/pos_invoice.py
Normal file
@ -0,0 +1,374 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
|
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
|
||||||
|
from erpnext.accounts.utils import get_account_currency
|
||||||
|
from erpnext.accounts.party import get_party_account, get_due_date
|
||||||
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
|
||||||
|
get_loyalty_program_details_with_points, validate_loyalty_points
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
|
||||||
|
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
class POSInvoice(SalesInvoice):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(POSInvoice, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if not cint(self.is_pos):
|
||||||
|
frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")))
|
||||||
|
|
||||||
|
# run on validate method of selling controller
|
||||||
|
super(SalesInvoice, self).validate()
|
||||||
|
self.validate_auto_set_posting_time()
|
||||||
|
self.validate_pos_paid_amount()
|
||||||
|
self.validate_pos_return()
|
||||||
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
|
self.validate_uom_is_integer("uom", "qty")
|
||||||
|
self.validate_debit_to_acc()
|
||||||
|
self.validate_write_off_account()
|
||||||
|
self.validate_change_amount()
|
||||||
|
self.validate_change_account()
|
||||||
|
self.validate_item_cost_centers()
|
||||||
|
self.validate_serialised_or_batched_item()
|
||||||
|
self.validate_stock_availablility()
|
||||||
|
self.validate_return_items()
|
||||||
|
self.set_status()
|
||||||
|
self.set_account_for_mode_of_payment()
|
||||||
|
self.validate_pos()
|
||||||
|
self.verify_payment_amount()
|
||||||
|
self.validate_loyalty_transaction()
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
||||||
|
if self.loyalty_program:
|
||||||
|
self.make_loyalty_point_entry()
|
||||||
|
elif self.is_return and self.return_against and self.loyalty_program:
|
||||||
|
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
|
||||||
|
against_psi_doc.delete_loyalty_point_entry()
|
||||||
|
against_psi_doc.make_loyalty_point_entry()
|
||||||
|
if self.redeem_loyalty_points and self.loyalty_points:
|
||||||
|
self.apply_loyalty_points()
|
||||||
|
self.set_status(update=True)
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
# run on cancel method of selling controller
|
||||||
|
super(SalesInvoice, self).on_cancel()
|
||||||
|
if self.loyalty_program:
|
||||||
|
self.delete_loyalty_point_entry()
|
||||||
|
elif self.is_return and self.return_against and self.loyalty_program:
|
||||||
|
against_psi_doc = frappe.get_doc("POS Invoice", self.return_against)
|
||||||
|
against_psi_doc.delete_loyalty_point_entry()
|
||||||
|
against_psi_doc.make_loyalty_point_entry()
|
||||||
|
|
||||||
|
def validate_stock_availablility(self):
|
||||||
|
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||||
|
|
||||||
|
for d in self.get('items'):
|
||||||
|
if d.serial_no:
|
||||||
|
filters = {
|
||||||
|
"item_code": d.item_code,
|
||||||
|
"warehouse": d.warehouse,
|
||||||
|
"delivery_document_no": "",
|
||||||
|
"sales_invoice": ""
|
||||||
|
}
|
||||||
|
if d.batch_no:
|
||||||
|
filters["batch_no"] = d.batch_no
|
||||||
|
reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||||
|
serial_nos = d.serial_no.split("\n")
|
||||||
|
serial_nos = ' '.join(serial_nos).split() # remove whitespaces
|
||||||
|
invalid_serial_nos = []
|
||||||
|
for s in serial_nos:
|
||||||
|
if s in reserved_serial_nos:
|
||||||
|
invalid_serial_nos.append(s)
|
||||||
|
|
||||||
|
if len(invalid_serial_nos):
|
||||||
|
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
|
||||||
|
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
|
||||||
|
Please select valid serial no.".format(d.idx, multiple_nos,
|
||||||
|
frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
|
||||||
|
else:
|
||||||
|
if allow_negative_stock:
|
||||||
|
return
|
||||||
|
|
||||||
|
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||||
|
if not (flt(available_stock) > 0):
|
||||||
|
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.'
|
||||||
|
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
|
||||||
|
elif flt(available_stock) < flt(d.qty):
|
||||||
|
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
|
||||||
|
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
|
||||||
|
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
|
||||||
|
|
||||||
|
def validate_serialised_or_batched_item(self):
|
||||||
|
for d in self.get("items"):
|
||||||
|
serialized = d.get("has_serial_no")
|
||||||
|
batched = d.get("has_batch_no")
|
||||||
|
no_serial_selected = not d.get("serial_no")
|
||||||
|
no_batch_selected = not d.get("batch_no")
|
||||||
|
|
||||||
|
|
||||||
|
if serialized and batched and (no_batch_selected or no_serial_selected):
|
||||||
|
frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.'
|
||||||
|
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
||||||
|
if serialized and no_serial_selected:
|
||||||
|
frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.'
|
||||||
|
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
||||||
|
if batched and no_batch_selected:
|
||||||
|
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.'
|
||||||
|
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
||||||
|
|
||||||
|
def validate_return_items(self):
|
||||||
|
if not self.get("is_return"): return
|
||||||
|
|
||||||
|
for d in self.get("items"):
|
||||||
|
if d.get("qty") > 0:
|
||||||
|
frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
|
||||||
|
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||||
|
|
||||||
|
def validate_pos_paid_amount(self):
|
||||||
|
if len(self.payments) == 0 and self.is_pos:
|
||||||
|
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||||
|
|
||||||
|
def validate_change_account(self):
|
||||||
|
if frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company:
|
||||||
|
frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company))
|
||||||
|
|
||||||
|
def validate_change_amount(self):
|
||||||
|
grand_total = flt(self.rounded_total) or flt(self.grand_total)
|
||||||
|
base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total)
|
||||||
|
if not flt(self.change_amount) and grand_total < flt(self.paid_amount):
|
||||||
|
self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount))
|
||||||
|
self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount))
|
||||||
|
|
||||||
|
if flt(self.change_amount) and not self.account_for_change_amount:
|
||||||
|
msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
|
||||||
|
|
||||||
|
def verify_payment_amount(self):
|
||||||
|
for entry in self.payments:
|
||||||
|
if not self.is_return and entry.amount < 0:
|
||||||
|
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
|
||||||
|
if self.is_return and entry.amount > 0:
|
||||||
|
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
||||||
|
|
||||||
|
def validate_pos_return(self):
|
||||||
|
if self.is_pos and self.is_return:
|
||||||
|
total_amount_in_payments = 0
|
||||||
|
for payment in self.payments:
|
||||||
|
total_amount_in_payments += payment.amount
|
||||||
|
invoice_total = self.rounded_total or self.grand_total
|
||||||
|
if total_amount_in_payments < invoice_total:
|
||||||
|
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
|
||||||
|
|
||||||
|
def validate_loyalty_transaction(self):
|
||||||
|
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
|
||||||
|
expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"])
|
||||||
|
if not self.loyalty_redemption_account:
|
||||||
|
self.loyalty_redemption_account = expense_account
|
||||||
|
if not self.loyalty_redemption_cost_center:
|
||||||
|
self.loyalty_redemption_cost_center = cost_center
|
||||||
|
|
||||||
|
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points:
|
||||||
|
validate_loyalty_points(self, self.loyalty_points)
|
||||||
|
|
||||||
|
def set_status(self, update=False, status=None, update_modified=True):
|
||||||
|
if self.is_new():
|
||||||
|
if self.get('amended_from'):
|
||||||
|
self.status = 'Draft'
|
||||||
|
return
|
||||||
|
|
||||||
|
if not status:
|
||||||
|
if self.docstatus == 2:
|
||||||
|
status = "Cancelled"
|
||||||
|
elif self.docstatus == 1:
|
||||||
|
if self.consolidated_invoice:
|
||||||
|
self.status = "Consolidated"
|
||||||
|
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
|
||||||
|
self.status = "Overdue and Discounted"
|
||||||
|
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()):
|
||||||
|
self.status = "Overdue"
|
||||||
|
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed':
|
||||||
|
self.status = "Unpaid and Discounted"
|
||||||
|
elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()):
|
||||||
|
self.status = "Unpaid"
|
||||||
|
elif flt(self.outstanding_amount) <= 0 and self.is_return == 0 and frappe.db.get_value('POS Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
||||||
|
self.status = "Credit Note Issued"
|
||||||
|
elif self.is_return == 1:
|
||||||
|
self.status = "Return"
|
||||||
|
elif flt(self.outstanding_amount)<=0:
|
||||||
|
self.status = "Paid"
|
||||||
|
else:
|
||||||
|
self.status = "Submitted"
|
||||||
|
else:
|
||||||
|
self.status = "Draft"
|
||||||
|
|
||||||
|
if update:
|
||||||
|
self.db_set('status', self.status, update_modified = update_modified)
|
||||||
|
|
||||||
|
def set_pos_fields(self, for_validate=False):
|
||||||
|
"""Set retail related fields from POS Profiles"""
|
||||||
|
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
|
||||||
|
if not self.pos_profile:
|
||||||
|
pos_profile = get_pos_profile(self.company) or {}
|
||||||
|
self.pos_profile = pos_profile.get('name')
|
||||||
|
|
||||||
|
pos = {}
|
||||||
|
if self.pos_profile:
|
||||||
|
pos = frappe.get_doc('POS Profile', self.pos_profile)
|
||||||
|
|
||||||
|
if not self.get('payments') and not for_validate:
|
||||||
|
update_multi_mode_option(self, pos)
|
||||||
|
|
||||||
|
if not self.account_for_change_amount:
|
||||||
|
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
||||||
|
|
||||||
|
if pos:
|
||||||
|
if not for_validate:
|
||||||
|
self.tax_category = pos.get("tax_category")
|
||||||
|
|
||||||
|
if not for_validate and not self.customer:
|
||||||
|
self.customer = pos.customer
|
||||||
|
|
||||||
|
self.ignore_pricing_rule = pos.ignore_pricing_rule
|
||||||
|
if pos.get('account_for_change_amount'):
|
||||||
|
self.account_for_change_amount = pos.get('account_for_change_amount')
|
||||||
|
if pos.get('warehouse'):
|
||||||
|
self.set_warehouse = pos.get('warehouse')
|
||||||
|
|
||||||
|
for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name',
|
||||||
|
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
|
||||||
|
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
|
||||||
|
if (not for_validate) or (for_validate and not self.get(fieldname)):
|
||||||
|
self.set(fieldname, pos.get(fieldname))
|
||||||
|
|
||||||
|
if pos.get("company_address"):
|
||||||
|
self.company_address = pos.get("company_address")
|
||||||
|
|
||||||
|
if self.customer:
|
||||||
|
customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
|
||||||
|
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
|
||||||
|
selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
|
||||||
|
else:
|
||||||
|
selling_price_list = pos.get('selling_price_list')
|
||||||
|
|
||||||
|
if selling_price_list:
|
||||||
|
self.set('selling_price_list', selling_price_list)
|
||||||
|
|
||||||
|
if not for_validate:
|
||||||
|
self.update_stock = cint(pos.get("update_stock"))
|
||||||
|
|
||||||
|
# set pos values in items
|
||||||
|
for item in self.get("items"):
|
||||||
|
if item.get('item_code'):
|
||||||
|
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
|
||||||
|
for fname, val in iteritems(profile_details):
|
||||||
|
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||||
|
item.set(fname, val)
|
||||||
|
|
||||||
|
# fetch terms
|
||||||
|
if self.tc_name and not self.terms:
|
||||||
|
self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms")
|
||||||
|
|
||||||
|
# fetch charges
|
||||||
|
if self.taxes_and_charges and not len(self.get("taxes")):
|
||||||
|
self.set_taxes()
|
||||||
|
|
||||||
|
return pos
|
||||||
|
|
||||||
|
def set_missing_values(self, for_validate=False):
|
||||||
|
pos = self.set_pos_fields(for_validate)
|
||||||
|
|
||||||
|
if not self.debit_to:
|
||||||
|
self.debit_to = get_party_account("Customer", self.customer, self.company)
|
||||||
|
self.party_account_currency = frappe.db.get_value("Account", self.debit_to, "account_currency", cache=True)
|
||||||
|
if not self.due_date and self.customer:
|
||||||
|
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||||
|
|
||||||
|
super(SalesInvoice, self).set_missing_values(for_validate)
|
||||||
|
|
||||||
|
print_format = pos.get("print_format") if pos else None
|
||||||
|
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
|
||||||
|
print_format = 'POS Invoice'
|
||||||
|
|
||||||
|
if pos:
|
||||||
|
return {
|
||||||
|
"print_format": print_format,
|
||||||
|
"allow_edit_rate": pos.get("allow_user_to_edit_rate"),
|
||||||
|
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
|
||||||
|
"campaign": pos.get("campaign"),
|
||||||
|
"allow_print_before_pay": pos.get("allow_print_before_pay")
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_account_for_mode_of_payment(self):
|
||||||
|
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
|
||||||
|
for pay in self.payments:
|
||||||
|
if not pay.account:
|
||||||
|
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_stock_availability(item_code, warehouse):
|
||||||
|
latest_sle = frappe.db.sql("""select qty_after_transaction
|
||||||
|
from `tabStock Ledger Entry`
|
||||||
|
where item_code = %s and warehouse = %s
|
||||||
|
order by posting_date desc, posting_time desc
|
||||||
|
limit 1""", (item_code, warehouse), as_dict=1)
|
||||||
|
|
||||||
|
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
|
||||||
|
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||||
|
where p.name = p_item.parent
|
||||||
|
and p.consolidated_invoice is NULL
|
||||||
|
and p.docstatus = 1
|
||||||
|
and p_item.docstatus = 1
|
||||||
|
and p_item.item_code = %s
|
||||||
|
and p_item.warehouse = %s
|
||||||
|
""", (item_code, warehouse), as_dict=1)
|
||||||
|
|
||||||
|
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
|
||||||
|
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
|
||||||
|
|
||||||
|
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
|
||||||
|
return sle_qty - pos_sales_qty
|
||||||
|
else:
|
||||||
|
# when sle_qty is 0
|
||||||
|
# when sle_qty > 0 and pos_sales_qty is 0
|
||||||
|
return sle_qty
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_sales_return(source_name, target_doc=None):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
return make_return_doc("POS Invoice", source_name, target_doc)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_merge_log(invoices):
|
||||||
|
import json
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
if isinstance(invoices, string_types):
|
||||||
|
invoices = json.loads(invoices)
|
||||||
|
|
||||||
|
if len(invoices) == 0:
|
||||||
|
frappe.throw(_('Atleast one invoice has to be selected.'))
|
||||||
|
|
||||||
|
merge_log = frappe.new_doc("POS Invoice Merge Log")
|
||||||
|
merge_log.posting_date = getdate(nowdate())
|
||||||
|
for inv in invoices:
|
||||||
|
inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),
|
||||||
|
["customer", "posting_date", "grand_total"], as_dict=1)[0]
|
||||||
|
merge_log.customer = inv_data.customer
|
||||||
|
merge_log.append("pos_invoices", {
|
||||||
|
'pos_invoice': inv.get('name'),
|
||||||
|
'customer': inv_data.customer,
|
||||||
|
'posting_date': inv_data.posting_date,
|
||||||
|
'grand_total': inv_data.grand_total
|
||||||
|
})
|
||||||
|
|
||||||
|
if merge_log.get('pos_invoices'):
|
||||||
|
return merge_log.as_dict()
|
||||||
42
erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
Normal file
42
erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
// render
|
||||||
|
frappe.listview_settings['POS Invoice'] = {
|
||||||
|
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
||||||
|
"currency", "is_return"],
|
||||||
|
get_indicator: function(doc) {
|
||||||
|
var status_color = {
|
||||||
|
"Draft": "red",
|
||||||
|
"Unpaid": "orange",
|
||||||
|
"Paid": "green",
|
||||||
|
"Submitted": "blue",
|
||||||
|
"Consolidated": "green",
|
||||||
|
"Return": "darkgrey",
|
||||||
|
"Unpaid and Discounted": "orange",
|
||||||
|
"Overdue and Discounted": "red",
|
||||||
|
"Overdue": "red"
|
||||||
|
|
||||||
|
};
|
||||||
|
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||||
|
},
|
||||||
|
right_column: "grand_total",
|
||||||
|
onload: function(me) {
|
||||||
|
me.page.add_action_item('Make Merge Log', function() {
|
||||||
|
const invoices = me.get_checked_items();
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_merge_log",
|
||||||
|
freeze: true,
|
||||||
|
args:{
|
||||||
|
"invoices": invoices
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
var doc = frappe.model.sync(r.message)[0];
|
||||||
|
frappe.set_route("Form", doc.doctype, doc.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
324
erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
Normal file
324
erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest, copy, time
|
||||||
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
|
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||||
|
|
||||||
|
class TestPOSInvoice(unittest.TestCase):
|
||||||
|
def test_timestamp_change(self):
|
||||||
|
w = create_pos_invoice(do_not_save=1)
|
||||||
|
w.docstatus = 0
|
||||||
|
w.insert()
|
||||||
|
|
||||||
|
w2 = frappe.get_doc(w.doctype, w.name)
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
self.assertRaises(frappe.TimestampMismatchError, w2.save)
|
||||||
|
|
||||||
|
def test_change_naming_series(self):
|
||||||
|
inv = create_pos_invoice(do_not_submit=1)
|
||||||
|
inv.naming_series = 'TEST-'
|
||||||
|
|
||||||
|
self.assertRaises(frappe.CannotChangeConstantError, inv.save)
|
||||||
|
|
||||||
|
def test_discount_and_inclusive_tax(self):
|
||||||
|
inv = create_pos_invoice(qty=100, rate=50, do_not_save=1)
|
||||||
|
inv.append("taxes", {
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account Service Tax - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Service Tax",
|
||||||
|
"rate": 14,
|
||||||
|
'included_in_print_rate': 1
|
||||||
|
})
|
||||||
|
inv.insert()
|
||||||
|
|
||||||
|
self.assertEqual(inv.net_total, 4385.96)
|
||||||
|
self.assertEqual(inv.grand_total, 5000)
|
||||||
|
|
||||||
|
inv.reload()
|
||||||
|
|
||||||
|
inv.discount_amount = 100
|
||||||
|
inv.apply_discount_on = 'Net Total'
|
||||||
|
inv.payment_schedule = []
|
||||||
|
|
||||||
|
inv.save()
|
||||||
|
|
||||||
|
self.assertEqual(inv.net_total, 4285.96)
|
||||||
|
self.assertEqual(inv.grand_total, 4885.99)
|
||||||
|
|
||||||
|
inv.reload()
|
||||||
|
|
||||||
|
inv.discount_amount = 100
|
||||||
|
inv.apply_discount_on = 'Grand Total'
|
||||||
|
inv.payment_schedule = []
|
||||||
|
|
||||||
|
inv.save()
|
||||||
|
|
||||||
|
self.assertEqual(inv.net_total, 4298.25)
|
||||||
|
self.assertEqual(inv.grand_total, 4900.00)
|
||||||
|
|
||||||
|
def test_tax_calculation_with_multiple_items(self):
|
||||||
|
inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True)
|
||||||
|
item_row = inv.get("items")[0]
|
||||||
|
for qty in (54, 288, 144, 430):
|
||||||
|
item_row_copy = copy.deepcopy(item_row)
|
||||||
|
item_row_copy.qty = qty
|
||||||
|
inv.append("items", item_row_copy)
|
||||||
|
|
||||||
|
inv.append("taxes", {
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 19
|
||||||
|
})
|
||||||
|
inv.insert()
|
||||||
|
|
||||||
|
self.assertEqual(inv.net_total, 4600)
|
||||||
|
|
||||||
|
self.assertEqual(inv.get("taxes")[0].tax_amount, 874.0)
|
||||||
|
self.assertEqual(inv.get("taxes")[0].total, 5474.0)
|
||||||
|
|
||||||
|
self.assertEqual(inv.grand_total, 5474.0)
|
||||||
|
|
||||||
|
def test_tax_calculation_with_item_tax_template(self):
|
||||||
|
inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1)
|
||||||
|
item_row = inv.get("items")[0]
|
||||||
|
|
||||||
|
add_items = [
|
||||||
|
(54, '_Test Account Excise Duty @ 12'),
|
||||||
|
(288, '_Test Account Excise Duty @ 15'),
|
||||||
|
(144, '_Test Account Excise Duty @ 20'),
|
||||||
|
(430, '_Test Item Tax Template 1')
|
||||||
|
]
|
||||||
|
for qty, item_tax_template in add_items:
|
||||||
|
item_row_copy = copy.deepcopy(item_row)
|
||||||
|
item_row_copy.qty = qty
|
||||||
|
item_row_copy.item_tax_template = item_tax_template
|
||||||
|
inv.append("items", item_row_copy)
|
||||||
|
|
||||||
|
inv.append("taxes", {
|
||||||
|
"account_head": "_Test Account Excise Duty - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Excise Duty",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 11
|
||||||
|
})
|
||||||
|
inv.append("taxes", {
|
||||||
|
"account_head": "_Test Account Education Cess - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Education Cess",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 0
|
||||||
|
})
|
||||||
|
inv.append("taxes", {
|
||||||
|
"account_head": "_Test Account S&H Education Cess - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "S&H Education Cess",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 3
|
||||||
|
})
|
||||||
|
inv.insert()
|
||||||
|
|
||||||
|
self.assertEqual(inv.net_total, 4600)
|
||||||
|
|
||||||
|
self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41)
|
||||||
|
self.assertEqual(inv.get("taxes")[0].total, 5102.41)
|
||||||
|
|
||||||
|
self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80)
|
||||||
|
self.assertEqual(inv.get("taxes")[1].total, 5300.21)
|
||||||
|
|
||||||
|
self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36)
|
||||||
|
self.assertEqual(inv.get("taxes")[2].total, 5675.57)
|
||||||
|
|
||||||
|
self.assertEqual(inv.grand_total, 5675.57)
|
||||||
|
self.assertEqual(inv.rounding_adjustment, 0.43)
|
||||||
|
self.assertEqual(inv.rounded_total, 5676.0)
|
||||||
|
|
||||||
|
def test_tax_calculation_with_multiple_items_and_discount(self):
|
||||||
|
inv = create_pos_invoice(qty=1, rate=75, do_not_save=True)
|
||||||
|
item_row = inv.get("items")[0]
|
||||||
|
for rate in (500, 200, 100, 50, 50):
|
||||||
|
item_row_copy = copy.deepcopy(item_row)
|
||||||
|
item_row_copy.price_list_rate = rate
|
||||||
|
item_row_copy.rate = rate
|
||||||
|
inv.append("items", item_row_copy)
|
||||||
|
|
||||||
|
inv.apply_discount_on = "Net Total"
|
||||||
|
inv.discount_amount = 75.0
|
||||||
|
|
||||||
|
inv.append("taxes", {
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "VAT",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 24
|
||||||
|
})
|
||||||
|
inv.insert()
|
||||||
|
|
||||||
|
self.assertEqual(inv.total, 975)
|
||||||
|
self.assertEqual(inv.net_total, 900)
|
||||||
|
|
||||||
|
self.assertEqual(inv.get("taxes")[0].tax_amount, 216.0)
|
||||||
|
self.assertEqual(inv.get("taxes")[0].total, 1116.0)
|
||||||
|
|
||||||
|
self.assertEqual(inv.grand_total, 1116.0)
|
||||||
|
|
||||||
|
def test_pos_returns_with_repayment(self):
|
||||||
|
pos = create_pos_invoice(qty = 10, do_not_save=True)
|
||||||
|
|
||||||
|
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
|
||||||
|
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
|
||||||
|
pos.insert()
|
||||||
|
pos.submit()
|
||||||
|
|
||||||
|
pos_return = make_sales_return(pos.name)
|
||||||
|
|
||||||
|
pos_return.insert()
|
||||||
|
pos_return.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pos_return.get('payments')[0].amount, -500)
|
||||||
|
self.assertEqual(pos_return.get('payments')[1].amount, -500)
|
||||||
|
|
||||||
|
def test_pos_change_amount(self):
|
||||||
|
pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
|
||||||
|
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
|
||||||
|
cost_center = "Main - _TC", do_not_save=True)
|
||||||
|
|
||||||
|
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50})
|
||||||
|
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60})
|
||||||
|
|
||||||
|
pos.insert()
|
||||||
|
pos.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pos.grand_total, 105.0)
|
||||||
|
self.assertEqual(pos.change_amount, 5.0)
|
||||||
|
|
||||||
|
def test_without_payment(self):
|
||||||
|
inv = create_pos_invoice(do_not_save=1)
|
||||||
|
# Check that the invoice cannot be submitted without payments
|
||||||
|
inv.payments = []
|
||||||
|
self.assertRaises(frappe.ValidationError, inv.insert)
|
||||||
|
|
||||||
|
def test_serialized_item_transaction(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
|
||||||
|
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||||
|
|
||||||
|
pos = create_pos_invoice(item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||||
|
pos.get("items")[0].serial_no = serial_nos[0]
|
||||||
|
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||||
|
|
||||||
|
pos.insert()
|
||||||
|
pos.submit()
|
||||||
|
|
||||||
|
pos2 = create_pos_invoice(item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||||
|
pos2.get("items")[0].serial_no = serial_nos[0]
|
||||||
|
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, pos2.insert)
|
||||||
|
|
||||||
|
def test_loyalty_points(self):
|
||||||
|
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
|
||||||
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
|
||||||
|
|
||||||
|
create_records()
|
||||||
|
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||||
|
before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty")
|
||||||
|
|
||||||
|
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||||
|
|
||||||
|
lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'POS Invoice', 'invoice': inv.name, 'customer': inv.customer})
|
||||||
|
after_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
|
||||||
|
|
||||||
|
self.assertEqual(inv.get('loyalty_program'), "Test Single Loyalty")
|
||||||
|
self.assertEqual(lpe.loyalty_points, 10)
|
||||||
|
self.assertEqual(after_lp_details.loyalty_points, before_lp_details.loyalty_points + 10)
|
||||||
|
|
||||||
|
inv.cancel()
|
||||||
|
after_cancel_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
|
||||||
|
self.assertEqual(after_cancel_lp_details.loyalty_points, before_lp_details.loyalty_points)
|
||||||
|
|
||||||
|
def test_loyalty_points_redeemption(self):
|
||||||
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
|
||||||
|
# add 10 loyalty points
|
||||||
|
create_pos_invoice(customer="Test Loyalty Customer", rate=10000)
|
||||||
|
|
||||||
|
before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty")
|
||||||
|
|
||||||
|
inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1)
|
||||||
|
inv.redeem_loyalty_points = 1
|
||||||
|
inv.loyalty_points = before_lp_details.loyalty_points
|
||||||
|
inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor
|
||||||
|
inv.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 10000 - inv.loyalty_amount})
|
||||||
|
inv.paid_amount = 10000
|
||||||
|
inv.submit()
|
||||||
|
|
||||||
|
after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
|
||||||
|
self.assertEqual(after_redeem_lp_details.loyalty_points, 9)
|
||||||
|
|
||||||
|
def create_pos_invoice(**args):
|
||||||
|
args = frappe._dict(args)
|
||||||
|
pos_profile = None
|
||||||
|
if not args.pos_profile:
|
||||||
|
pos_profile = make_pos_profile()
|
||||||
|
pos_profile.save()
|
||||||
|
|
||||||
|
pos_inv = frappe.new_doc("POS Invoice")
|
||||||
|
pos_inv.update_stock = 1
|
||||||
|
pos_inv.is_pos = 1
|
||||||
|
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
||||||
|
|
||||||
|
pos_inv.set_missing_values()
|
||||||
|
|
||||||
|
if args.posting_date:
|
||||||
|
pos_inv.set_posting_time = 1
|
||||||
|
pos_inv.posting_date = args.posting_date or frappe.utils.nowdate()
|
||||||
|
|
||||||
|
pos_inv.company = args.company or "_Test Company"
|
||||||
|
pos_inv.customer = args.customer or "_Test Customer"
|
||||||
|
pos_inv.debit_to = args.debit_to or "Debtors - _TC"
|
||||||
|
pos_inv.is_return = args.is_return
|
||||||
|
pos_inv.return_against = args.return_against
|
||||||
|
pos_inv.currency=args.currency or "INR"
|
||||||
|
pos_inv.conversion_rate = args.conversion_rate or 1
|
||||||
|
pos_inv.account_for_change_amount = "Cash - _TC"
|
||||||
|
|
||||||
|
pos_inv.append("items", {
|
||||||
|
"item_code": args.item or args.item_code or "_Test Item",
|
||||||
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
|
"qty": args.qty or 1,
|
||||||
|
"rate": args.rate if args.get("rate") is not None else 100,
|
||||||
|
"income_account": args.income_account or "Sales - _TC",
|
||||||
|
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||||
|
"serial_no": args.serial_no
|
||||||
|
})
|
||||||
|
|
||||||
|
if not args.do_not_save:
|
||||||
|
pos_inv.insert()
|
||||||
|
if not args.do_not_submit:
|
||||||
|
pos_inv.submit()
|
||||||
|
else:
|
||||||
|
pos_inv.payment_schedule = []
|
||||||
|
else:
|
||||||
|
pos_inv.payment_schedule = []
|
||||||
|
|
||||||
|
return pos_inv
|
||||||
805
erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
Normal file
805
erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
Normal file
@ -0,0 +1,805 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "hash",
|
||||||
|
"creation": "2020-01-27 13:04:55.229516",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"document_type": "Document",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"barcode",
|
||||||
|
"item_code",
|
||||||
|
"col_break1",
|
||||||
|
"item_name",
|
||||||
|
"customer_item_code",
|
||||||
|
"description_section",
|
||||||
|
"description",
|
||||||
|
"item_group",
|
||||||
|
"brand",
|
||||||
|
"image_section",
|
||||||
|
"image",
|
||||||
|
"image_view",
|
||||||
|
"quantity_and_rate",
|
||||||
|
"qty",
|
||||||
|
"stock_uom",
|
||||||
|
"col_break2",
|
||||||
|
"uom",
|
||||||
|
"conversion_factor",
|
||||||
|
"stock_qty",
|
||||||
|
"section_break_17",
|
||||||
|
"price_list_rate",
|
||||||
|
"base_price_list_rate",
|
||||||
|
"discount_and_margin",
|
||||||
|
"margin_type",
|
||||||
|
"margin_rate_or_amount",
|
||||||
|
"rate_with_margin",
|
||||||
|
"column_break_19",
|
||||||
|
"discount_percentage",
|
||||||
|
"discount_amount",
|
||||||
|
"base_rate_with_margin",
|
||||||
|
"section_break1",
|
||||||
|
"rate",
|
||||||
|
"amount",
|
||||||
|
"item_tax_template",
|
||||||
|
"col_break3",
|
||||||
|
"base_rate",
|
||||||
|
"base_amount",
|
||||||
|
"pricing_rules",
|
||||||
|
"is_free_item",
|
||||||
|
"section_break_21",
|
||||||
|
"net_rate",
|
||||||
|
"net_amount",
|
||||||
|
"column_break_24",
|
||||||
|
"base_net_rate",
|
||||||
|
"base_net_amount",
|
||||||
|
"drop_ship",
|
||||||
|
"delivered_by_supplier",
|
||||||
|
"accounting",
|
||||||
|
"income_account",
|
||||||
|
"is_fixed_asset",
|
||||||
|
"asset",
|
||||||
|
"finance_book",
|
||||||
|
"col_break4",
|
||||||
|
"expense_account",
|
||||||
|
"deferred_revenue",
|
||||||
|
"deferred_revenue_account",
|
||||||
|
"service_stop_date",
|
||||||
|
"enable_deferred_revenue",
|
||||||
|
"column_break_50",
|
||||||
|
"service_start_date",
|
||||||
|
"service_end_date",
|
||||||
|
"section_break_18",
|
||||||
|
"weight_per_unit",
|
||||||
|
"total_weight",
|
||||||
|
"column_break_21",
|
||||||
|
"weight_uom",
|
||||||
|
"warehouse_and_reference",
|
||||||
|
"warehouse",
|
||||||
|
"target_warehouse",
|
||||||
|
"quality_inspection",
|
||||||
|
"batch_no",
|
||||||
|
"col_break5",
|
||||||
|
"allow_zero_valuation_rate",
|
||||||
|
"serial_no",
|
||||||
|
"item_tax_rate",
|
||||||
|
"actual_batch_qty",
|
||||||
|
"actual_qty",
|
||||||
|
"edit_references",
|
||||||
|
"sales_order",
|
||||||
|
"so_detail",
|
||||||
|
"column_break_74",
|
||||||
|
"delivery_note",
|
||||||
|
"dn_detail",
|
||||||
|
"delivered_qty",
|
||||||
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
|
"section_break_54",
|
||||||
|
"page_break"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "barcode",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Barcode",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"columns": 4,
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item",
|
||||||
|
"oldfieldname": "item_code",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Item",
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "col_break1",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_global_search": 1,
|
||||||
|
"label": "Item Name",
|
||||||
|
"oldfieldname": "item_name",
|
||||||
|
"oldfieldtype": "Data",
|
||||||
|
"print_hide": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "customer_item_code",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Customer's Item Code",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "description_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Text Editor",
|
||||||
|
"label": "Description",
|
||||||
|
"oldfieldname": "description",
|
||||||
|
"oldfieldtype": "Text",
|
||||||
|
"print_width": "200px",
|
||||||
|
"reqd": 1,
|
||||||
|
"width": "200px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Item Group",
|
||||||
|
"oldfieldname": "item_group",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Item Group",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "brand",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Brand Name",
|
||||||
|
"oldfieldname": "brand",
|
||||||
|
"oldfieldtype": "Data",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "image_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Attach",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "image_view",
|
||||||
|
"fieldtype": "Image",
|
||||||
|
"label": "Image View",
|
||||||
|
"options": "image",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "quantity_and_rate",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Quantity",
|
||||||
|
"oldfieldname": "qty",
|
||||||
|
"oldfieldtype": "Currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Stock UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "col_break2",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "conversion_factor",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "UOM Conversion Factor",
|
||||||
|
"print_hide": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Qty as per Stock UOM",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_17",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "price_list_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Price List Rate",
|
||||||
|
"oldfieldname": "ref_rate",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_price_list_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Price List Rate (Company Currency)",
|
||||||
|
"oldfieldname": "base_ref_rate",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "discount_and_margin",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Discount and Margin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "price_list_rate",
|
||||||
|
"fieldname": "margin_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Margin Type",
|
||||||
|
"options": "\nPercentage\nAmount",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.margin_type && doc.price_list_rate",
|
||||||
|
"fieldname": "margin_rate_or_amount",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Margin Rate or Amount",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
|
||||||
|
"fieldname": "rate_with_margin",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Rate With Margin",
|
||||||
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_19",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "price_list_rate",
|
||||||
|
"fieldname": "discount_percentage",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"label": "Discount (%) on Price List Rate with Margin",
|
||||||
|
"oldfieldname": "adj_rate",
|
||||||
|
"oldfieldtype": "Float",
|
||||||
|
"precision": "2",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "price_list_rate",
|
||||||
|
"fieldname": "discount_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Discount Amount",
|
||||||
|
"options": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
|
||||||
|
"fieldname": "base_rate_with_margin",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Rate With Margin (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break1",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bold": 1,
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Rate",
|
||||||
|
"oldfieldname": "export_rate",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Amount",
|
||||||
|
"oldfieldname": "export_amount",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_tax_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item Tax Template",
|
||||||
|
"options": "Item Tax Template",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "col_break3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Rate (Company Currency)",
|
||||||
|
"oldfieldname": "basic_rate",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount (Company Currency)",
|
||||||
|
"oldfieldname": "amount",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pricing_rules",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Pricing Rules",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_free_item",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Free Item",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_21",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "net_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Rate",
|
||||||
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "net_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Amount",
|
||||||
|
"options": "currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_24",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_net_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Rate (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_net_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Net Amount (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "eval:doc.delivered_by_supplier==1",
|
||||||
|
"fieldname": "drop_ship",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Drop Ship"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "delivered_by_supplier",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Delivered By Supplier",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "accounting",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "income_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Income Account",
|
||||||
|
"oldfieldname": "income_account",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Account",
|
||||||
|
"print_hide": 1,
|
||||||
|
"print_width": "120px",
|
||||||
|
"reqd": 1,
|
||||||
|
"width": "120px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_fixed_asset",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Is Fixed Asset",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "asset",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Asset",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Asset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "asset",
|
||||||
|
"fieldname": "finance_book",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Finance Book",
|
||||||
|
"options": "Finance Book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "col_break4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "expense_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Expense Account",
|
||||||
|
"options": "Account",
|
||||||
|
"print_hide": 1,
|
||||||
|
"width": "120px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "deferred_revenue",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Deferred Revenue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_deferred_revenue",
|
||||||
|
"fieldname": "deferred_revenue_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Deferred Revenue Account",
|
||||||
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"depends_on": "enable_deferred_revenue",
|
||||||
|
"fieldname": "service_stop_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Service Stop Date",
|
||||||
|
"no_copy": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_deferred_revenue",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Deferred Revenue"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_50",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_deferred_revenue",
|
||||||
|
"fieldname": "service_start_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Service Start Date",
|
||||||
|
"no_copy": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_deferred_revenue",
|
||||||
|
"fieldname": "service_end_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Service End Date",
|
||||||
|
"no_copy": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "section_break_18",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Item Weight Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "weight_per_unit",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Weight Per Unit",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total_weight",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Total Weight",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_21",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "weight_uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Weight UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
|
||||||
|
"fieldname": "warehouse_and_reference",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Stock Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Warehouse",
|
||||||
|
"oldfieldname": "warehouse",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Warehouse",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "target_warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
|
"label": "Customer Warehouse (Optional)",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Warehouse",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.__islocal",
|
||||||
|
"fieldname": "quality_inspection",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Quality Inspection",
|
||||||
|
"options": "Quality Inspection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "batch_no",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Batch No",
|
||||||
|
"options": "Batch",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "col_break5",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "allow_zero_valuation_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Zero Valuation Rate",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "serial_no",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Serial No",
|
||||||
|
"oldfieldname": "serial_no",
|
||||||
|
"oldfieldtype": "Small Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_tax_rate",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Item Tax Rate",
|
||||||
|
"oldfieldname": "item_tax_rate",
|
||||||
|
"oldfieldtype": "Small Text",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "actual_batch_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Available Batch Qty at Warehouse",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"print_width": "150px",
|
||||||
|
"read_only": 1,
|
||||||
|
"width": "150px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "actual_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Available Qty at Warehouse",
|
||||||
|
"oldfieldname": "actual_qty",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "edit_references",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "References"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Sales Order",
|
||||||
|
"no_copy": 1,
|
||||||
|
"oldfieldname": "sales_order",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Sales Order",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "so_detail",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Sales Order Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"oldfieldname": "so_detail",
|
||||||
|
"oldfieldtype": "Data",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_74",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "delivery_note",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Delivery Note",
|
||||||
|
"no_copy": 1,
|
||||||
|
"oldfieldname": "delivery_note",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Delivery Note",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dn_detail",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Delivery Note Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"oldfieldname": "dn_detail",
|
||||||
|
"oldfieldtype": "Data",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "delivered_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Delivered Qty",
|
||||||
|
"oldfieldname": "delivered_qty",
|
||||||
|
"oldfieldtype": "Currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "accounting_dimensions_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Dimensions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": ":Company",
|
||||||
|
"fieldname": "cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cost Center",
|
||||||
|
"oldfieldname": "cost_center",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Cost Center",
|
||||||
|
"print_hide": 1,
|
||||||
|
"print_width": "120px",
|
||||||
|
"reqd": 1,
|
||||||
|
"width": "120px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimension_col_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_54",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "page_break",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Page Break",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"report_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-07-22 13:40:34.418346",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Invoice Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class POSInvoiceItem(Document):
|
||||||
|
pass
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('POS Invoice Merge Log', {
|
||||||
|
setup: function(frm) {
|
||||||
|
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
||||||
|
return{
|
||||||
|
filters: {
|
||||||
|
'docstatus': 1,
|
||||||
|
'customer': doc.customer,
|
||||||
|
'consolidated_invoice': ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-01-28 11:56:33.945372",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"posting_date",
|
||||||
|
"customer",
|
||||||
|
"section_break_3",
|
||||||
|
"pos_invoices",
|
||||||
|
"references_section",
|
||||||
|
"consolidated_invoice",
|
||||||
|
"column_break_7",
|
||||||
|
"consolidated_credit_note",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Posting Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "customer",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Customer",
|
||||||
|
"options": "Customer",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_3",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pos_invoices",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "POS Invoices",
|
||||||
|
"options": "POS Invoice Reference",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "references_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "References"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "POS Invoice Merge Log",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "consolidated_invoice",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Consolidated Sales Invoice",
|
||||||
|
"options": "Sales Invoice",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_7",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "consolidated_credit_note",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Consolidated Credit Note",
|
||||||
|
"options": "Sales Invoice",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:08:41.317100",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Invoice Merge Log",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Administrator",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@ -0,0 +1,180 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.model.mapper import map_doc
|
||||||
|
from frappe.model import default_fields
|
||||||
|
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
class POSInvoiceMergeLog(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_customer()
|
||||||
|
self.validate_pos_invoice_status()
|
||||||
|
|
||||||
|
def validate_customer(self):
|
||||||
|
for d in self.pos_invoices:
|
||||||
|
if d.customer != self.customer:
|
||||||
|
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
|
||||||
|
|
||||||
|
def validate_pos_invoice_status(self):
|
||||||
|
for d in self.pos_invoices:
|
||||||
|
status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus'])
|
||||||
|
if docstatus != 1:
|
||||||
|
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
|
||||||
|
if status in ['Consolidated']:
|
||||||
|
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||||
|
|
||||||
|
returns = [d for d in pos_invoice_docs if d.get('is_return') == 1]
|
||||||
|
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
|
||||||
|
|
||||||
|
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||||
|
|
||||||
|
if len(returns):
|
||||||
|
credit_note = self.process_merging_into_credit_note(returns)
|
||||||
|
else:
|
||||||
|
credit_note = ""
|
||||||
|
|
||||||
|
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||||
|
|
||||||
|
self.update_pos_invoices(sales_invoice, credit_note)
|
||||||
|
|
||||||
|
def process_merging_into_sales_invoice(self, data):
|
||||||
|
sales_invoice = self.get_new_sales_invoice()
|
||||||
|
|
||||||
|
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
|
||||||
|
|
||||||
|
sales_invoice.is_consolidated = 1
|
||||||
|
sales_invoice.save()
|
||||||
|
sales_invoice.submit()
|
||||||
|
self.consolidated_invoice = sales_invoice.name
|
||||||
|
|
||||||
|
return sales_invoice.name
|
||||||
|
|
||||||
|
def process_merging_into_credit_note(self, data):
|
||||||
|
credit_note = self.get_new_sales_invoice()
|
||||||
|
credit_note.is_return = 1
|
||||||
|
|
||||||
|
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||||
|
|
||||||
|
credit_note.is_consolidated = 1
|
||||||
|
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||||
|
credit_note.return_against = self.consolidated_invoice
|
||||||
|
credit_note.save()
|
||||||
|
credit_note.submit()
|
||||||
|
self.consolidated_credit_note = credit_note.name
|
||||||
|
|
||||||
|
return credit_note.name
|
||||||
|
|
||||||
|
def merge_pos_invoice_into(self, invoice, data):
|
||||||
|
items, payments, taxes = [], [], []
|
||||||
|
loyalty_amount_sum, loyalty_points_sum = 0, 0
|
||||||
|
for doc in data:
|
||||||
|
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
|
||||||
|
|
||||||
|
if doc.redeem_loyalty_points:
|
||||||
|
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
|
||||||
|
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
|
||||||
|
loyalty_points_sum += doc.loyalty_points
|
||||||
|
loyalty_amount_sum += doc.loyalty_amount
|
||||||
|
|
||||||
|
for item in doc.get('items'):
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
for tax in doc.get('taxes'):
|
||||||
|
found = False
|
||||||
|
for t in taxes:
|
||||||
|
if t.account_head == tax.account_head and t.cost_center == tax.cost_center and t.rate == tax.rate:
|
||||||
|
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount)
|
||||||
|
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount)
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
tax.charge_type = 'Actual'
|
||||||
|
taxes.append(tax)
|
||||||
|
|
||||||
|
for payment in doc.get('payments'):
|
||||||
|
found = False
|
||||||
|
for pay in payments:
|
||||||
|
if pay.account == payment.account and pay.mode_of_payment == payment.mode_of_payment:
|
||||||
|
pay.amount = flt(pay.amount) + flt(payment.amount)
|
||||||
|
pay.base_amount = flt(pay.base_amount) + flt(payment.base_amount)
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
payments.append(payment)
|
||||||
|
|
||||||
|
if loyalty_points_sum:
|
||||||
|
invoice.redeem_loyalty_points = 1
|
||||||
|
invoice.loyalty_points = loyalty_points_sum
|
||||||
|
invoice.loyalty_amount = loyalty_amount_sum
|
||||||
|
|
||||||
|
invoice.set('items', items)
|
||||||
|
invoice.set('payments', payments)
|
||||||
|
invoice.set('taxes', taxes)
|
||||||
|
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
def get_new_sales_invoice(self):
|
||||||
|
sales_invoice = frappe.new_doc('Sales Invoice')
|
||||||
|
sales_invoice.customer = self.customer
|
||||||
|
sales_invoice.is_pos = 1
|
||||||
|
# date can be pos closing date?
|
||||||
|
sales_invoice.posting_date = getdate(nowdate())
|
||||||
|
|
||||||
|
return sales_invoice
|
||||||
|
|
||||||
|
def update_pos_invoices(self, sales_invoice, credit_note):
|
||||||
|
for d in self.pos_invoices:
|
||||||
|
doc = frappe.get_doc('POS Invoice', d.pos_invoice)
|
||||||
|
if not doc.is_return:
|
||||||
|
doc.update({'consolidated_invoice': sales_invoice})
|
||||||
|
else:
|
||||||
|
doc.update({'consolidated_invoice': credit_note})
|
||||||
|
doc.set_status(update=True)
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
def get_all_invoices():
|
||||||
|
filters = {
|
||||||
|
'consolidated_invoice': [ 'in', [ '', None ]],
|
||||||
|
'status': ['not in', ['Consolidated']],
|
||||||
|
'docstatus': 1
|
||||||
|
}
|
||||||
|
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
|
||||||
|
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
|
||||||
|
|
||||||
|
return pos_invoices
|
||||||
|
|
||||||
|
def get_invoices_customer_map(pos_invoices):
|
||||||
|
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
|
||||||
|
pos_invoice_customer_map = {}
|
||||||
|
for invoice in pos_invoices:
|
||||||
|
customer = invoice.get('customer')
|
||||||
|
pos_invoice_customer_map.setdefault(customer, [])
|
||||||
|
pos_invoice_customer_map[customer].append(invoice)
|
||||||
|
|
||||||
|
return pos_invoice_customer_map
|
||||||
|
|
||||||
|
def merge_pos_invoices(pos_invoices=[]):
|
||||||
|
if not pos_invoices:
|
||||||
|
pos_invoices = get_all_invoices()
|
||||||
|
|
||||||
|
pos_invoice_map = get_invoices_customer_map(pos_invoices)
|
||||||
|
create_merge_logs(pos_invoice_map)
|
||||||
|
|
||||||
|
def create_merge_logs(pos_invoice_customer_map):
|
||||||
|
for customer, invoices in iteritems(pos_invoice_customer_map):
|
||||||
|
merge_log = frappe.new_doc('POS Invoice Merge Log')
|
||||||
|
merge_log.posting_date = getdate(nowdate())
|
||||||
|
merge_log.customer = customer
|
||||||
|
|
||||||
|
merge_log.set('pos_invoices', invoices)
|
||||||
|
merge_log.save(ignore_permissions=True)
|
||||||
|
merge_log.submit()
|
||||||
|
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||||
|
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||||
|
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
|
||||||
|
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||||
|
|
||||||
|
class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||||
|
def test_consolidated_invoice_creation(self):
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
test_user, pos_profile = init_user_and_profile()
|
||||||
|
|
||||||
|
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||||
|
pos_inv.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||||
|
})
|
||||||
|
pos_inv.submit()
|
||||||
|
|
||||||
|
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||||
|
pos_inv2.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||||
|
})
|
||||||
|
pos_inv2.submit()
|
||||||
|
|
||||||
|
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||||
|
pos_inv3.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
|
||||||
|
})
|
||||||
|
pos_inv3.submit()
|
||||||
|
|
||||||
|
merge_pos_invoices()
|
||||||
|
|
||||||
|
pos_inv.load_from_db()
|
||||||
|
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||||
|
|
||||||
|
pos_inv3.load_from_db()
|
||||||
|
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||||
|
|
||||||
|
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
def test_consolidated_credit_note_creation(self):
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
test_user, pos_profile = init_user_and_profile()
|
||||||
|
|
||||||
|
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||||
|
pos_inv.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||||
|
})
|
||||||
|
pos_inv.submit()
|
||||||
|
|
||||||
|
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||||
|
pos_inv2.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||||
|
})
|
||||||
|
pos_inv2.submit()
|
||||||
|
|
||||||
|
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||||
|
pos_inv3.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
|
||||||
|
})
|
||||||
|
pos_inv3.submit()
|
||||||
|
|
||||||
|
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||||
|
pos_inv_cn.set("payments", [])
|
||||||
|
pos_inv_cn.append('payments', {
|
||||||
|
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
|
||||||
|
})
|
||||||
|
pos_inv_cn.paid_amount = -300
|
||||||
|
pos_inv_cn.submit()
|
||||||
|
|
||||||
|
merge_pos_invoices()
|
||||||
|
|
||||||
|
pos_inv.load_from_db()
|
||||||
|
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||||
|
|
||||||
|
pos_inv3.load_from_db()
|
||||||
|
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||||
|
|
||||||
|
pos_inv_cn.load_from_db()
|
||||||
|
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
|
||||||
|
self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
|
||||||
|
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-01-28 11:54:47.149392",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"pos_invoice",
|
||||||
|
"posting_date",
|
||||||
|
"column_break_3",
|
||||||
|
"customer",
|
||||||
|
"grand_total"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "pos_invoice",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "POS Invoice",
|
||||||
|
"options": "POS Invoice",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "pos_invoice.customer",
|
||||||
|
"fieldname": "customer",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Customer",
|
||||||
|
"options": "Customer",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "pos_invoice.posting_date",
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "pos_invoice.grand_total",
|
||||||
|
"fieldname": "grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Amount",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:08:42.194979",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Invoice Reference",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class POSInvoiceReference(Document):
|
||||||
|
pass
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('POS Opening Entry', {
|
||||||
|
setup(frm) {
|
||||||
|
if (frm.doc.docstatus == 0) {
|
||||||
|
frm.trigger('set_posting_date_read_only');
|
||||||
|
frm.set_value('period_start_date', frappe.datetime.now_datetime());
|
||||||
|
frm.set_value('user', frappe.session.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.set_query("user", function(doc) {
|
||||||
|
return {
|
||||||
|
query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers",
|
||||||
|
filters: { 'parent': doc.pos_profile }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh(frm) {
|
||||||
|
// set default posting date / time
|
||||||
|
if(frm.doc.docstatus == 0) {
|
||||||
|
if(!frm.doc.posting_date) {
|
||||||
|
frm.set_value('posting_date', frappe.datetime.nowdate());
|
||||||
|
}
|
||||||
|
frm.trigger('set_posting_date_read_only');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set_posting_date_read_only(frm) {
|
||||||
|
if(frm.doc.docstatus == 0 && frm.doc.set_posting_date) {
|
||||||
|
frm.set_df_property('posting_date', 'read_only', 0);
|
||||||
|
} else {
|
||||||
|
frm.set_df_property('posting_date', 'read_only', 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set_posting_date(frm) {
|
||||||
|
frm.trigger('set_posting_date_read_only');
|
||||||
|
},
|
||||||
|
|
||||||
|
pos_profile: (frm) => {
|
||||||
|
if (frm.doc.pos_profile) {
|
||||||
|
frappe.db.get_doc("POS Profile", frm.doc.pos_profile)
|
||||||
|
.then(({ payments }) => {
|
||||||
|
if (payments.length) {
|
||||||
|
frm.doc.balance_details = [];
|
||||||
|
payments.forEach(({ mode_of_payment }) => {
|
||||||
|
frm.add_child("balance_details", { mode_of_payment });
|
||||||
|
})
|
||||||
|
frm.refresh_field("balance_details");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,185 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "POS-OPE-.YYYY.-.#####",
|
||||||
|
"creation": "2020-03-05 16:58:53.083708",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"period_start_date",
|
||||||
|
"period_end_date",
|
||||||
|
"status",
|
||||||
|
"column_break_3",
|
||||||
|
"posting_date",
|
||||||
|
"set_posting_date",
|
||||||
|
"section_break_5",
|
||||||
|
"company",
|
||||||
|
"pos_profile",
|
||||||
|
"pos_closing_entry",
|
||||||
|
"column_break_7",
|
||||||
|
"user",
|
||||||
|
"opening_balance_details_section",
|
||||||
|
"balance_details",
|
||||||
|
"section_break_9",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "period_start_date",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Period Start Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "period_end_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Period End Date",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Posting Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_5",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pos_profile",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "POS Profile",
|
||||||
|
"options": "POS Profile",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_7",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "user",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cashier",
|
||||||
|
"options": "User",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_9",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "POS Opening Entry",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "set_posting_date",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Set Posting Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"default": "Draft",
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Draft\nOpen\nClosed\nCancelled",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "pos_closing_entry",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "POS Closing Entry",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "opening_balance_details_section",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "balance_details",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Opening Balance Details",
|
||||||
|
"options": "POS Opening Entry Detail",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:08:40.955310",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Opening Entry",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Administrator",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from erpnext.controllers.status_updater import StatusUpdater
|
||||||
|
|
||||||
|
class POSOpeningEntry(StatusUpdater):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_pos_profile_and_cashier()
|
||||||
|
self.set_status()
|
||||||
|
|
||||||
|
def validate_pos_profile_and_cashier(self):
|
||||||
|
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
|
||||||
|
frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company)))
|
||||||
|
|
||||||
|
if not cint(frappe.db.get_value("User", self.user, "enabled")):
|
||||||
|
frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user)))
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
self.set_status(update=True)
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
// render
|
||||||
|
frappe.listview_settings['POS Opening Entry'] = {
|
||||||
|
get_indicator: function(doc) {
|
||||||
|
var status_color = {
|
||||||
|
"Draft": "grey",
|
||||||
|
"Open": "orange",
|
||||||
|
"Closed": "green",
|
||||||
|
"Cancelled": "red"
|
||||||
|
|
||||||
|
};
|
||||||
|
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestPOSOpeningEntry(unittest.TestCase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_opening_entry(pos_profile, user):
|
||||||
|
entry = frappe.new_doc("POS Opening Entry")
|
||||||
|
entry.pos_profile = pos_profile.name
|
||||||
|
entry.user = user
|
||||||
|
entry.company = pos_profile.company
|
||||||
|
entry.period_start_date = frappe.utils.get_datetime()
|
||||||
|
|
||||||
|
balance_details = [];
|
||||||
|
for d in pos_profile.payments:
|
||||||
|
balance_details.append(frappe._dict({
|
||||||
|
'mode_of_payment': d.mode_of_payment
|
||||||
|
}))
|
||||||
|
|
||||||
|
entry.set("balance_details", balance_details)
|
||||||
|
entry.submit()
|
||||||
|
|
||||||
|
return entry.as_dict()
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-04-28 16:44:32.440794",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"mode_of_payment",
|
||||||
|
"opening_amount"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "mode_of_payment",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Mode of Payment",
|
||||||
|
"options": "Mode of Payment",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "opening_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Opening Amount",
|
||||||
|
"options": "company:company_currency",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:08:41.949378",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Opening Entry Detail",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class POSOpeningEntryDetail(Document):
|
||||||
|
pass
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-04-30 14:37:08.148707",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"default",
|
||||||
|
"mode_of_payment"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:parent.doctype == 'POS Profile'",
|
||||||
|
"fieldname": "default",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "mode_of_payment",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Mode of Payment",
|
||||||
|
"options": "Mode of Payment",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-05-29 15:08:41.704844",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "POS Payment Method",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class POSPaymentMethod(Document):
|
||||||
|
pass
|
||||||
@ -28,7 +28,7 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) {
|
|||||||
|
|
||||||
frappe.ui.form.on('POS Profile', {
|
frappe.ui.form.on('POS Profile', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.set_query("print_format_for_online", function() {
|
frm.set_query("print_format", function() {
|
||||||
return {
|
return {
|
||||||
filters: [
|
filters: [
|
||||||
['Print Format', 'doc_type', '=', 'Sales Invoice'],
|
['Print Format', 'doc_type', '=', 'Sales Invoice'],
|
||||||
@ -49,12 +49,6 @@ frappe.ui.form.on('POS Profile', {
|
|||||||
return { filters: { doc_type: "Sales Invoice", print_format_type: "JS"} };
|
return { filters: { doc_type: "Sales Invoice", print_format_type: "JS"} };
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.db.get_value('POS Settings', 'POS Settings', 'use_pos_in_offline_mode', (r) => {
|
|
||||||
const is_offline = r && cint(r.use_pos_in_offline_mode)
|
|
||||||
frm.toggle_display('offline_pos_section', is_offline);
|
|
||||||
frm.toggle_display('print_format_for_online', !is_offline);
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.set_query('company_address', function(doc) {
|
frm.set_query('company_address', function(doc) {
|
||||||
if(!doc.company) {
|
if(!doc.company) {
|
||||||
frappe.throw(__('Please set Company'));
|
frappe.throw(__('Please set Company'));
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "Prompt",
|
"autoname": "Prompt",
|
||||||
"creation": "2013-05-24 12:15:51",
|
"creation": "2013-05-24 12:15:51",
|
||||||
@ -11,17 +12,12 @@
|
|||||||
"customer",
|
"customer",
|
||||||
"company",
|
"company",
|
||||||
"country",
|
"country",
|
||||||
"warehouse",
|
|
||||||
"campaign",
|
|
||||||
"company_address",
|
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
"update_stock",
|
"update_stock",
|
||||||
"ignore_pricing_rule",
|
"ignore_pricing_rule",
|
||||||
"allow_delete",
|
"warehouse",
|
||||||
"allow_user_to_edit_rate",
|
"campaign",
|
||||||
"allow_user_to_edit_discount",
|
"company_address",
|
||||||
"allow_print_before_pay",
|
|
||||||
"display_items_in_stock",
|
|
||||||
"section_break_15",
|
"section_break_15",
|
||||||
"applicable_for_users",
|
"applicable_for_users",
|
||||||
"section_break_11",
|
"section_break_11",
|
||||||
@ -31,16 +27,11 @@
|
|||||||
"column_break_16",
|
"column_break_16",
|
||||||
"customer_groups",
|
"customer_groups",
|
||||||
"section_break_16",
|
"section_break_16",
|
||||||
"print_format_for_online",
|
"print_format",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"column_break0",
|
"column_break0",
|
||||||
"tc_name",
|
"tc_name",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"offline_pos_section",
|
|
||||||
"territory",
|
|
||||||
"column_break_31",
|
|
||||||
"print_format",
|
|
||||||
"customer_group",
|
|
||||||
"section_break_19",
|
"section_break_19",
|
||||||
"selling_price_list",
|
"selling_price_list",
|
||||||
"currency",
|
"currency",
|
||||||
@ -104,15 +95,6 @@
|
|||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"label": "Country"
|
"label": "Country"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "update_stock",
|
|
||||||
"fieldname": "warehouse",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Warehouse",
|
|
||||||
"oldfieldname": "warehouse",
|
|
||||||
"oldfieldtype": "Link",
|
|
||||||
"options": "Warehouse"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "campaign",
|
"fieldname": "campaign",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -129,48 +111,6 @@
|
|||||||
"fieldname": "column_break_9",
|
"fieldname": "column_break_9",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"fieldname": "update_stock",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Update Stock"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "ignore_pricing_rule",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Ignore Pricing Rule"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "allow_delete",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Allow Delete"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "allow_user_to_edit_rate",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Allow user to edit Rate"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "allow_user_to_edit_discount",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Allow user to edit Discount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "allow_print_before_pay",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Allow Print Before Pay"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "display_items_in_stock",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Display Items In Stock"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_15",
|
"fieldname": "section_break_15",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -185,13 +125,13 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_11",
|
"fieldname": "section_break_11",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Mode of Payment"
|
"label": "Payment Methods"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "payments",
|
"fieldname": "payments",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Sales Invoice Payment",
|
"options": "POS Payment Method",
|
||||||
"options": "Sales Invoice Payment"
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_14",
|
"fieldname": "section_break_14",
|
||||||
@ -220,12 +160,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Print Settings"
|
"label": "Print Settings"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "print_format_for_online",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Print Format for Online",
|
|
||||||
"options": "Print Format"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "letter_head",
|
"fieldname": "letter_head",
|
||||||
@ -258,39 +192,6 @@
|
|||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "Print Heading"
|
"options": "Print Heading"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "offline_pos_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Offline POS Settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "territory",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Territory",
|
|
||||||
"oldfieldname": "territory",
|
|
||||||
"oldfieldtype": "Link",
|
|
||||||
"options": "Territory",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_31",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "Point of Sale",
|
|
||||||
"fieldname": "print_format",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Print Format",
|
|
||||||
"options": "Print Format"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "customer_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Customer Group",
|
|
||||||
"options": "Customer Group",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_19",
|
"fieldname": "section_break_19",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -380,20 +281,49 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Accounting Dimensions"
|
"label": "Accounting Dimensions"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "dimension_col_break",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "tax_category",
|
"fieldname": "tax_category",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Tax Category",
|
"label": "Tax Category",
|
||||||
"options": "Tax Category"
|
"options": "Tax Category"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimension_col_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "print_format",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Print Format",
|
||||||
|
"options": "Print Format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "update_stock",
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Warehouse",
|
||||||
|
"oldfieldname": "warehouse",
|
||||||
|
"oldfieldtype": "Link",
|
||||||
|
"options": "Warehouse",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "update_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Update Stock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "ignore_pricing_rule",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Ignore Pricing Rule"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"modified": "2020-01-24 15:52:03.797701",
|
"links": [],
|
||||||
|
"modified": "2020-06-29 12:20:30.977272",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Profile",
|
"name": "POS Profile",
|
||||||
|
|||||||
@ -5,8 +5,6 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
from frappe.utils import cint, now
|
from frappe.utils import cint, now
|
||||||
from erpnext.accounts.doctype.sales_invoice.pos import get_child_nodes
|
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import set_account_for_mode_of_payment
|
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
@ -16,7 +14,6 @@ class POSProfile(Document):
|
|||||||
self.validate_all_link_fields()
|
self.validate_all_link_fields()
|
||||||
self.validate_duplicate_groups()
|
self.validate_duplicate_groups()
|
||||||
self.check_default_payment()
|
self.check_default_payment()
|
||||||
self.validate_customer_territory_group()
|
|
||||||
|
|
||||||
def validate_default_profile(self):
|
def validate_default_profile(self):
|
||||||
for row in self.applicable_for_users:
|
for row in self.applicable_for_users:
|
||||||
@ -64,19 +61,6 @@ class POSProfile(Document):
|
|||||||
if len(default_mode_of_payment) > 1:
|
if len(default_mode_of_payment) > 1:
|
||||||
frappe.throw(_("Multiple default mode of payment is not allowed"))
|
frappe.throw(_("Multiple default mode of payment is not allowed"))
|
||||||
|
|
||||||
def validate_customer_territory_group(self):
|
|
||||||
if not frappe.db.get_single_value('POS Settings', 'use_pos_in_offline_mode'):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.territory:
|
|
||||||
frappe.throw(_("Territory is Required in POS Profile"), title="Mandatory Field")
|
|
||||||
|
|
||||||
if not self.customer_group:
|
|
||||||
frappe.throw(_("Customer Group is Required in POS Profile"), title="Mandatory Field")
|
|
||||||
|
|
||||||
def before_save(self):
|
|
||||||
set_account_for_mode_of_payment(self)
|
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.set_defaults()
|
self.set_defaults()
|
||||||
|
|
||||||
@ -111,9 +95,14 @@ def get_item_groups(pos_profile):
|
|||||||
|
|
||||||
return list(set(item_groups))
|
return list(set(item_groups))
|
||||||
|
|
||||||
|
def get_child_nodes(group_type, root):
|
||||||
|
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
|
||||||
|
return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where
|
||||||
|
lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_series():
|
def get_series():
|
||||||
return frappe.get_meta("Sales Invoice").get_field("naming_series").options or ""
|
return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s"
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
|||||||
@ -8,7 +8,7 @@ def get_data():
|
|||||||
'fieldname': 'pos_profile',
|
'fieldname': 'pos_profile',
|
||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
{
|
||||||
'items': ['Sales Invoice', 'POS Closing Voucher']
|
'items': ['Sales Invoice', 'POS Closing Entry', 'POS Opening Entry']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
import unittest
|
import unittest
|
||||||
from erpnext.stock.get_item_details import get_pos_profile
|
from erpnext.stock.get_item_details import get_pos_profile
|
||||||
from erpnext.accounts.doctype.sales_invoice.pos import get_items_list, get_customers_list
|
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
|
||||||
|
|
||||||
class TestPOSProfile(unittest.TestCase):
|
class TestPOSProfile(unittest.TestCase):
|
||||||
def test_pos_profile(self):
|
def test_pos_profile(self):
|
||||||
@ -29,6 +29,44 @@ class TestPOSProfile(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.sql("delete from `tabPOS Profile`")
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
|
||||||
|
def get_customers_list(pos_profile={}):
|
||||||
|
cond = "1=1"
|
||||||
|
customer_groups = []
|
||||||
|
if pos_profile.get('customer_groups'):
|
||||||
|
# Get customers based on the customer groups defined in the POS profile
|
||||||
|
for d in pos_profile.get('customer_groups'):
|
||||||
|
customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))])
|
||||||
|
cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups)))
|
||||||
|
|
||||||
|
return frappe.db.sql(""" select name, customer_name, customer_group,
|
||||||
|
territory, customer_pos_id from tabCustomer where disabled = 0
|
||||||
|
and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {}
|
||||||
|
|
||||||
|
def get_items_list(pos_profile, company):
|
||||||
|
cond = ""
|
||||||
|
args_list = []
|
||||||
|
if pos_profile.get('item_groups'):
|
||||||
|
# Get items based on the item groups defined in the POS profile
|
||||||
|
for d in pos_profile.get('item_groups'):
|
||||||
|
args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)])
|
||||||
|
if args_list:
|
||||||
|
cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list)))
|
||||||
|
|
||||||
|
return frappe.db.sql("""
|
||||||
|
select
|
||||||
|
i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
|
||||||
|
i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image,
|
||||||
|
id.expense_account, id.selling_cost_center, id.default_warehouse,
|
||||||
|
i.sales_uom, c.conversion_factor
|
||||||
|
from
|
||||||
|
`tabItem` i
|
||||||
|
left join `tabItem Default` id on id.parent = i.name and id.company = %s
|
||||||
|
left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom
|
||||||
|
where
|
||||||
|
i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 and i.is_fixed_asset = 0
|
||||||
|
{cond}
|
||||||
|
""".format(cond=cond), tuple([company] + args_list), as_dict=1)
|
||||||
|
|
||||||
def make_pos_profile(**args):
|
def make_pos_profile(**args):
|
||||||
frappe.db.sql("delete from `tabPOS Profile`")
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
|
||||||
@ -51,6 +89,12 @@ def make_pos_profile(**args):
|
|||||||
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
|
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
payments = [{
|
||||||
|
'mode_of_payment': 'Cash',
|
||||||
|
'default': 1
|
||||||
|
}]
|
||||||
|
pos_profile.set("payments", payments)
|
||||||
|
|
||||||
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
|
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
|
||||||
pos_profile.insert()
|
pos_profile.insert()
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-01 09:46:47.599173",
|
"modified": "2020-05-13 23:57:33.627305",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Profile User",
|
"name": "POS Profile User",
|
||||||
|
|||||||
@ -6,12 +6,7 @@ frappe.ui.form.on('POS Settings', {
|
|||||||
frm.trigger("get_invoice_fields");
|
frm.trigger("get_invoice_fields");
|
||||||
},
|
},
|
||||||
|
|
||||||
use_pos_in_offline_mode: function(frm) {
|
|
||||||
frm.trigger("get_invoice_fields");
|
|
||||||
},
|
|
||||||
|
|
||||||
get_invoice_fields: function(frm) {
|
get_invoice_fields: function(frm) {
|
||||||
if (!frm.doc.use_pos_in_offline_mode) {
|
|
||||||
frappe.model.with_doctype("Sales Invoice", () => {
|
frappe.model.with_doctype("Sales Invoice", () => {
|
||||||
var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) {
|
var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) {
|
||||||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
||||||
@ -24,9 +19,6 @@ frappe.ui.form.on('POS Settings', {
|
|||||||
|
|
||||||
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
|
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -5,24 +5,11 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"use_pos_in_offline_mode",
|
"invoice_fields"
|
||||||
"section_break_2",
|
|
||||||
"fields"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"default": "0",
|
"fieldname": "invoice_fields",
|
||||||
"fieldname": "use_pos_in_offline_mode",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Use POS in Offline Mode"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_2",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval:!doc.use_pos_in_offline_mode",
|
|
||||||
"fieldname": "fields",
|
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "POS Field",
|
"label": "POS Field",
|
||||||
"options": "POS Field"
|
"options": "POS Field"
|
||||||
@ -30,7 +17,7 @@
|
|||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2019-12-26 11:50:47.122997",
|
"modified": "2020-06-01 15:46:41.478928",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Settings",
|
"name": "POS Settings",
|
||||||
|
|||||||
@ -276,7 +276,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
|||||||
|
|
||||||
item_details.has_pricing_rule = 1
|
item_details.has_pricing_rule = 1
|
||||||
|
|
||||||
item_details.pricing_rules = ','.join([d.pricing_rule for d in rules])
|
item_details.pricing_rules = frappe.as_json([d.pricing_rule for d in rules])
|
||||||
|
|
||||||
if not doc: return item_details
|
if not doc: return item_details
|
||||||
|
|
||||||
@ -366,7 +366,7 @@ def set_discount_amount(rate, item_details):
|
|||||||
|
|
||||||
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
||||||
from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items
|
from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items
|
||||||
for d in pricing_rules.split(','):
|
for d in json.loads(pricing_rules):
|
||||||
if not d or not frappe.db.exists("Pricing Rule", d): continue
|
if not d or not frappe.db.exists("Pricing Rule", d): continue
|
||||||
pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
|
pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
|
||||||
|
|
||||||
|
|||||||
@ -448,7 +448,7 @@ def apply_pricing_rule_on_transaction(doc):
|
|||||||
doc.set_missing_values()
|
doc.set_missing_values()
|
||||||
|
|
||||||
def get_applied_pricing_rules(item_row):
|
def get_applied_pricing_rules(item_row):
|
||||||
return (item_row.get("pricing_rules").split(',')
|
return (json.loads(item_row.get("pricing_rules"))
|
||||||
if item_row.get("pricing_rules") else [])
|
if item_row.get("pricing_rules") else [])
|
||||||
|
|
||||||
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,626 +0,0 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from erpnext.accounts.party import get_party_account_currency
|
|
||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
|
||||||
from erpnext.stock.get_item_details import get_pos_profile
|
|
||||||
from frappe import _
|
|
||||||
from frappe.core.doctype.communication.email import make
|
|
||||||
from frappe.utils import nowdate, cint
|
|
||||||
|
|
||||||
from six import string_types, iteritems
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_pos_data():
|
|
||||||
doc = frappe.new_doc('Sales Invoice')
|
|
||||||
doc.is_pos = 1
|
|
||||||
pos_profile = get_pos_profile(doc.company) or {}
|
|
||||||
if not pos_profile:
|
|
||||||
frappe.throw(_("POS Profile is required to use Point-of-Sale"))
|
|
||||||
|
|
||||||
if not doc.company:
|
|
||||||
doc.company = pos_profile.get('company')
|
|
||||||
|
|
||||||
doc.update_stock = pos_profile.get('update_stock')
|
|
||||||
|
|
||||||
if pos_profile.get('name'):
|
|
||||||
pos_profile = frappe.get_doc('POS Profile', pos_profile.get('name'))
|
|
||||||
pos_profile.validate()
|
|
||||||
|
|
||||||
company_data = get_company_data(doc.company)
|
|
||||||
update_pos_profile_data(doc, pos_profile, company_data)
|
|
||||||
update_multi_mode_option(doc, pos_profile)
|
|
||||||
default_print_format = pos_profile.get('print_format') or "Point of Sale"
|
|
||||||
print_template = frappe.db.get_value('Print Format', default_print_format, 'html')
|
|
||||||
items_list = get_items_list(pos_profile, doc.company)
|
|
||||||
customers = get_customers_list(pos_profile)
|
|
||||||
|
|
||||||
doc.plc_conversion_rate = update_plc_conversion_rate(doc, pos_profile)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'doc': doc,
|
|
||||||
'default_customer': pos_profile.get('customer'),
|
|
||||||
'items': items_list,
|
|
||||||
'item_groups': get_item_groups(pos_profile),
|
|
||||||
'customers': customers,
|
|
||||||
'address': get_customers_address(customers),
|
|
||||||
'contacts': get_contacts(customers),
|
|
||||||
'serial_no_data': get_serial_no_data(pos_profile, doc.company),
|
|
||||||
'batch_no_data': get_batch_no_data(),
|
|
||||||
'barcode_data': get_barcode_data(items_list),
|
|
||||||
'tax_data': get_item_tax_data(),
|
|
||||||
'price_list_data': get_price_list_data(doc.selling_price_list, doc.plc_conversion_rate),
|
|
||||||
'customer_wise_price_list': get_customer_wise_price_list(),
|
|
||||||
'bin_data': get_bin_data(pos_profile),
|
|
||||||
'pricing_rules': get_pricing_rule_data(doc),
|
|
||||||
'print_template': print_template,
|
|
||||||
'pos_profile': pos_profile,
|
|
||||||
'meta': get_meta()
|
|
||||||
}
|
|
||||||
|
|
||||||
def update_plc_conversion_rate(doc, pos_profile):
|
|
||||||
conversion_rate = 1.0
|
|
||||||
|
|
||||||
price_list_currency = frappe.get_cached_value("Price List", doc.selling_price_list, "currency")
|
|
||||||
if pos_profile.get("currency") != price_list_currency:
|
|
||||||
conversion_rate = get_exchange_rate(price_list_currency,
|
|
||||||
pos_profile.get("currency"), nowdate(), args="for_selling") or 1.0
|
|
||||||
|
|
||||||
return conversion_rate
|
|
||||||
|
|
||||||
def get_meta():
|
|
||||||
doctype_meta = {
|
|
||||||
'customer': frappe.get_meta('Customer'),
|
|
||||||
'invoice': frappe.get_meta('Sales Invoice')
|
|
||||||
}
|
|
||||||
|
|
||||||
for row in frappe.get_all('DocField', fields=['fieldname', 'options'],
|
|
||||||
filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}):
|
|
||||||
doctype_meta[row.fieldname] = frappe.get_meta(row.options)
|
|
||||||
|
|
||||||
return doctype_meta
|
|
||||||
|
|
||||||
|
|
||||||
def get_company_data(company):
|
|
||||||
return frappe.get_all('Company', fields=["*"], filters={'name': company})[0]
|
|
||||||
|
|
||||||
|
|
||||||
def update_pos_profile_data(doc, pos_profile, company_data):
|
|
||||||
doc.campaign = pos_profile.get('campaign')
|
|
||||||
if pos_profile and not pos_profile.get('country'):
|
|
||||||
pos_profile.country = company_data.country
|
|
||||||
|
|
||||||
doc.write_off_account = pos_profile.get('write_off_account') or \
|
|
||||||
company_data.write_off_account
|
|
||||||
doc.change_amount_account = pos_profile.get('change_amount_account') or \
|
|
||||||
company_data.default_cash_account
|
|
||||||
doc.taxes_and_charges = pos_profile.get('taxes_and_charges')
|
|
||||||
if doc.taxes_and_charges:
|
|
||||||
update_tax_table(doc)
|
|
||||||
|
|
||||||
doc.currency = pos_profile.get('currency') or company_data.default_currency
|
|
||||||
doc.conversion_rate = 1.0
|
|
||||||
|
|
||||||
if doc.currency != company_data.default_currency:
|
|
||||||
doc.conversion_rate = get_exchange_rate(doc.currency, company_data.default_currency, doc.posting_date, args="for_selling")
|
|
||||||
|
|
||||||
doc.selling_price_list = pos_profile.get('selling_price_list') or \
|
|
||||||
frappe.db.get_value('Selling Settings', None, 'selling_price_list')
|
|
||||||
doc.naming_series = pos_profile.get('naming_series') or 'SINV-'
|
|
||||||
doc.letter_head = pos_profile.get('letter_head') or company_data.default_letter_head
|
|
||||||
doc.ignore_pricing_rule = pos_profile.get('ignore_pricing_rule') or 0
|
|
||||||
doc.apply_discount_on = pos_profile.get('apply_discount_on') or 'Grand Total'
|
|
||||||
doc.customer_group = pos_profile.get('customer_group') or get_root('Customer Group')
|
|
||||||
doc.territory = pos_profile.get('territory') or get_root('Territory')
|
|
||||||
doc.terms = frappe.db.get_value('Terms and Conditions', pos_profile.get('tc_name'), 'terms') or doc.terms or ''
|
|
||||||
doc.offline_pos_name = ''
|
|
||||||
|
|
||||||
|
|
||||||
def get_root(table):
|
|
||||||
root = frappe.db.sql(""" select name from `tab%(table)s` having
|
|
||||||
min(lft)""" % {'table': table}, as_dict=1)
|
|
||||||
|
|
||||||
return root[0].name
|
|
||||||
|
|
||||||
|
|
||||||
def update_multi_mode_option(doc, pos_profile):
|
|
||||||
from frappe.model import default_fields
|
|
||||||
|
|
||||||
if not pos_profile or not pos_profile.get('payments'):
|
|
||||||
for payment in get_mode_of_payment(doc):
|
|
||||||
payments = doc.append('payments', {})
|
|
||||||
payments.mode_of_payment = payment.parent
|
|
||||||
payments.account = payment.default_account
|
|
||||||
payments.type = payment.type
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
for payment_mode in pos_profile.payments:
|
|
||||||
payment_mode = payment_mode.as_dict()
|
|
||||||
|
|
||||||
for fieldname in default_fields:
|
|
||||||
if fieldname in payment_mode:
|
|
||||||
del payment_mode[fieldname]
|
|
||||||
|
|
||||||
doc.append('payments', payment_mode)
|
|
||||||
|
|
||||||
|
|
||||||
def get_mode_of_payment(doc):
|
|
||||||
return frappe.db.sql("""
|
|
||||||
select mpa.default_account, mpa.parent, mp.type as type
|
|
||||||
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
|
||||||
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
|
|
||||||
{'company': doc.company}, as_dict=1)
|
|
||||||
|
|
||||||
|
|
||||||
def update_tax_table(doc):
|
|
||||||
taxes = get_taxes_and_charges('Sales Taxes and Charges Template', doc.taxes_and_charges)
|
|
||||||
for tax in taxes:
|
|
||||||
doc.append('taxes', tax)
|
|
||||||
|
|
||||||
|
|
||||||
def get_items_list(pos_profile, company):
|
|
||||||
cond = ""
|
|
||||||
args_list = []
|
|
||||||
if pos_profile.get('item_groups'):
|
|
||||||
# Get items based on the item groups defined in the POS profile
|
|
||||||
for d in pos_profile.get('item_groups'):
|
|
||||||
args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)])
|
|
||||||
if args_list:
|
|
||||||
cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list)))
|
|
||||||
|
|
||||||
return frappe.db.sql("""
|
|
||||||
select
|
|
||||||
i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
|
|
||||||
i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image,
|
|
||||||
id.expense_account, id.selling_cost_center, id.default_warehouse,
|
|
||||||
i.sales_uom, c.conversion_factor
|
|
||||||
from
|
|
||||||
`tabItem` i
|
|
||||||
left join `tabItem Default` id on id.parent = i.name and id.company = %s
|
|
||||||
left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom
|
|
||||||
where
|
|
||||||
i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1
|
|
||||||
{cond}
|
|
||||||
""".format(cond=cond), tuple([company] + args_list), as_dict=1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_groups(pos_profile):
|
|
||||||
item_group_dict = {}
|
|
||||||
item_groups = frappe.db.sql("""Select name,
|
|
||||||
lft, rgt from `tabItem Group` order by lft""", as_dict=1)
|
|
||||||
|
|
||||||
for data in item_groups:
|
|
||||||
item_group_dict[data.name] = [data.lft, data.rgt]
|
|
||||||
return item_group_dict
|
|
||||||
|
|
||||||
|
|
||||||
def get_customers_list(pos_profile={}):
|
|
||||||
cond = "1=1"
|
|
||||||
customer_groups = []
|
|
||||||
if pos_profile.get('customer_groups'):
|
|
||||||
# Get customers based on the customer groups defined in the POS profile
|
|
||||||
for d in pos_profile.get('customer_groups'):
|
|
||||||
customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))])
|
|
||||||
cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups)))
|
|
||||||
|
|
||||||
return frappe.db.sql(""" select name, customer_name, customer_group,
|
|
||||||
territory, customer_pos_id from tabCustomer where disabled = 0
|
|
||||||
and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {}
|
|
||||||
|
|
||||||
|
|
||||||
def get_customers_address(customers):
|
|
||||||
customer_address = {}
|
|
||||||
if isinstance(customers, string_types):
|
|
||||||
customers = [frappe._dict({'name': customers})]
|
|
||||||
|
|
||||||
for data in customers:
|
|
||||||
address = frappe.db.sql(""" select name, address_line1, address_line2, city, state,
|
|
||||||
email_id, phone, fax, pincode from `tabAddress` where is_primary_address =1 and name in
|
|
||||||
(select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s
|
|
||||||
and parenttype = 'Address')""", data.name, as_dict=1)
|
|
||||||
address_data = {}
|
|
||||||
if address:
|
|
||||||
address_data = address[0]
|
|
||||||
|
|
||||||
address_data.update({'full_name': data.customer_name, 'customer_pos_id': data.customer_pos_id})
|
|
||||||
customer_address[data.name] = address_data
|
|
||||||
|
|
||||||
return customer_address
|
|
||||||
|
|
||||||
|
|
||||||
def get_contacts(customers):
|
|
||||||
customer_contact = {}
|
|
||||||
if isinstance(customers, string_types):
|
|
||||||
customers = [frappe._dict({'name': customers})]
|
|
||||||
|
|
||||||
for data in customers:
|
|
||||||
contact = frappe.db.sql(""" select email_id, phone, mobile_no from `tabContact`
|
|
||||||
where is_primary_contact=1 and name in
|
|
||||||
(select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s
|
|
||||||
and parenttype = 'Contact')""", data.name, as_dict=1)
|
|
||||||
if contact:
|
|
||||||
customer_contact[data.name] = contact[0]
|
|
||||||
|
|
||||||
return customer_contact
|
|
||||||
|
|
||||||
|
|
||||||
def get_child_nodes(group_type, root):
|
|
||||||
lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"])
|
|
||||||
return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where
|
|
||||||
lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_serial_no_data(pos_profile, company):
|
|
||||||
# get itemwise serial no data
|
|
||||||
# example {'Nokia Lumia 1020': {'SN0001': 'Pune'}}
|
|
||||||
# where Nokia Lumia 1020 is item code, SN0001 is serial no and Pune is warehouse
|
|
||||||
|
|
||||||
cond = "1=1"
|
|
||||||
if pos_profile.get('update_stock') and pos_profile.get('warehouse'):
|
|
||||||
cond = "warehouse = %(warehouse)s"
|
|
||||||
|
|
||||||
serial_nos = frappe.db.sql("""select name, warehouse, item_code
|
|
||||||
from `tabSerial No` where {0} and company = %(company)s """.format(cond),{
|
|
||||||
'company': company, 'warehouse': frappe.db.escape(pos_profile.get('warehouse'))
|
|
||||||
}, as_dict=1)
|
|
||||||
|
|
||||||
itemwise_serial_no = {}
|
|
||||||
for sn in serial_nos:
|
|
||||||
if sn.item_code not in itemwise_serial_no:
|
|
||||||
itemwise_serial_no.setdefault(sn.item_code, {})
|
|
||||||
itemwise_serial_no[sn.item_code][sn.name] = sn.warehouse
|
|
||||||
|
|
||||||
return itemwise_serial_no
|
|
||||||
|
|
||||||
|
|
||||||
def get_batch_no_data():
|
|
||||||
# get itemwise batch no data
|
|
||||||
# exmaple: {'LED-GRE': [Batch001, Batch002]}
|
|
||||||
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
|
|
||||||
|
|
||||||
itemwise_batch = {}
|
|
||||||
batches = frappe.db.sql("""select name, item from `tabBatch`
|
|
||||||
where ifnull(expiry_date, '4000-10-10') >= curdate()""", as_dict=1)
|
|
||||||
|
|
||||||
for batch in batches:
|
|
||||||
if batch.item not in itemwise_batch:
|
|
||||||
itemwise_batch.setdefault(batch.item, [])
|
|
||||||
itemwise_batch[batch.item].append(batch.name)
|
|
||||||
|
|
||||||
return itemwise_batch
|
|
||||||
|
|
||||||
|
|
||||||
def get_barcode_data(items_list):
|
|
||||||
# get itemwise batch no data
|
|
||||||
# exmaple: {'LED-GRE': [Batch001, Batch002]}
|
|
||||||
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
|
|
||||||
|
|
||||||
itemwise_barcode = {}
|
|
||||||
for item in items_list:
|
|
||||||
barcodes = frappe.db.sql("""
|
|
||||||
select barcode from `tabItem Barcode` where parent = %s
|
|
||||||
""", item.item_code, as_dict=1)
|
|
||||||
|
|
||||||
for barcode in barcodes:
|
|
||||||
if item.item_code not in itemwise_barcode:
|
|
||||||
itemwise_barcode.setdefault(item.item_code, [])
|
|
||||||
itemwise_barcode[item.item_code].append(barcode.get("barcode"))
|
|
||||||
|
|
||||||
return itemwise_barcode
|
|
||||||
|
|
||||||
|
|
||||||
def get_item_tax_data():
|
|
||||||
# get default tax of an item
|
|
||||||
# example: {'Consulting Services': {'Excise 12 - TS': '12.000'}}
|
|
||||||
|
|
||||||
itemwise_tax = {}
|
|
||||||
taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax Template Detail`""", as_dict=1)
|
|
||||||
|
|
||||||
for tax in taxes:
|
|
||||||
if tax.parent not in itemwise_tax:
|
|
||||||
itemwise_tax.setdefault(tax.parent, {})
|
|
||||||
itemwise_tax[tax.parent][tax.tax_type] = tax.tax_rate
|
|
||||||
|
|
||||||
return itemwise_tax
|
|
||||||
|
|
||||||
|
|
||||||
def get_price_list_data(selling_price_list, conversion_rate):
|
|
||||||
itemwise_price_list = {}
|
|
||||||
price_lists = frappe.db.sql("""Select ifnull(price_list_rate, 0) as price_list_rate,
|
|
||||||
item_code from `tabItem Price` ip where price_list = %(price_list)s""",
|
|
||||||
{'price_list': selling_price_list}, as_dict=1)
|
|
||||||
|
|
||||||
for item in price_lists:
|
|
||||||
itemwise_price_list[item.item_code] = item.price_list_rate * conversion_rate
|
|
||||||
|
|
||||||
return itemwise_price_list
|
|
||||||
|
|
||||||
def get_customer_wise_price_list():
|
|
||||||
customer_wise_price = {}
|
|
||||||
customer_price_list_mapping = frappe._dict(frappe.get_all('Customer',fields = ['default_price_list', 'name'], as_list=1))
|
|
||||||
|
|
||||||
price_lists = frappe.db.sql(""" Select ifnull(price_list_rate, 0) as price_list_rate,
|
|
||||||
item_code, price_list from `tabItem Price` """, as_dict=1)
|
|
||||||
|
|
||||||
for item in price_lists:
|
|
||||||
if item.price_list and customer_price_list_mapping.get(item.price_list):
|
|
||||||
|
|
||||||
customer_wise_price.setdefault(customer_price_list_mapping.get(item.price_list),{}).setdefault(
|
|
||||||
item.item_code, item.price_list_rate
|
|
||||||
)
|
|
||||||
|
|
||||||
return customer_wise_price
|
|
||||||
|
|
||||||
def get_bin_data(pos_profile):
|
|
||||||
itemwise_bin_data = {}
|
|
||||||
filters = { 'actual_qty': ['>', 0] }
|
|
||||||
if pos_profile.get('warehouse'):
|
|
||||||
filters.update({ 'warehouse': pos_profile.get('warehouse') })
|
|
||||||
|
|
||||||
bin_data = frappe.db.get_all('Bin', fields = ['item_code', 'warehouse', 'actual_qty'], filters=filters)
|
|
||||||
|
|
||||||
for bins in bin_data:
|
|
||||||
if bins.item_code not in itemwise_bin_data:
|
|
||||||
itemwise_bin_data.setdefault(bins.item_code, {})
|
|
||||||
itemwise_bin_data[bins.item_code][bins.warehouse] = bins.actual_qty
|
|
||||||
|
|
||||||
return itemwise_bin_data
|
|
||||||
|
|
||||||
|
|
||||||
def get_pricing_rule_data(doc):
|
|
||||||
pricing_rules = ""
|
|
||||||
if doc.ignore_pricing_rule == 0:
|
|
||||||
pricing_rules = frappe.db.sql(""" Select * from `tabPricing Rule` where docstatus < 2
|
|
||||||
and ifnull(for_price_list, '') in (%(price_list)s, '') and selling = 1
|
|
||||||
and ifnull(company, '') in (%(company)s, '') and disable = 0 and %(date)s
|
|
||||||
between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')
|
|
||||||
order by priority desc, name desc""",
|
|
||||||
{'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1)
|
|
||||||
return pricing_rules
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def make_invoice(pos_profile, doc_list={}, email_queue_list={}, customers_list={}):
|
|
||||||
import json
|
|
||||||
|
|
||||||
if isinstance(doc_list, string_types):
|
|
||||||
doc_list = json.loads(doc_list)
|
|
||||||
|
|
||||||
if isinstance(email_queue_list, string_types):
|
|
||||||
email_queue_list = json.loads(email_queue_list)
|
|
||||||
|
|
||||||
if isinstance(customers_list, string_types):
|
|
||||||
customers_list = json.loads(customers_list)
|
|
||||||
|
|
||||||
customers_list = make_customer_and_address(customers_list)
|
|
||||||
name_list = []
|
|
||||||
for docs in doc_list:
|
|
||||||
for name, doc in iteritems(docs):
|
|
||||||
if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}):
|
|
||||||
if isinstance(doc, dict):
|
|
||||||
validate_records(doc)
|
|
||||||
si_doc = frappe.new_doc('Sales Invoice')
|
|
||||||
si_doc.offline_pos_name = name
|
|
||||||
si_doc.update(doc)
|
|
||||||
si_doc.set_posting_time = 1
|
|
||||||
si_doc.customer = get_customer_id(doc)
|
|
||||||
si_doc.due_date = doc.get('posting_date')
|
|
||||||
name_list = submit_invoice(si_doc, name, doc, name_list)
|
|
||||||
else:
|
|
||||||
doc.due_date = doc.get('posting_date')
|
|
||||||
doc.customer = get_customer_id(doc)
|
|
||||||
doc.set_posting_time = 1
|
|
||||||
doc.offline_pos_name = name
|
|
||||||
name_list = submit_invoice(doc, name, doc, name_list)
|
|
||||||
else:
|
|
||||||
name_list.append(name)
|
|
||||||
|
|
||||||
email_queue = make_email_queue(email_queue_list)
|
|
||||||
|
|
||||||
if isinstance(pos_profile, string_types):
|
|
||||||
pos_profile = json.loads(pos_profile)
|
|
||||||
|
|
||||||
customers = get_customers_list(pos_profile)
|
|
||||||
return {
|
|
||||||
'invoice': name_list,
|
|
||||||
'email_queue': email_queue,
|
|
||||||
'customers': customers_list,
|
|
||||||
'synced_customers_list': customers,
|
|
||||||
'synced_address': get_customers_address(customers),
|
|
||||||
'synced_contacts': get_contacts(customers)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def validate_records(doc):
|
|
||||||
validate_item(doc)
|
|
||||||
|
|
||||||
|
|
||||||
def get_customer_id(doc, customer=None):
|
|
||||||
cust_id = None
|
|
||||||
if doc.get('customer_pos_id'):
|
|
||||||
cust_id = frappe.db.get_value('Customer',{'customer_pos_id': doc.get('customer_pos_id')}, 'name')
|
|
||||||
|
|
||||||
if not cust_id:
|
|
||||||
customer = customer or doc.get('customer')
|
|
||||||
if frappe.db.exists('Customer', customer):
|
|
||||||
cust_id = customer
|
|
||||||
else:
|
|
||||||
cust_id = add_customer(doc)
|
|
||||||
|
|
||||||
return cust_id
|
|
||||||
|
|
||||||
def make_customer_and_address(customers):
|
|
||||||
customers_list = []
|
|
||||||
for customer, data in iteritems(customers):
|
|
||||||
data = json.loads(data)
|
|
||||||
cust_id = get_customer_id(data, customer)
|
|
||||||
if not cust_id:
|
|
||||||
cust_id = add_customer(data)
|
|
||||||
else:
|
|
||||||
frappe.db.set_value("Customer", cust_id, "customer_name", data.get('full_name'))
|
|
||||||
|
|
||||||
make_contact(data, cust_id)
|
|
||||||
make_address(data, cust_id)
|
|
||||||
customers_list.append(customer)
|
|
||||||
frappe.db.commit()
|
|
||||||
return customers_list
|
|
||||||
|
|
||||||
def add_customer(data):
|
|
||||||
customer = data.get('full_name') or data.get('customer')
|
|
||||||
if frappe.db.exists("Customer", customer.strip()):
|
|
||||||
return customer.strip()
|
|
||||||
|
|
||||||
customer_doc = frappe.new_doc('Customer')
|
|
||||||
customer_doc.customer_name = data.get('full_name') or data.get('customer')
|
|
||||||
customer_doc.customer_pos_id = data.get('customer_pos_id')
|
|
||||||
customer_doc.customer_type = 'Company'
|
|
||||||
customer_doc.customer_group = get_customer_group(data)
|
|
||||||
customer_doc.territory = get_territory(data)
|
|
||||||
customer_doc.flags.ignore_mandatory = True
|
|
||||||
customer_doc.save(ignore_permissions=True)
|
|
||||||
frappe.db.commit()
|
|
||||||
return customer_doc.name
|
|
||||||
|
|
||||||
def get_territory(data):
|
|
||||||
if data.get('territory'):
|
|
||||||
return data.get('territory')
|
|
||||||
|
|
||||||
return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories')
|
|
||||||
|
|
||||||
def get_customer_group(data):
|
|
||||||
if data.get('customer_group'):
|
|
||||||
return data.get('customer_group')
|
|
||||||
|
|
||||||
return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name')
|
|
||||||
|
|
||||||
def make_contact(args, customer):
|
|
||||||
if args.get('email_id') or args.get('phone'):
|
|
||||||
name = frappe.db.get_value('Dynamic Link',
|
|
||||||
{'link_doctype': 'Customer', 'link_name': customer, 'parenttype': 'Contact'}, 'parent')
|
|
||||||
|
|
||||||
args = {
|
|
||||||
'first_name': args.get('full_name'),
|
|
||||||
'email_id': args.get('email_id'),
|
|
||||||
'phone': args.get('phone')
|
|
||||||
}
|
|
||||||
|
|
||||||
doc = frappe.new_doc('Contact')
|
|
||||||
if name:
|
|
||||||
doc = frappe.get_doc('Contact', name)
|
|
||||||
|
|
||||||
doc.update(args)
|
|
||||||
doc.is_primary_contact = 1
|
|
||||||
if not name:
|
|
||||||
doc.append('links', {
|
|
||||||
'link_doctype': 'Customer',
|
|
||||||
'link_name': customer
|
|
||||||
})
|
|
||||||
doc.flags.ignore_mandatory = True
|
|
||||||
doc.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
def make_address(args, customer):
|
|
||||||
if not args.get('address_line1'):
|
|
||||||
return
|
|
||||||
|
|
||||||
name = args.get('name')
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
data = get_customers_address(customer)
|
|
||||||
name = data[customer].get('name') if data else None
|
|
||||||
|
|
||||||
if name:
|
|
||||||
address = frappe.get_doc('Address', name)
|
|
||||||
else:
|
|
||||||
address = frappe.new_doc('Address')
|
|
||||||
if args.get('company'):
|
|
||||||
address.country = frappe.get_cached_value('Company',
|
|
||||||
args.get('company'), 'country')
|
|
||||||
|
|
||||||
address.append('links', {
|
|
||||||
'link_doctype': 'Customer',
|
|
||||||
'link_name': customer
|
|
||||||
})
|
|
||||||
|
|
||||||
address.is_primary_address = 1
|
|
||||||
address.is_shipping_address = 1
|
|
||||||
address.update(args)
|
|
||||||
address.flags.ignore_mandatory = True
|
|
||||||
address.save(ignore_permissions=True)
|
|
||||||
|
|
||||||
def make_email_queue(email_queue):
|
|
||||||
name_list = []
|
|
||||||
|
|
||||||
for key, data in iteritems(email_queue):
|
|
||||||
name = frappe.db.get_value('Sales Invoice', {'offline_pos_name': key}, 'name')
|
|
||||||
if not name: continue
|
|
||||||
|
|
||||||
data = json.loads(data)
|
|
||||||
sender = frappe.session.user
|
|
||||||
print_format = "POS Invoice" if not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')) else None
|
|
||||||
|
|
||||||
attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)]
|
|
||||||
|
|
||||||
make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'),
|
|
||||||
sender=sender, attachments=attachments, send_email=True,
|
|
||||||
doctype='Sales Invoice', name=name)
|
|
||||||
name_list.append(key)
|
|
||||||
|
|
||||||
return name_list
|
|
||||||
|
|
||||||
def validate_item(doc):
|
|
||||||
for item in doc.get('items'):
|
|
||||||
if not frappe.db.exists('Item', item.get('item_code')):
|
|
||||||
item_doc = frappe.new_doc('Item')
|
|
||||||
item_doc.name = item.get('item_code')
|
|
||||||
item_doc.item_code = item.get('item_code')
|
|
||||||
item_doc.item_name = item.get('item_name')
|
|
||||||
item_doc.description = item.get('description')
|
|
||||||
item_doc.stock_uom = item.get('stock_uom')
|
|
||||||
item_doc.uom = item.get('uom')
|
|
||||||
item_doc.item_group = item.get('item_group')
|
|
||||||
item_doc.append('item_defaults', {
|
|
||||||
"company": doc.get("company"),
|
|
||||||
"default_warehouse": item.get('warehouse')
|
|
||||||
})
|
|
||||||
item_doc.save(ignore_permissions=True)
|
|
||||||
frappe.db.commit()
|
|
||||||
|
|
||||||
def submit_invoice(si_doc, name, doc, name_list):
|
|
||||||
try:
|
|
||||||
si_doc.insert()
|
|
||||||
si_doc.submit()
|
|
||||||
frappe.db.commit()
|
|
||||||
name_list.append(name)
|
|
||||||
except Exception as e:
|
|
||||||
if frappe.message_log:
|
|
||||||
frappe.message_log.pop()
|
|
||||||
frappe.db.rollback()
|
|
||||||
frappe.log_error(frappe.get_traceback())
|
|
||||||
name_list = save_invoice(doc, name, name_list)
|
|
||||||
|
|
||||||
return name_list
|
|
||||||
|
|
||||||
def save_invoice(doc, name, name_list):
|
|
||||||
try:
|
|
||||||
if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}):
|
|
||||||
si = frappe.new_doc('Sales Invoice')
|
|
||||||
si.update(doc)
|
|
||||||
si.set_posting_time = 1
|
|
||||||
si.customer = get_customer_id(doc)
|
|
||||||
si.due_date = doc.get('posting_date')
|
|
||||||
si.flags.ignore_mandatory = True
|
|
||||||
si.insert(ignore_permissions=True)
|
|
||||||
frappe.db.commit()
|
|
||||||
name_list.append(name)
|
|
||||||
except Exception:
|
|
||||||
frappe.db.rollback()
|
|
||||||
frappe.log_error(frappe.get_traceback())
|
|
||||||
|
|
||||||
return name_list
|
|
||||||
@ -96,6 +96,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
|||||||
cur_frm.add_custom_button(__('Invoice Discounting'), function() {
|
cur_frm.add_custom_button(__('Invoice Discounting'), function() {
|
||||||
cur_frm.events.create_invoice_discounting(cur_frm);
|
cur_frm.events.create_invoice_discounting(cur_frm);
|
||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
|
|
||||||
|
if (doc.due_date < frappe.datetime.get_today()) {
|
||||||
|
cur_frm.add_custom_button(__('Dunning'), function() {
|
||||||
|
cur_frm.events.create_dunning(cur_frm);
|
||||||
|
}, __('Create'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.docstatus === 1) {
|
if (doc.docstatus === 1) {
|
||||||
@ -276,7 +282,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
|||||||
"customer": this.frm.doc.customer
|
"customer": this.frm.doc.customer
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.message && r.message.length) {
|
if(r.message && r.message.length > 1) {
|
||||||
select_loyalty_program(me.frm, r.message);
|
select_loyalty_program(me.frm, r.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -824,6 +830,12 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
|
||||||
frm: frm
|
frm: frm
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
create_dunning: function(frm) {
|
||||||
|
frappe.model.open_mapped_doc({
|
||||||
|
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||||
|
frm: frm
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
|
"allow_workflow": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2013-05-24 19:29:05",
|
"creation": "2013-05-24 19:29:05",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@ -13,6 +14,7 @@
|
|||||||
"customer_name",
|
"customer_name",
|
||||||
"tax_id",
|
"tax_id",
|
||||||
"is_pos",
|
"is_pos",
|
||||||
|
"is_consolidated",
|
||||||
"pos_profile",
|
"pos_profile",
|
||||||
"offline_pos_name",
|
"offline_pos_name",
|
||||||
"is_return",
|
"is_return",
|
||||||
@ -1158,6 +1160,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "In Words (Company Currency)",
|
"label": "In Words (Company Currency)",
|
||||||
|
"length": 240,
|
||||||
"oldfieldname": "in_words",
|
"oldfieldname": "in_words",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@ -1215,6 +1218,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "In Words",
|
"label": "In Words",
|
||||||
|
"length": 240,
|
||||||
"oldfieldname": "in_words_export",
|
"oldfieldname": "in_words_export",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@ -1921,6 +1925,13 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1
|
"hide_seconds": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_consolidated",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Consolidated",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fetch_from": "customer.is_internal_customer",
|
"fetch_from": "customer.is_internal_customer",
|
||||||
@ -1936,7 +1947,7 @@
|
|||||||
"idx": 181,
|
"idx": 181,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-30 12:00:03.890180",
|
"modified": "2020-07-18 05:07:16.725974",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@ -8,8 +8,6 @@ from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_d
|
|||||||
from frappe import _, msgprint, throw
|
from frappe import _, msgprint, throw
|
||||||
from erpnext.accounts.party import get_party_account, get_due_date
|
from erpnext.accounts.party import get_party_account, get_due_date
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from erpnext.accounts.doctype.sales_invoice.pos import update_multi_mode_option
|
|
||||||
|
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
from erpnext.accounts.utils import get_account_currency
|
from erpnext.accounts.utils import get_account_currency
|
||||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
||||||
@ -133,7 +131,7 @@ class SalesInvoice(SellingController):
|
|||||||
if self.is_pos and self.is_return:
|
if self.is_pos and self.is_return:
|
||||||
self.verify_payment_amount_is_negative()
|
self.verify_payment_amount_is_negative()
|
||||||
|
|
||||||
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 and not self.is_consolidated:
|
||||||
validate_loyalty_points(self, self.loyalty_points)
|
validate_loyalty_points(self, self.loyalty_points)
|
||||||
|
|
||||||
def validate_fixed_asset(self):
|
def validate_fixed_asset(self):
|
||||||
@ -200,13 +198,13 @@ class SalesInvoice(SellingController):
|
|||||||
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||||
|
|
||||||
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
# create the loyalty point ledger entry if the customer is enrolled in any loyalty program
|
||||||
if not self.is_return and self.loyalty_program:
|
if not self.is_return and not self.is_consolidated and self.loyalty_program:
|
||||||
self.make_loyalty_point_entry()
|
self.make_loyalty_point_entry()
|
||||||
elif self.is_return and self.return_against and self.loyalty_program:
|
elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program:
|
||||||
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
|
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
|
||||||
against_si_doc.delete_loyalty_point_entry()
|
against_si_doc.delete_loyalty_point_entry()
|
||||||
against_si_doc.make_loyalty_point_entry()
|
against_si_doc.make_loyalty_point_entry()
|
||||||
if self.redeem_loyalty_points and self.loyalty_points:
|
if self.redeem_loyalty_points and not self.is_consolidated and self.loyalty_points:
|
||||||
self.apply_loyalty_points()
|
self.apply_loyalty_points()
|
||||||
|
|
||||||
# Healthcare Service Invoice.
|
# Healthcare Service Invoice.
|
||||||
@ -265,9 +263,9 @@ class SalesInvoice(SellingController):
|
|||||||
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
|
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
|
||||||
update_company_current_month_sales(self.company)
|
update_company_current_month_sales(self.company)
|
||||||
self.update_project()
|
self.update_project()
|
||||||
if not self.is_return and self.loyalty_program:
|
if not self.is_return and not self.is_consolidated and self.loyalty_program:
|
||||||
self.delete_loyalty_point_entry()
|
self.delete_loyalty_point_entry()
|
||||||
elif self.is_return and self.return_against and self.loyalty_program:
|
elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program:
|
||||||
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
|
against_si_doc = frappe.get_doc("Sales Invoice", self.return_against)
|
||||||
against_si_doc.delete_loyalty_point_entry()
|
against_si_doc.delete_loyalty_point_entry()
|
||||||
against_si_doc.make_loyalty_point_entry()
|
against_si_doc.make_loyalty_point_entry()
|
||||||
@ -347,7 +345,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
super(SalesInvoice, self).set_missing_values(for_validate)
|
super(SalesInvoice, self).set_missing_values(for_validate)
|
||||||
|
|
||||||
print_format = pos.get("print_format_for_online") if pos else None
|
print_format = pos.get("print_format") if pos else None
|
||||||
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
|
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
|
||||||
print_format = 'POS Invoice'
|
print_format = 'POS Invoice'
|
||||||
|
|
||||||
@ -420,8 +418,6 @@ class SalesInvoice(SellingController):
|
|||||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
||||||
|
|
||||||
if pos:
|
if pos:
|
||||||
self.allow_print_before_pay = pos.allow_print_before_pay
|
|
||||||
|
|
||||||
if not for_validate:
|
if not for_validate:
|
||||||
self.tax_category = pos.get("tax_category")
|
self.tax_category = pos.get("tax_category")
|
||||||
|
|
||||||
@ -432,8 +428,8 @@ class SalesInvoice(SellingController):
|
|||||||
if pos.get('account_for_change_amount'):
|
if pos.get('account_for_change_amount'):
|
||||||
self.account_for_change_amount = pos.get('account_for_change_amount')
|
self.account_for_change_amount = pos.get('account_for_change_amount')
|
||||||
|
|
||||||
for fieldname in ('territory', 'naming_series', 'currency', 'letter_head', 'tc_name',
|
for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name',
|
||||||
'company', 'select_print_heading', 'cash_bank_account', 'write_off_account', 'taxes_and_charges',
|
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
|
||||||
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
|
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
|
||||||
if (not for_validate) or (for_validate and not self.get(fieldname)):
|
if (not for_validate) or (for_validate and not self.get(fieldname)):
|
||||||
self.set(fieldname, pos.get(fieldname))
|
self.set(fieldname, pos.get(fieldname))
|
||||||
@ -1123,7 +1119,8 @@ class SalesInvoice(SellingController):
|
|||||||
"loyalty_program": lp_details.loyalty_program,
|
"loyalty_program": lp_details.loyalty_program,
|
||||||
"loyalty_program_tier": lp_details.tier_name,
|
"loyalty_program_tier": lp_details.tier_name,
|
||||||
"customer": self.customer,
|
"customer": self.customer,
|
||||||
"sales_invoice": self.name,
|
"invoice_type": self.doctype,
|
||||||
|
"invoice": self.name,
|
||||||
"loyalty_points": points_earned,
|
"loyalty_points": points_earned,
|
||||||
"purchase_amount": eligible_amount,
|
"purchase_amount": eligible_amount,
|
||||||
"expiry_date": add_days(self.posting_date, lp_details.expiry_duration),
|
"expiry_date": add_days(self.posting_date, lp_details.expiry_duration),
|
||||||
@ -1135,18 +1132,18 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
# valdite the redemption and then delete the loyalty points earned on cancel of the invoice
|
# valdite the redemption and then delete the loyalty points earned on cancel of the invoice
|
||||||
def delete_loyalty_point_entry(self):
|
def delete_loyalty_point_entry(self):
|
||||||
lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where sales_invoice=%s",
|
lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where invoice=%s",
|
||||||
(self.name), as_dict=1)
|
(self.name), as_dict=1)
|
||||||
|
|
||||||
if not lp_entry: return
|
if not lp_entry: return
|
||||||
against_lp_entry = frappe.db.sql('''select name, sales_invoice from `tabLoyalty Point Entry`
|
against_lp_entry = frappe.db.sql('''select name, invoice from `tabLoyalty Point Entry`
|
||||||
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
|
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
|
||||||
if against_lp_entry:
|
if against_lp_entry:
|
||||||
invoice_list = ", ".join([d.sales_invoice for d in against_lp_entry])
|
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
|
||||||
frappe.throw(_('''Sales Invoice can't be cancelled since the Loyalty Points earned has been redeemed.
|
frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed.
|
||||||
First cancel the Sales Invoice No {0}''').format(invoice_list))
|
First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
|
||||||
else:
|
else:
|
||||||
frappe.db.sql('''delete from `tabLoyalty Point Entry` where sales_invoice=%s''', (self.name))
|
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
|
||||||
# Set loyalty program
|
# Set loyalty program
|
||||||
self.set_loyalty_program_tier()
|
self.set_loyalty_program_tier()
|
||||||
|
|
||||||
@ -1172,7 +1169,9 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
points_to_redeem = self.loyalty_points
|
points_to_redeem = self.loyalty_points
|
||||||
for lp_entry in loyalty_point_entries:
|
for lp_entry in loyalty_point_entries:
|
||||||
if lp_entry.sales_invoice == self.name:
|
if lp_entry.invoice_type != self.doctype or lp_entry.invoice == self.name:
|
||||||
|
# redeemption should be done against same doctype
|
||||||
|
# also it shouldn't be against itself
|
||||||
continue
|
continue
|
||||||
available_points = lp_entry.loyalty_points - flt(redemption_details.get(lp_entry.name))
|
available_points = lp_entry.loyalty_points - flt(redemption_details.get(lp_entry.name))
|
||||||
if available_points > points_to_redeem:
|
if available_points > points_to_redeem:
|
||||||
@ -1185,7 +1184,8 @@ class SalesInvoice(SellingController):
|
|||||||
"loyalty_program": self.loyalty_program,
|
"loyalty_program": self.loyalty_program,
|
||||||
"loyalty_program_tier": lp_entry.loyalty_program_tier,
|
"loyalty_program_tier": lp_entry.loyalty_program_tier,
|
||||||
"customer": self.customer,
|
"customer": self.customer,
|
||||||
"sales_invoice": self.name,
|
"invoice_type": self.doctype,
|
||||||
|
"invoice": self.name,
|
||||||
"redeem_against": lp_entry.name,
|
"redeem_against": lp_entry.name,
|
||||||
"loyalty_points": -1*redeemed_points,
|
"loyalty_points": -1*redeemed_points,
|
||||||
"purchase_amount": self.grand_total,
|
"purchase_amount": self.grand_total,
|
||||||
@ -1576,13 +1576,13 @@ def get_loyalty_programs(customer):
|
|||||||
from erpnext.selling.doctype.customer.customer import get_loyalty_programs
|
from erpnext.selling.doctype.customer.customer import get_loyalty_programs
|
||||||
|
|
||||||
customer = frappe.get_doc('Customer', customer)
|
customer = frappe.get_doc('Customer', customer)
|
||||||
if customer.loyalty_program: return
|
if customer.loyalty_program: return [customer.loyalty_program]
|
||||||
|
|
||||||
lp_details = get_loyalty_programs(customer)
|
lp_details = get_loyalty_programs(customer)
|
||||||
|
|
||||||
if len(lp_details) == 1:
|
if len(lp_details) == 1:
|
||||||
frappe.db.set(customer, 'loyalty_program', lp_details[0])
|
frappe.db.set(customer, 'loyalty_program', lp_details[0])
|
||||||
return []
|
return lp_details
|
||||||
else:
|
else:
|
||||||
return lp_details
|
return lp_details
|
||||||
|
|
||||||
@ -1602,3 +1602,71 @@ def create_invoice_discounting(source_name, target_doc=None):
|
|||||||
})
|
})
|
||||||
|
|
||||||
return invoice_discounting
|
return invoice_discounting
|
||||||
|
|
||||||
|
def update_multi_mode_option(doc, pos_profile):
|
||||||
|
def append_payment(payment_mode):
|
||||||
|
payment = doc.append('payments', {})
|
||||||
|
payment.default = payment_mode.default
|
||||||
|
payment.mode_of_payment = payment_mode.parent
|
||||||
|
payment.account = payment_mode.default_account
|
||||||
|
payment.type = payment_mode.type
|
||||||
|
|
||||||
|
doc.set('payments', [])
|
||||||
|
if not pos_profile or not pos_profile.get('payments'):
|
||||||
|
for payment_mode in get_all_mode_of_payments(doc):
|
||||||
|
append_payment(payment_mode)
|
||||||
|
return
|
||||||
|
|
||||||
|
for pos_payment_method in pos_profile.get('payments'):
|
||||||
|
pos_payment_method = pos_payment_method.as_dict()
|
||||||
|
|
||||||
|
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
|
||||||
|
payment_mode[0].default = pos_payment_method.default
|
||||||
|
append_payment(payment_mode[0])
|
||||||
|
|
||||||
|
def get_all_mode_of_payments(doc):
|
||||||
|
return frappe.db.sql("""
|
||||||
|
select mpa.default_account, mpa.parent, mp.type as type
|
||||||
|
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
||||||
|
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
|
||||||
|
{'company': doc.company}, as_dict=1)
|
||||||
|
|
||||||
|
def get_mode_of_payment_info(mode_of_payment, company):
|
||||||
|
return frappe.db.sql("""
|
||||||
|
select mpa.default_account, mpa.parent, mp.type as type
|
||||||
|
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
||||||
|
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
|
||||||
|
(company, mode_of_payment), as_dict=1)
|
||||||
|
|
||||||
|
def create_dunning(source_name, target_doc=None):
|
||||||
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount
|
||||||
|
def set_missing_values(source, target):
|
||||||
|
target.sales_invoice = source_name
|
||||||
|
target.outstanding_amount = source.outstanding_amount
|
||||||
|
overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
|
||||||
|
target.overdue_days = overdue_days
|
||||||
|
if frappe.db.exists('Dunning Type', {'start_day': [
|
||||||
|
'<', overdue_days], 'end_day': ['>=', overdue_days]}):
|
||||||
|
dunning_type = frappe.get_doc('Dunning Type', {'start_day': [
|
||||||
|
'<', overdue_days], 'end_day': ['>=', overdue_days]})
|
||||||
|
target.dunning_type = dunning_type.name
|
||||||
|
target.rate_of_interest = dunning_type.rate_of_interest
|
||||||
|
target.dunning_fee = dunning_type.dunning_fee
|
||||||
|
letter_text = get_dunning_letter_text(dunning_type = dunning_type.name, doc = target.as_dict())
|
||||||
|
if letter_text:
|
||||||
|
target.body_text = letter_text.get('body_text')
|
||||||
|
target.closing_text = letter_text.get('closing_text')
|
||||||
|
target.language = letter_text.get('language')
|
||||||
|
amounts = calculate_interest_and_amount(target.posting_date, target.outstanding_amount,
|
||||||
|
target.rate_of_interest, target.dunning_fee, target.overdue_days)
|
||||||
|
target.interest_amount = amounts.get('interest_amount')
|
||||||
|
target.dunning_amount = amounts.get('dunning_amount')
|
||||||
|
target.grand_total = amounts.get('grand_total')
|
||||||
|
|
||||||
|
doclist = get_mapped_doc("Sales Invoice", source_name, {
|
||||||
|
"Sales Invoice": {
|
||||||
|
"doctype": "Dunning",
|
||||||
|
}
|
||||||
|
}, target_doc, set_missing_values)
|
||||||
|
return doclist
|
||||||
|
|||||||
@ -706,37 +706,15 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
self.pos_gl_entry(si, pos, 50)
|
self.pos_gl_entry(si, pos, 50)
|
||||||
|
|
||||||
def test_pos_returns_without_repayment(self):
|
|
||||||
pos_profile = make_pos_profile()
|
|
||||||
|
|
||||||
pos = create_sales_invoice(qty = 10, do_not_save=True)
|
|
||||||
pos.is_pos = 1
|
|
||||||
pos.pos_profile = pos_profile.name
|
|
||||||
|
|
||||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
|
|
||||||
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
|
|
||||||
pos.insert()
|
|
||||||
pos.submit()
|
|
||||||
|
|
||||||
pos_return = create_sales_invoice(is_return=1,
|
|
||||||
return_against=pos.name, qty=-5, do_not_save=True)
|
|
||||||
|
|
||||||
pos_return.is_pos = 1
|
|
||||||
pos_return.pos_profile = pos_profile.name
|
|
||||||
|
|
||||||
pos_return.insert()
|
|
||||||
pos_return.submit()
|
|
||||||
|
|
||||||
self.assertFalse(pos_return.is_pos)
|
|
||||||
self.assertFalse(pos_return.get('payments'))
|
|
||||||
|
|
||||||
def test_pos_returns_with_repayment(self):
|
def test_pos_returns_with_repayment(self):
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||||
|
|
||||||
pos_profile = make_pos_profile()
|
pos_profile = make_pos_profile()
|
||||||
|
|
||||||
|
pos_profile.payments = []
|
||||||
pos_profile.append('payments', {
|
pos_profile.append('payments', {
|
||||||
'default': 1,
|
'default': 1,
|
||||||
'mode_of_payment': 'Cash',
|
'mode_of_payment': 'Cash'
|
||||||
'amount': 0.0
|
|
||||||
})
|
})
|
||||||
|
|
||||||
pos_profile.save()
|
pos_profile.save()
|
||||||
@ -751,18 +729,12 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
pos.insert()
|
pos.insert()
|
||||||
pos.submit()
|
pos.submit()
|
||||||
|
|
||||||
pos_return = create_sales_invoice(is_return=1,
|
pos_return = make_sales_return(pos.name)
|
||||||
return_against=pos.name, qty=-5, do_not_save=True)
|
|
||||||
|
|
||||||
pos_return.is_pos = 1
|
|
||||||
pos_return.pos_profile = pos_profile.name
|
|
||||||
pos_return.insert()
|
pos_return.insert()
|
||||||
pos_return.submit()
|
pos_return.submit()
|
||||||
|
|
||||||
self.assertEqual(pos_return.get('payments')[0].amount, -500)
|
self.assertEqual(pos_return.get('payments')[0].amount, -1000)
|
||||||
pos_profile.payments = []
|
|
||||||
pos_profile.save()
|
|
||||||
|
|
||||||
|
|
||||||
def test_pos_change_amount(self):
|
def test_pos_change_amount(self):
|
||||||
make_pos_profile()
|
make_pos_profile()
|
||||||
@ -788,82 +760,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(pos.grand_total, 100.0)
|
self.assertEqual(pos.grand_total, 100.0)
|
||||||
self.assertEqual(pos.write_off_amount, -5)
|
self.assertEqual(pos.write_off_amount, -5)
|
||||||
|
|
||||||
def test_make_pos_invoice(self):
|
|
||||||
from erpnext.accounts.doctype.sales_invoice.pos import make_invoice
|
|
||||||
|
|
||||||
pos_profile = make_pos_profile()
|
|
||||||
|
|
||||||
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
|
|
||||||
item_code= "_Test FG Item",
|
|
||||||
warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
|
|
||||||
|
|
||||||
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
|
|
||||||
debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
|
|
||||||
income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
|
|
||||||
cost_center = "Main - TCP1", do_not_save=True)
|
|
||||||
|
|
||||||
pos.is_pos = 1
|
|
||||||
pos.update_stock = 1
|
|
||||||
|
|
||||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
|
|
||||||
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 50})
|
|
||||||
|
|
||||||
taxes = get_taxes_and_charges()
|
|
||||||
pos.taxes = []
|
|
||||||
for tax in taxes:
|
|
||||||
pos.append("taxes", tax)
|
|
||||||
|
|
||||||
invoice_data = [{'09052016142': pos}]
|
|
||||||
si = make_invoice(pos_profile, invoice_data).get('invoice')
|
|
||||||
self.assertEqual(si[0], '09052016142')
|
|
||||||
|
|
||||||
sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': '09052016142', 'docstatus': 1})
|
|
||||||
si = frappe.get_doc('Sales Invoice', sales_invoice[0].name)
|
|
||||||
|
|
||||||
self.assertEqual(si.grand_total, 100)
|
|
||||||
|
|
||||||
self.pos_gl_entry(si, pos, 50)
|
|
||||||
|
|
||||||
def test_make_pos_invoice_in_draft(self):
|
|
||||||
from erpnext.accounts.doctype.sales_invoice.pos import make_invoice
|
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
|
||||||
|
|
||||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
|
||||||
if allow_negative_stock:
|
|
||||||
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0)
|
|
||||||
|
|
||||||
pos_profile = make_pos_profile()
|
|
||||||
timestamp = cint(time.time())
|
|
||||||
|
|
||||||
item = make_item("_Test POS Item")
|
|
||||||
pos = copy.deepcopy(test_records[1])
|
|
||||||
pos['items'][0]['item_code'] = item.name
|
|
||||||
pos['items'][0]['warehouse'] = "_Test Warehouse - _TC"
|
|
||||||
pos["is_pos"] = 1
|
|
||||||
pos["offline_pos_name"] = timestamp
|
|
||||||
pos["update_stock"] = 1
|
|
||||||
pos["payments"] = [{'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 300},
|
|
||||||
{'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 330}]
|
|
||||||
|
|
||||||
invoice_data = [{timestamp: pos}]
|
|
||||||
si = make_invoice(pos_profile, invoice_data).get('invoice')
|
|
||||||
self.assertEqual(si[0], timestamp)
|
|
||||||
|
|
||||||
sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': timestamp})
|
|
||||||
self.assertEqual(sales_invoice[0].docstatus, 0)
|
|
||||||
|
|
||||||
timestamp = cint(time.time())
|
|
||||||
pos["offline_pos_name"] = timestamp
|
|
||||||
invoice_data = [{timestamp: pos}]
|
|
||||||
si1 = make_invoice(pos_profile, invoice_data).get('invoice')
|
|
||||||
self.assertEqual(si1[0], timestamp)
|
|
||||||
|
|
||||||
sales_invoice1 = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': timestamp})
|
|
||||||
self.assertEqual(sales_invoice1[0].docstatus, 0)
|
|
||||||
|
|
||||||
if allow_negative_stock:
|
|
||||||
frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1)
|
|
||||||
|
|
||||||
def pos_gl_entry(self, si, pos, cash_amount):
|
def pos_gl_entry(self, si, pos, cash_amount):
|
||||||
# check stock ledger entries
|
# check stock ledger entries
|
||||||
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
|
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
|
||||||
|
|||||||
@ -795,7 +795,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-03-11 12:24:41.749986",
|
"modified": "2020-07-18 12:24:41.749986",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
|||||||
@ -1,314 +1,90 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2016-05-08 23:49:38.842621",
|
"creation": "2016-05-08 23:49:38.842621",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"default",
|
||||||
|
"mode_of_payment",
|
||||||
|
"amount",
|
||||||
|
"column_break_3",
|
||||||
|
"account",
|
||||||
|
"type",
|
||||||
|
"base_amount",
|
||||||
|
"clearance_date"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:parent.doctype == 'POS Profile'",
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "default",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Default",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "mode_of_payment",
|
"fieldname": "mode_of_payment",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Mode of Payment",
|
"label": "Mode of Payment",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Mode of Payment",
|
"options": "Mode of Payment",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "account",
|
"fieldname": "account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_from": "mode_of_payment.type",
|
"fetch_from": "mode_of_payment.type",
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "type",
|
"fieldname": "type",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"hidden": 0,
|
"label": "Type"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Type",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "base_amount",
|
"fieldname": "base_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Base Amount (Company Currency)",
|
"label": "Base Amount (Company Currency)",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "clearance_date",
|
"fieldname": "clearance_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Clearance Date",
|
"label": "Clearance Date",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
},
|
||||||
"remember_last_selected_value": 0,
|
{
|
||||||
"report_hide": 0,
|
"default": "0",
|
||||||
"reqd": 0,
|
"fieldname": "default",
|
||||||
"search_index": 0,
|
"fieldtype": "Check",
|
||||||
"set_only_once": 0,
|
"hidden": 1,
|
||||||
"translatable": 0,
|
"label": "Default",
|
||||||
"unique": 0
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2019-03-19 14:54:56.524556",
|
"modified": "2020-05-05 16:51:20.091441",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Payment",
|
"name": "Sales Invoice Payment",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC"
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
||||||
@ -2,6 +2,16 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Subscription', {
|
frappe.ui.form.on('Subscription', {
|
||||||
|
setup: function(frm) {
|
||||||
|
frm.set_query('party_type', function() {
|
||||||
|
return {
|
||||||
|
filters : {
|
||||||
|
name: ['in', ['Customer', 'Supplier']]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if(!frm.is_new()){
|
if(!frm.is_new()){
|
||||||
if(frm.doc.status !== 'Cancelled'){
|
if(frm.doc.status !== 'Cancelled'){
|
||||||
|
|||||||
@ -6,14 +6,18 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"customer",
|
"party_type",
|
||||||
"cb_1",
|
|
||||||
"status",
|
"status",
|
||||||
|
"cb_1",
|
||||||
|
"party",
|
||||||
"subscription_period",
|
"subscription_period",
|
||||||
"start",
|
"start_date",
|
||||||
|
"end_date",
|
||||||
"cancelation_date",
|
"cancelation_date",
|
||||||
"trial_period_start",
|
"trial_period_start",
|
||||||
"trial_period_end",
|
"trial_period_end",
|
||||||
|
"follow_calendar_months",
|
||||||
|
"generate_new_invoices_past_due_date",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"current_invoice_start",
|
"current_invoice_start",
|
||||||
"current_invoice_end",
|
"current_invoice_end",
|
||||||
@ -23,7 +27,8 @@
|
|||||||
"sb_4",
|
"sb_4",
|
||||||
"plans",
|
"plans",
|
||||||
"sb_1",
|
"sb_1",
|
||||||
"tax_template",
|
"sales_tax_template",
|
||||||
|
"purchase_tax_template",
|
||||||
"sb_2",
|
"sb_2",
|
||||||
"apply_additional_discount",
|
"apply_additional_discount",
|
||||||
"cb_2",
|
"cb_2",
|
||||||
@ -32,18 +37,10 @@
|
|||||||
"sb_3",
|
"sb_3",
|
||||||
"invoices",
|
"invoices",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
"dimension_col_break"
|
"dimension_col_break"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
|
||||||
"fieldname": "customer",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Customer",
|
|
||||||
"options": "Customer",
|
|
||||||
"reqd": 1,
|
|
||||||
"set_only_once": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "cb_1",
|
"fieldname": "cb_1",
|
||||||
@ -53,7 +50,7 @@
|
|||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid",
|
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -61,12 +58,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Subscription Period"
|
"label": "Subscription Period"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "start",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"label": "Subscription Start Date",
|
|
||||||
"set_only_once": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "cancelation_date",
|
"fieldname": "cancelation_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
@ -137,16 +128,11 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
|
||||||
"fieldname": "sb_1",
|
"fieldname": "sb_1",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Taxes"
|
"label": "Taxes"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "tax_template",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Sales Taxes and Charges Template",
|
|
||||||
"options": "Sales Taxes and Charges Template"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "sb_2",
|
"fieldname": "sb_2",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -195,10 +181,74 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Party Type",
|
||||||
|
"options": "DocType",
|
||||||
|
"reqd": 1,
|
||||||
|
"set_only_once": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Party",
|
||||||
|
"options": "party_type",
|
||||||
|
"reqd": 1,
|
||||||
|
"set_only_once": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.party_type === 'Customer'",
|
||||||
|
"fieldname": "sales_tax_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Sales Taxes and Charges Template",
|
||||||
|
"options": "Sales Taxes and Charges Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.party_type === 'Supplier'",
|
||||||
|
"fieldname": "purchase_tax_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Purchase Taxes and Charges Template",
|
||||||
|
"options": "Purchase Taxes and Charges Template"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If this is checked subsequent new invoices will be created on calendar month and quarter start dates irrespective of current invoice start date",
|
||||||
|
"fieldname": "follow_calendar_months",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Follow Calendar Months",
|
||||||
|
"set_only_once": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
|
||||||
|
"fieldname": "generate_new_invoices_past_due_date",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Generate New Invoices Past Due Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "end_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Subscription End Date",
|
||||||
|
"set_only_once": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "start_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Subscription Start Date",
|
||||||
|
"set_only_once": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cost Center",
|
||||||
|
"options": "Cost Center"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-01-27 14:37:32.845173",
|
"modified": "2020-06-25 10:52:52.265105",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription",
|
"name": "Subscription",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils.data import nowdate, getdate, cint, add_days, date_diff, get_last_day, add_to_date, flt
|
from frappe.utils.data import nowdate, getdate, cstr, cint, add_days, date_diff, get_last_day, add_to_date, flt
|
||||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
|
|||||||
class Subscription(Document):
|
class Subscription(Document):
|
||||||
def before_insert(self):
|
def before_insert(self):
|
||||||
# update start just before the subscription doc is created
|
# update start just before the subscription doc is created
|
||||||
self.update_subscription_period(self.start)
|
self.update_subscription_period(self.start_date)
|
||||||
|
|
||||||
def update_subscription_period(self, date=None):
|
def update_subscription_period(self, date=None):
|
||||||
"""
|
"""
|
||||||
@ -35,7 +35,9 @@ class Subscription(Document):
|
|||||||
If the `date` parameter is not given , it will be automatically set as today's
|
If the `date` parameter is not given , it will be automatically set as today's
|
||||||
date.
|
date.
|
||||||
"""
|
"""
|
||||||
if self.trial_period_start and self.is_trialling():
|
if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
|
||||||
|
self.current_invoice_start = add_days(self.trial_period_end, 1)
|
||||||
|
elif self.trial_period_start and self.is_trialling():
|
||||||
self.current_invoice_start = self.trial_period_start
|
self.current_invoice_start = self.trial_period_start
|
||||||
elif date:
|
elif date:
|
||||||
self.current_invoice_start = date
|
self.current_invoice_start = date
|
||||||
@ -53,15 +55,45 @@ class Subscription(Document):
|
|||||||
current billing period where `x` is the billing interval from the
|
current billing period where `x` is the billing interval from the
|
||||||
`Subscription Plan` in the `Subscription`.
|
`Subscription Plan` in the `Subscription`.
|
||||||
"""
|
"""
|
||||||
if self.is_trialling():
|
if self.is_trialling() and getdate(self.current_invoice_start) < getdate(self.trial_period_end):
|
||||||
self.current_invoice_end = self.trial_period_end
|
self.current_invoice_end = self.trial_period_end
|
||||||
else:
|
else:
|
||||||
billing_cycle_info = self.get_billing_cycle_data()
|
billing_cycle_info = self.get_billing_cycle_data()
|
||||||
if billing_cycle_info:
|
if billing_cycle_info:
|
||||||
|
if self.is_new_subscription() and getdate(self.start_date) < getdate(self.current_invoice_start):
|
||||||
|
self.current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
|
||||||
|
|
||||||
|
# For cases where trial period is for an entire billing interval
|
||||||
|
if getdate(self.current_invoice_end) < getdate(self.current_invoice_start):
|
||||||
|
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
|
||||||
|
else:
|
||||||
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
|
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
|
||||||
else:
|
else:
|
||||||
self.current_invoice_end = get_last_day(self.current_invoice_start)
|
self.current_invoice_end = get_last_day(self.current_invoice_start)
|
||||||
|
|
||||||
|
if self.follow_calendar_months:
|
||||||
|
billing_info = self.get_billing_cycle_and_interval()
|
||||||
|
billing_interval_count = billing_info[0]['billing_interval_count']
|
||||||
|
calendar_months = get_calendar_months(billing_interval_count)
|
||||||
|
calendar_month = 0
|
||||||
|
current_invoice_end_month = getdate(self.current_invoice_end).month
|
||||||
|
current_invoice_end_year = getdate(self.current_invoice_end).year
|
||||||
|
|
||||||
|
for month in calendar_months:
|
||||||
|
if month <= current_invoice_end_month:
|
||||||
|
calendar_month = month
|
||||||
|
|
||||||
|
if cint(calendar_month - billing_interval_count) <= 0 and \
|
||||||
|
getdate(self.current_invoice_start).month != 1:
|
||||||
|
calendar_month = 12
|
||||||
|
current_invoice_end_year -= 1
|
||||||
|
|
||||||
|
self.current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' \
|
||||||
|
+ cstr(calendar_month) + '-01')
|
||||||
|
|
||||||
|
if self.end_date and getdate(self.current_invoice_end) > getdate(self.end_date):
|
||||||
|
self.current_invoice_end = self.end_date
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_plans_billing_cycle(billing_cycle_data):
|
def validate_plans_billing_cycle(billing_cycle_data):
|
||||||
"""
|
"""
|
||||||
@ -132,21 +164,22 @@ class Subscription(Document):
|
|||||||
"""
|
"""
|
||||||
if self.is_trialling():
|
if self.is_trialling():
|
||||||
self.status = 'Trialling'
|
self.status = 'Trialling'
|
||||||
elif self.status == 'Past Due Date' and self.is_past_grace_period():
|
elif self.status == 'Active' and self.end_date and getdate() > getdate(self.end_date):
|
||||||
|
self.status = 'Completed'
|
||||||
|
elif self.is_past_grace_period():
|
||||||
subscription_settings = frappe.get_single('Subscription Settings')
|
subscription_settings = frappe.get_single('Subscription Settings')
|
||||||
self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
|
self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
|
||||||
elif self.status == 'Past Due Date' and not self.has_outstanding_invoice():
|
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||||
self.status = 'Active'
|
|
||||||
elif self.current_invoice_is_past_due():
|
|
||||||
self.status = 'Past Due Date'
|
self.status = 'Past Due Date'
|
||||||
|
elif not self.has_outstanding_invoice():
|
||||||
|
self.status = 'Active'
|
||||||
elif self.is_new_subscription():
|
elif self.is_new_subscription():
|
||||||
self.status = 'Active'
|
self.status = 'Active'
|
||||||
# todo: then generate new invoice
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def is_trialling(self):
|
def is_trialling(self):
|
||||||
"""
|
"""
|
||||||
Returns `True` if the `Subscription` is trial period.
|
Returns `True` if the `Subscription` is in trial period.
|
||||||
"""
|
"""
|
||||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
||||||
|
|
||||||
@ -160,7 +193,7 @@ class Subscription(Document):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
end_date = getdate(end_date)
|
end_date = getdate(end_date)
|
||||||
return getdate(nowdate()) > getdate(end_date)
|
return getdate() > getdate(end_date)
|
||||||
|
|
||||||
def is_past_grace_period(self):
|
def is_past_grace_period(self):
|
||||||
"""
|
"""
|
||||||
@ -171,7 +204,7 @@ class Subscription(Document):
|
|||||||
subscription_settings = frappe.get_single('Subscription Settings')
|
subscription_settings = frappe.get_single('Subscription Settings')
|
||||||
grace_period = cint(subscription_settings.grace_period)
|
grace_period = cint(subscription_settings.grace_period)
|
||||||
|
|
||||||
return getdate(nowdate()) > add_days(current_invoice.due_date, grace_period)
|
return getdate() > add_days(current_invoice.due_date, grace_period)
|
||||||
|
|
||||||
def current_invoice_is_past_due(self, current_invoice=None):
|
def current_invoice_is_past_due(self, current_invoice=None):
|
||||||
"""
|
"""
|
||||||
@ -180,22 +213,24 @@ class Subscription(Document):
|
|||||||
if not current_invoice:
|
if not current_invoice:
|
||||||
current_invoice = self.get_current_invoice()
|
current_invoice = self.get_current_invoice()
|
||||||
|
|
||||||
if not current_invoice:
|
if not current_invoice or self.is_paid(current_invoice):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return getdate(nowdate()) > getdate(current_invoice.due_date)
|
return getdate() > getdate(current_invoice.due_date)
|
||||||
|
|
||||||
def get_current_invoice(self):
|
def get_current_invoice(self):
|
||||||
"""
|
"""
|
||||||
Returns the most recent generated invoice.
|
Returns the most recent generated invoice.
|
||||||
"""
|
"""
|
||||||
|
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||||
|
|
||||||
if len(self.invoices):
|
if len(self.invoices):
|
||||||
current = self.invoices[-1]
|
current = self.invoices[-1]
|
||||||
if frappe.db.exists('Sales Invoice', current.invoice):
|
if frappe.db.exists(doctype, current.get('invoice')):
|
||||||
doc = frappe.get_doc('Sales Invoice', current.invoice)
|
doc = frappe.get_doc(doctype, current.get('invoice'))
|
||||||
return doc
|
return doc
|
||||||
else:
|
else:
|
||||||
frappe.throw(_('Invoice {0} no longer exists').format(current.invoice))
|
frappe.throw(_('Invoice {0} no longer exists').format(current.get('invoice')))
|
||||||
|
|
||||||
def is_new_subscription(self):
|
def is_new_subscription(self):
|
||||||
"""
|
"""
|
||||||
@ -206,6 +241,8 @@ class Subscription(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_trial_period()
|
self.validate_trial_period()
|
||||||
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
|
||||||
|
self.validate_end_date()
|
||||||
|
self.validate_to_follow_calendar_months()
|
||||||
|
|
||||||
def validate_trial_period(self):
|
def validate_trial_period(self):
|
||||||
"""
|
"""
|
||||||
@ -215,34 +252,72 @@ class Subscription(Document):
|
|||||||
if getdate(self.trial_period_end) < getdate(self.trial_period_start):
|
if getdate(self.trial_period_end) < getdate(self.trial_period_start):
|
||||||
frappe.throw(_('Trial Period End Date Cannot be before Trial Period Start Date'))
|
frappe.throw(_('Trial Period End Date Cannot be before Trial Period Start Date'))
|
||||||
|
|
||||||
elif self.trial_period_start or self.trial_period_end:
|
if self.trial_period_start and not self.trial_period_end:
|
||||||
frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set'))
|
frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set'))
|
||||||
|
|
||||||
|
if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date):
|
||||||
|
frappe.throw(_('Trial Period Start date cannot be after Subscription Start Date'))
|
||||||
|
|
||||||
|
def validate_end_date(self):
|
||||||
|
billing_cycle_info = self.get_billing_cycle_data()
|
||||||
|
end_date = add_to_date(self.start_date, **billing_cycle_info)
|
||||||
|
|
||||||
|
if self.end_date and getdate(self.end_date) <= getdate(end_date):
|
||||||
|
frappe.throw(_('Subscription End Date must be after {0} as per the subscription plan').format(end_date))
|
||||||
|
|
||||||
|
def validate_to_follow_calendar_months(self):
|
||||||
|
if self.follow_calendar_months:
|
||||||
|
billing_info = self.get_billing_cycle_and_interval()
|
||||||
|
|
||||||
|
if not self.end_date:
|
||||||
|
frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
|
||||||
|
|
||||||
|
if billing_info[0]['billing_interval'] != 'Month':
|
||||||
|
frappe.throw('Billing Interval in Subscription Plan must be Month to follow calendar months')
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
|
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
|
||||||
self.set_subscription_status()
|
self.set_subscription_status()
|
||||||
|
|
||||||
def generate_invoice(self, prorate=0):
|
def generate_invoice(self, prorate=0):
|
||||||
"""
|
"""
|
||||||
Creates a `Sales Invoice` for the `Subscription`, updates `self.invoices` and
|
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
|
||||||
saves the `Subscription`.
|
saves the `Subscription`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||||
|
|
||||||
invoice = self.create_invoice(prorate)
|
invoice = self.create_invoice(prorate)
|
||||||
self.append('invoices', {'invoice': invoice.name})
|
self.append('invoices', {
|
||||||
|
'document_type': doctype,
|
||||||
|
'invoice': invoice.name
|
||||||
|
})
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
def create_invoice(self, prorate):
|
def create_invoice(self, prorate):
|
||||||
"""
|
"""
|
||||||
Creates a `Sales Invoice`, submits it and returns it
|
Creates a `Invoice`, submits it and returns it
|
||||||
"""
|
"""
|
||||||
invoice = frappe.new_doc('Sales Invoice')
|
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||||
invoice.set_posting_time = 1
|
|
||||||
invoice.posting_date = self.current_invoice_start
|
|
||||||
invoice.customer = self.customer
|
|
||||||
|
|
||||||
## Add dimesnions in invoice for subscription:
|
invoice = frappe.new_doc(doctype)
|
||||||
|
invoice.set_posting_time = 1
|
||||||
|
invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \
|
||||||
|
else self.current_invoice_end
|
||||||
|
|
||||||
|
invoice.cost_center = self.cost_center
|
||||||
|
|
||||||
|
if doctype == 'Sales Invoice':
|
||||||
|
invoice.customer = self.party
|
||||||
|
else:
|
||||||
|
invoice.supplier = self.party
|
||||||
|
if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
|
||||||
|
invoice.apply_tds = 1
|
||||||
|
|
||||||
|
## Add dimensions in invoice for subscription:
|
||||||
accounting_dimensions = get_accounting_dimensions()
|
accounting_dimensions = get_accounting_dimensions()
|
||||||
|
|
||||||
for dimension in accounting_dimensions:
|
for dimension in accounting_dimensions:
|
||||||
@ -258,15 +333,22 @@ class Subscription(Document):
|
|||||||
invoice.append('items', item)
|
invoice.append('items', item)
|
||||||
|
|
||||||
# Taxes
|
# Taxes
|
||||||
if self.tax_template:
|
tax_template = ''
|
||||||
invoice.taxes_and_charges = self.tax_template
|
|
||||||
|
if doctype == 'Sales Invoice' and self.sales_tax_template:
|
||||||
|
tax_template = self.sales_tax_template
|
||||||
|
if doctype == 'Purchase Invoice' and self.purchase_tax_template:
|
||||||
|
tax_template = self.purchase_tax_template
|
||||||
|
|
||||||
|
if tax_template:
|
||||||
|
invoice.taxes_and_charges = tax_template
|
||||||
invoice.set_taxes()
|
invoice.set_taxes()
|
||||||
|
|
||||||
# Due date
|
# Due date
|
||||||
invoice.append(
|
invoice.append(
|
||||||
'payment_schedule',
|
'payment_schedule',
|
||||||
{
|
{
|
||||||
'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
|
'due_date': add_days(invoice.posting_date, cint(self.days_until_due)),
|
||||||
'invoice_portion': 100
|
'invoice_portion': 100
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -300,13 +382,42 @@ class Subscription(Document):
|
|||||||
prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start)
|
prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start)
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
customer = self.customer
|
party = self.party
|
||||||
for plan in plans:
|
for plan in plans:
|
||||||
item_code = frappe.db.get_value("Subscription Plan", plan.plan, "item")
|
plan_doc = frappe.get_doc('Subscription Plan', plan.plan)
|
||||||
if not prorate:
|
|
||||||
items.append({'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, customer)})
|
item_code = plan_doc.item
|
||||||
|
|
||||||
|
if self.party == 'Customer':
|
||||||
|
deferred_field = 'enable_deferred_revenue'
|
||||||
else:
|
else:
|
||||||
items.append({'item_code': item_code, 'qty': plan.qty, 'rate': (get_plan_rate(plan.plan, plan.qty, customer) * prorate_factor)})
|
deferred_field = 'enable_deferred_expense'
|
||||||
|
|
||||||
|
deferred = frappe.db.get_value('Item', item_code, deferred_field)
|
||||||
|
|
||||||
|
if not prorate:
|
||||||
|
item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party,
|
||||||
|
self.current_invoice_start, self.current_invoice_end), 'cost_center': plan_doc.cost_center}
|
||||||
|
else:
|
||||||
|
item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party,
|
||||||
|
self.current_invoice_start, self.current_invoice_end, prorate_factor), 'cost_center': plan_doc.cost_center}
|
||||||
|
|
||||||
|
if deferred:
|
||||||
|
item.update({
|
||||||
|
deferred_field: deferred,
|
||||||
|
'service_start_date': self.current_invoice_start,
|
||||||
|
'service_end_date': self.current_invoice_end
|
||||||
|
})
|
||||||
|
|
||||||
|
accounting_dimensions = get_accounting_dimensions()
|
||||||
|
|
||||||
|
for dimension in accounting_dimensions:
|
||||||
|
if plan_doc.get(dimension):
|
||||||
|
item.update({
|
||||||
|
dimension: plan_doc.get(dimension)
|
||||||
|
})
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
|
||||||
@ -322,12 +433,13 @@ class Subscription(Document):
|
|||||||
elif self.status in ['Past Due Date', 'Unpaid']:
|
elif self.status in ['Past Due Date', 'Unpaid']:
|
||||||
self.process_for_past_due_date()
|
self.process_for_past_due_date()
|
||||||
|
|
||||||
|
self.set_subscription_status()
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def is_postpaid_to_invoice(self):
|
def is_postpaid_to_invoice(self):
|
||||||
return getdate(nowdate()) > getdate(self.current_invoice_end) or \
|
return getdate() > getdate(self.current_invoice_end) or \
|
||||||
(getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \
|
(getdate() >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start))
|
||||||
not self.has_outstanding_invoice()
|
|
||||||
|
|
||||||
def is_prepaid_to_invoice(self):
|
def is_prepaid_to_invoice(self):
|
||||||
if not self.generate_invoice_at_period_start:
|
if not self.generate_invoice_at_period_start:
|
||||||
@ -337,14 +449,12 @@ class Subscription(Document):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Check invoice dates and make sure it doesn't have outstanding invoices
|
# Check invoice dates and make sure it doesn't have outstanding invoices
|
||||||
return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice()
|
return getdate() >= getdate(self.current_invoice_start)
|
||||||
|
|
||||||
def is_current_invoice_paid(self):
|
def is_current_invoice_generated(self):
|
||||||
if self.is_new_subscription():
|
invoice = self.get_current_invoice()
|
||||||
return False
|
|
||||||
|
|
||||||
last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice)
|
if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
|
||||||
if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid':
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@ -358,21 +468,23 @@ class Subscription(Document):
|
|||||||
2. Change the `Subscription` status to 'Past Due Date'
|
2. Change the `Subscription` status to 'Past Due Date'
|
||||||
3. Change the `Subscription` status to 'Cancelled'
|
3. Change the `Subscription` status to 'Cancelled'
|
||||||
"""
|
"""
|
||||||
if not self.is_current_invoice_paid() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
|
||||||
self.generate_invoice()
|
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||||
if self.current_invoice_is_past_due():
|
|
||||||
self.status = 'Past Due Date'
|
|
||||||
|
|
||||||
if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end):
|
if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||||
self.status = 'Past Due Date'
|
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||||
|
self.generate_invoice(prorate)
|
||||||
|
|
||||||
if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end):
|
if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
|
||||||
self.cancel_subscription_at_period_end()
|
self.cancel_subscription_at_period_end()
|
||||||
|
|
||||||
def cancel_subscription_at_period_end(self):
|
def cancel_subscription_at_period_end(self):
|
||||||
"""
|
"""
|
||||||
Called when `Subscription.cancel_at_period_end` is truthy
|
Called when `Subscription.cancel_at_period_end` is truthy
|
||||||
"""
|
"""
|
||||||
|
if self.end_date and getdate() < getdate(self.end_date):
|
||||||
|
return
|
||||||
|
|
||||||
self.status = 'Cancelled'
|
self.status = 'Cancelled'
|
||||||
if not self.cancelation_date:
|
if not self.cancelation_date:
|
||||||
self.cancelation_date = nowdate()
|
self.cancelation_date = nowdate()
|
||||||
@ -390,14 +502,22 @@ class Subscription(Document):
|
|||||||
if not current_invoice:
|
if not current_invoice:
|
||||||
frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice))
|
frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice))
|
||||||
else:
|
else:
|
||||||
if self.is_not_outstanding(current_invoice):
|
if not self.has_outstanding_invoice():
|
||||||
self.status = 'Active'
|
self.status = 'Active'
|
||||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
|
||||||
else:
|
else:
|
||||||
self.set_status_grace_period()
|
self.set_status_grace_period()
|
||||||
|
|
||||||
|
if getdate() > getdate(self.current_invoice_end):
|
||||||
|
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||||
|
|
||||||
|
# Generate invoices periodically even if current invoice are unpaid
|
||||||
|
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
|
||||||
|
or self.is_prepaid_to_invoice()):
|
||||||
|
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||||
|
self.generate_invoice(prorate)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_not_outstanding(invoice):
|
def is_paid(invoice):
|
||||||
"""
|
"""
|
||||||
Return `True` if the given invoice is paid
|
Return `True` if the given invoice is paid
|
||||||
"""
|
"""
|
||||||
@ -407,11 +527,17 @@ class Subscription(Document):
|
|||||||
"""
|
"""
|
||||||
Returns `True` if the most recent invoice for the `Subscription` is not paid
|
Returns `True` if the most recent invoice for the `Subscription` is not paid
|
||||||
"""
|
"""
|
||||||
|
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||||
current_invoice = self.get_current_invoice()
|
current_invoice = self.get_current_invoice()
|
||||||
if not current_invoice:
|
invoice_list = [d.invoice for d in self.invoices]
|
||||||
return False
|
|
||||||
|
outstanding_invoices = frappe.get_all(doctype, fields=['name'],
|
||||||
|
filters={'status': ('!=', 'Paid'), 'name': ('in', invoice_list)})
|
||||||
|
|
||||||
|
if outstanding_invoices:
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
return not self.is_not_outstanding(current_invoice)
|
False
|
||||||
|
|
||||||
def cancel_subscription(self):
|
def cancel_subscription(self):
|
||||||
"""
|
"""
|
||||||
@ -419,7 +545,7 @@ class Subscription(Document):
|
|||||||
but it will not affect already created invoices.
|
but it will not affect already created invoices.
|
||||||
"""
|
"""
|
||||||
if self.status != 'Cancelled':
|
if self.status != 'Cancelled':
|
||||||
to_generate_invoice = True if self.status == 'Active' else False
|
to_generate_invoice = True if self.status == 'Active' and not self.generate_invoice_at_period_start else False
|
||||||
to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||||
self.status = 'Cancelled'
|
self.status = 'Cancelled'
|
||||||
self.cancelation_date = nowdate()
|
self.cancelation_date = nowdate()
|
||||||
@ -435,7 +561,7 @@ class Subscription(Document):
|
|||||||
"""
|
"""
|
||||||
if self.status == 'Cancelled':
|
if self.status == 'Cancelled':
|
||||||
self.status = 'Active'
|
self.status = 'Active'
|
||||||
self.db_set('start', nowdate())
|
self.db_set('start_date', nowdate())
|
||||||
self.update_subscription_period(nowdate())
|
self.update_subscription_period(nowdate())
|
||||||
self.invoices = []
|
self.invoices = []
|
||||||
self.save()
|
self.save()
|
||||||
@ -447,6 +573,14 @@ class Subscription(Document):
|
|||||||
if invoice:
|
if invoice:
|
||||||
return invoice.precision('grand_total')
|
return invoice.precision('grand_total')
|
||||||
|
|
||||||
|
def get_calendar_months(billing_interval):
|
||||||
|
calendar_months = []
|
||||||
|
start = 0
|
||||||
|
while start < 12:
|
||||||
|
start += billing_interval
|
||||||
|
calendar_months.append(start)
|
||||||
|
|
||||||
|
return calendar_months
|
||||||
|
|
||||||
def get_prorata_factor(period_end, period_start):
|
def get_prorata_factor(period_end, period_start):
|
||||||
diff = flt(date_diff(nowdate(), period_start) + 1)
|
diff = flt(date_diff(nowdate(), period_start) + 1)
|
||||||
@ -469,10 +603,7 @@ def get_all_subscriptions():
|
|||||||
"""
|
"""
|
||||||
Returns all `Subscription` documents
|
Returns all `Subscription` documents
|
||||||
"""
|
"""
|
||||||
return frappe.db.sql(
|
return frappe.db.get_all('Subscription', {'status': ('!=','Cancelled')})
|
||||||
'select name from `tabSubscription` where status != "Cancelled"',
|
|
||||||
as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process(data):
|
def process(data):
|
||||||
|
|||||||
@ -4,6 +4,8 @@ frappe.listview_settings['Subscription'] = {
|
|||||||
return [__("Trialling"), "green"];
|
return [__("Trialling"), "green"];
|
||||||
} else if(doc.status === 'Active') {
|
} else if(doc.status === 'Active') {
|
||||||
return [__("Active"), "green"];
|
return [__("Active"), "green"];
|
||||||
|
} else if(doc.status === 'Completed') {
|
||||||
|
return [__("Completed"), "green"];
|
||||||
} else if(doc.status === 'Past Due Date') {
|
} else if(doc.status === 'Past Due Date') {
|
||||||
return [__("Past Due Date"), "orange"];
|
return [__("Past Due Date"), "orange"];
|
||||||
} else if(doc.status === 'Unpaid') {
|
} else if(doc.status === 'Unpaid') {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
|
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
|
||||||
from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt
|
from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt, get_date_str
|
||||||
|
|
||||||
|
|
||||||
def create_plan():
|
def create_plan():
|
||||||
@ -15,7 +15,7 @@ def create_plan():
|
|||||||
plan = frappe.new_doc('Subscription Plan')
|
plan = frappe.new_doc('Subscription Plan')
|
||||||
plan.plan_name = '_Test Plan Name'
|
plan.plan_name = '_Test Plan Name'
|
||||||
plan.item = '_Test Non Stock Item'
|
plan.item = '_Test Non Stock Item'
|
||||||
plan.price_determination = "Fixed rate"
|
plan.price_determination = "Fixed Rate"
|
||||||
plan.cost = 900
|
plan.cost = 900
|
||||||
plan.billing_interval = 'Month'
|
plan.billing_interval = 'Month'
|
||||||
plan.billing_interval_count = 1
|
plan.billing_interval_count = 1
|
||||||
@ -25,7 +25,7 @@ def create_plan():
|
|||||||
plan = frappe.new_doc('Subscription Plan')
|
plan = frappe.new_doc('Subscription Plan')
|
||||||
plan.plan_name = '_Test Plan Name 2'
|
plan.plan_name = '_Test Plan Name 2'
|
||||||
plan.item = '_Test Non Stock Item'
|
plan.item = '_Test Non Stock Item'
|
||||||
plan.price_determination = "Fixed rate"
|
plan.price_determination = "Fixed Rate"
|
||||||
plan.cost = 1999
|
plan.cost = 1999
|
||||||
plan.billing_interval = 'Month'
|
plan.billing_interval = 'Month'
|
||||||
plan.billing_interval_count = 1
|
plan.billing_interval_count = 1
|
||||||
@ -35,12 +35,29 @@ def create_plan():
|
|||||||
plan = frappe.new_doc('Subscription Plan')
|
plan = frappe.new_doc('Subscription Plan')
|
||||||
plan.plan_name = '_Test Plan Name 3'
|
plan.plan_name = '_Test Plan Name 3'
|
||||||
plan.item = '_Test Non Stock Item'
|
plan.item = '_Test Non Stock Item'
|
||||||
plan.price_determination = "Fixed rate"
|
plan.price_determination = "Fixed Rate"
|
||||||
plan.cost = 1999
|
plan.cost = 1999
|
||||||
plan.billing_interval = 'Day'
|
plan.billing_interval = 'Day'
|
||||||
plan.billing_interval_count = 14
|
plan.billing_interval_count = 14
|
||||||
plan.insert()
|
plan.insert()
|
||||||
|
|
||||||
|
# Defined a quarterly Subscription Plan
|
||||||
|
if not frappe.db.exists('Subscription Plan', '_Test Plan Name 4'):
|
||||||
|
plan = frappe.new_doc('Subscription Plan')
|
||||||
|
plan.plan_name = '_Test Plan Name 4'
|
||||||
|
plan.item = '_Test Non Stock Item'
|
||||||
|
plan.price_determination = "Monthly Rate"
|
||||||
|
plan.cost = 20000
|
||||||
|
plan.billing_interval = 'Month'
|
||||||
|
plan.billing_interval_count = 3
|
||||||
|
plan.insert()
|
||||||
|
|
||||||
|
if not frappe.db.exists('Supplier', '_Test Supplier'):
|
||||||
|
supplier = frappe.new_doc('Supplier')
|
||||||
|
supplier.supplier_name = '_Test Supplier'
|
||||||
|
supplier.supplier_group = 'All Supplier Groups'
|
||||||
|
supplier.insert()
|
||||||
|
|
||||||
class TestSubscription(unittest.TestCase):
|
class TestSubscription(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -48,7 +65,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_create_subscription_with_trial_with_correct_period(self):
|
def test_create_subscription_with_trial_with_correct_period(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.trial_period_start = nowdate()
|
subscription.trial_period_start = nowdate()
|
||||||
subscription.trial_period_end = add_days(nowdate(), 30)
|
subscription.trial_period_end = add_days(nowdate(), 30)
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
@ -56,8 +74,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(subscription.trial_period_start, nowdate())
|
self.assertEqual(subscription.trial_period_start, nowdate())
|
||||||
self.assertEqual(subscription.trial_period_end, add_days(nowdate(), 30))
|
self.assertEqual(subscription.trial_period_end, add_days(nowdate(), 30))
|
||||||
self.assertEqual(subscription.trial_period_start, subscription.current_invoice_start)
|
self.assertEqual(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start))
|
||||||
self.assertEqual(subscription.trial_period_end, subscription.current_invoice_end)
|
self.assertEqual(add_days(subscription.current_invoice_start, 30), get_date_str(subscription.current_invoice_end))
|
||||||
self.assertEqual(subscription.invoices, [])
|
self.assertEqual(subscription.invoices, [])
|
||||||
self.assertEqual(subscription.status, 'Trialling')
|
self.assertEqual(subscription.status, 'Trialling')
|
||||||
|
|
||||||
@ -65,7 +83,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_create_subscription_without_trial_with_correct_period(self):
|
def test_create_subscription_without_trial_with_correct_period(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
|
|
||||||
@ -81,7 +100,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_create_subscription_trial_with_wrong_dates(self):
|
def test_create_subscription_trial_with_wrong_dates(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.trial_period_end = nowdate()
|
subscription.trial_period_end = nowdate()
|
||||||
subscription.trial_period_start = add_days(nowdate(), 30)
|
subscription.trial_period_start = add_days(nowdate(), 30)
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
@ -91,7 +111,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_create_subscription_multi_with_different_billing_fails(self):
|
def test_create_subscription_multi_with_different_billing_fails(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.trial_period_end = nowdate()
|
subscription.trial_period_end = nowdate()
|
||||||
subscription.trial_period_start = add_days(nowdate(), 30)
|
subscription.trial_period_start = add_days(nowdate(), 30)
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
@ -102,8 +123,9 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_invoice_is_generated_at_end_of_billing_period(self):
|
def test_invoice_is_generated_at_end_of_billing_period(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
subscription.start = '2018-01-01'
|
subscription.party = '_Test Customer'
|
||||||
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
|
|
||||||
@ -114,18 +136,22 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.current_invoice_start, '2018-01-01')
|
self.assertEqual(subscription.current_invoice_start, '2018-01-01')
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
subscription.process()
|
||||||
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
subscription.delete()
|
subscription.delete()
|
||||||
|
|
||||||
def test_status_goes_back_to_active_after_invoice_is_paid(self):
|
def test_status_goes_back_to_active_after_invoice_is_paid(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
|
||||||
|
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||||
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
|
|
||||||
subscription.get_current_invoice()
|
subscription.get_current_invoice()
|
||||||
current_invoice = subscription.get_current_invoice()
|
current_invoice = subscription.get_current_invoice()
|
||||||
@ -137,7 +163,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
subscription.process()
|
subscription.process()
|
||||||
|
|
||||||
self.assertEqual(subscription.status, 'Active')
|
self.assertEqual(subscription.status, 'Active')
|
||||||
self.assertEqual(subscription.current_invoice_start, add_months(subscription.start, 1))
|
self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1))
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
subscription.delete()
|
subscription.delete()
|
||||||
@ -149,16 +175,17 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
|
|
||||||
|
self.assertEqual(subscription.status, 'Active')
|
||||||
|
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
|
||||||
|
|
||||||
subscription.process()
|
|
||||||
# This should change status to Cancelled since grace period is 0
|
# This should change status to Cancelled since grace period is 0
|
||||||
|
# And is backdated subscription so subscription will be cancelled after processing
|
||||||
self.assertEqual(subscription.status, 'Cancelled')
|
self.assertEqual(subscription.status, 'Cancelled')
|
||||||
|
|
||||||
settings.cancel_after_grace = default_grace_period_action
|
settings.cancel_after_grace = default_grace_period_action
|
||||||
@ -172,16 +199,14 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||||
|
|
||||||
subscription.process()
|
|
||||||
# This should change status to Cancelled since grace period is 0
|
|
||||||
self.assertEqual(subscription.status, 'Unpaid')
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
|
|
||||||
settings.cancel_after_grace = default_grace_period_action
|
settings.cancel_after_grace = default_grace_period_action
|
||||||
@ -190,10 +215,11 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_invoice_days_until_due(self):
|
def test_subscription_invoice_days_until_due(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.days_until_due = 10
|
subscription.days_until_due = 10
|
||||||
subscription.start = add_months(nowdate(), -1)
|
subscription.start_date = add_months(nowdate(), -1)
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
@ -208,9 +234,10 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
|
||||||
@ -232,7 +259,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_remains_active_during_invoice_period(self):
|
def test_subscription_remains_active_during_invoice_period(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
subscription.process() # no changes expected
|
subscription.process() # no changes expected
|
||||||
@ -258,7 +286,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_cancelation(self):
|
def test_subscription_cancelation(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
@ -274,7 +303,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
|
|
||||||
@ -309,7 +339,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
@ -329,7 +360,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
@ -353,16 +385,14 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
invoices = len(subscription.invoices)
|
invoices = len(subscription.invoices)
|
||||||
|
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
|
||||||
self.assertEqual(len(subscription.invoices), invoices)
|
|
||||||
|
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
self.assertEqual(subscription.status, 'Cancelled')
|
self.assertEqual(subscription.status, 'Cancelled')
|
||||||
self.assertEqual(len(subscription.invoices), invoices)
|
self.assertEqual(len(subscription.invoices), invoices)
|
||||||
@ -387,15 +417,14 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||||
|
|
||||||
subscription.process()
|
|
||||||
self.assertEqual(subscription.status, 'Unpaid')
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
|
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
@ -424,16 +453,14 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start_date = '2018-01-01'
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
|
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
# This should change status to Unpaid since grace period is 0
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
|
||||||
|
|
||||||
subscription.process()
|
|
||||||
# This should change status to Cancelled since grace period is 0
|
|
||||||
self.assertEqual(subscription.status, 'Unpaid')
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
|
|
||||||
invoice = subscription.get_current_invoice()
|
invoice = subscription.get_current_invoice()
|
||||||
@ -445,7 +472,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
# A new invoice is generated
|
# A new invoice is generated
|
||||||
subscription.process()
|
subscription.process()
|
||||||
self.assertEqual(subscription.status, 'Past Due Date')
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
|
|
||||||
settings.cancel_after_grace = default_grace_period_action
|
settings.cancel_after_grace = default_grace_period_action
|
||||||
settings.save()
|
settings.save()
|
||||||
@ -453,7 +480,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_restart_active_subscription(self):
|
def test_restart_active_subscription(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
|
|
||||||
@ -463,7 +491,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_invoice_discount_percentage(self):
|
def test_subscription_invoice_discount_percentage(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.additional_discount_percentage = 10
|
subscription.additional_discount_percentage = 10
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
@ -478,7 +507,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_invoice_discount_amount(self):
|
def test_subscription_invoice_discount_amount(self):
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.additional_discount_amount = 11
|
subscription.additional_discount_amount = 11
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
@ -495,7 +525,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
# Create a non pre-billed subscription, processing should not create
|
# Create a non pre-billed subscription, processing should not create
|
||||||
# invoices.
|
# invoices.
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
subscription.process()
|
subscription.process()
|
||||||
@ -517,10 +548,12 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.party_type = 'Customer'
|
||||||
|
subscription.party = '_Test Customer'
|
||||||
subscription.generate_invoice_at_period_start = True
|
subscription.generate_invoice_at_period_start = True
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.save()
|
subscription.save()
|
||||||
|
subscription.process()
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
|
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
@ -538,3 +571,65 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription.delete()
|
subscription.delete()
|
||||||
|
|
||||||
|
def test_subscription_with_follow_calendar_months(self):
|
||||||
|
subscription = frappe.new_doc('Subscription')
|
||||||
|
subscription.party_type = 'Supplier'
|
||||||
|
subscription.party = '_Test Supplier'
|
||||||
|
subscription.generate_invoice_at_period_start = 1
|
||||||
|
subscription.follow_calendar_months = 1
|
||||||
|
|
||||||
|
# select subscription start date as '2018-01-15'
|
||||||
|
subscription.start_date = '2018-01-15'
|
||||||
|
subscription.end_date = '2018-07-15'
|
||||||
|
subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
|
||||||
|
subscription.save()
|
||||||
|
|
||||||
|
# even though subscription starts at '2018-01-15' and Billing interval is Month and count 3
|
||||||
|
# First invoice will end at '2018-03-31' instead of '2018-04-14'
|
||||||
|
self.assertEqual(get_date_str(subscription.current_invoice_end), '2018-03-31')
|
||||||
|
|
||||||
|
def test_subscription_generate_invoice_past_due(self):
|
||||||
|
subscription = frappe.new_doc('Subscription')
|
||||||
|
subscription.party_type = 'Supplier'
|
||||||
|
subscription.party = '_Test Supplier'
|
||||||
|
subscription.generate_invoice_at_period_start = 1
|
||||||
|
subscription.generate_new_invoices_past_due_date = 1
|
||||||
|
# select subscription start date as '2018-01-15'
|
||||||
|
subscription.start_date = '2018-01-01'
|
||||||
|
subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
|
||||||
|
subscription.save()
|
||||||
|
|
||||||
|
# Process subscription and create first invoice
|
||||||
|
# Subscription status will be unpaid since due date has already passed
|
||||||
|
subscription.process()
|
||||||
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
|
|
||||||
|
# Now the Subscription is unpaid
|
||||||
|
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
|
||||||
|
# subscription
|
||||||
|
|
||||||
|
subscription.process()
|
||||||
|
self.assertEqual(len(subscription.invoices), 2)
|
||||||
|
|
||||||
|
def test_subscription_without_generate_invoice_past_due(self):
|
||||||
|
subscription = frappe.new_doc('Subscription')
|
||||||
|
subscription.party_type = 'Supplier'
|
||||||
|
subscription.party = '_Test Supplier'
|
||||||
|
subscription.generate_invoice_at_period_start = 1
|
||||||
|
# select subscription start date as '2018-01-15'
|
||||||
|
subscription.start_date = '2018-01-01'
|
||||||
|
subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
|
||||||
|
subscription.save()
|
||||||
|
|
||||||
|
# Process subscription and create first invoice
|
||||||
|
# Subscription status will be unpaid since due date has already passed
|
||||||
|
subscription.process()
|
||||||
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
self.assertEqual(subscription.status, 'Unpaid')
|
||||||
|
|
||||||
|
subscription.process()
|
||||||
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,73 +1,40 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-02-26 04:21:41.265055",
|
"creation": "2018-02-26 04:21:41.265055",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"document_type",
|
||||||
|
"invoice"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"fieldname": "document_type",
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "invoice",
|
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"label": "Document Type ",
|
||||||
"ignore_user_permissions": 0,
|
"options": "DocType",
|
||||||
"ignore_xss_filter": 0,
|
"read_only": 1
|
||||||
"in_filter": 0,
|
},
|
||||||
"in_global_search": 0,
|
{
|
||||||
|
"fieldname": "invoice",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Invoice",
|
"label": "Invoice",
|
||||||
"length": 0,
|
"options": "document_type",
|
||||||
"no_copy": 0,
|
"read_only": 1
|
||||||
"options": "Sales Invoice",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-02-26 10:48:07.033422",
|
"modified": "2020-06-01 22:23:54.462718",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription Invoice",
|
"name": "Subscription Invoice",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "field:plan_name",
|
"autoname": "field:plan_name",
|
||||||
"creation": "2018-02-24 11:31:23.066506",
|
"creation": "2018-02-24 11:31:23.066506",
|
||||||
@ -24,6 +25,7 @@
|
|||||||
"column_break_16",
|
"column_break_16",
|
||||||
"payment_gateway",
|
"payment_gateway",
|
||||||
"accounting_dimensions_section",
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
"dimension_col_break"
|
"dimension_col_break"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -60,8 +62,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "price_determination",
|
"fieldname": "price_determination",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Price Determination",
|
"label": "Subscription Price Based On",
|
||||||
"options": "\nFixed rate\nBased on price list",
|
"options": "\nFixed Rate\nBased On Price List\nMonthly Rate",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -69,7 +71,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.price_determination==\"Fixed rate\"",
|
"depends_on": "eval:['Fixed Rate', 'Monthly Rate'].includes(doc.price_determination)",
|
||||||
"fieldname": "cost",
|
"fieldname": "cost",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -136,9 +138,16 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cost Center",
|
||||||
|
"options": "Cost Center"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2019-07-25 18:35:04.362556",
|
"links": [],
|
||||||
|
"modified": "2020-06-25 10:53:44.205774",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription Plan",
|
"name": "Subscription Plan",
|
||||||
@ -155,6 +164,30 @@
|
|||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.utils import get_first_day, get_last_day, date_diff, flt, getdate
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.utilities.product import get_price
|
from erpnext.utilities.product import get_price
|
||||||
|
|
||||||
@ -17,12 +18,12 @@ class SubscriptionPlan(Document):
|
|||||||
frappe.throw(_('Billing Interval Count cannot be less than 1'))
|
frappe.throw(_('Billing Interval Count cannot be less than 1'))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_plan_rate(plan, quantity=1, customer=None):
|
def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1):
|
||||||
plan = frappe.get_doc("Subscription Plan", plan)
|
plan = frappe.get_doc("Subscription Plan", plan)
|
||||||
if plan.price_determination == "Fixed rate":
|
if plan.price_determination == "Fixed Rate":
|
||||||
return plan.cost
|
return plan.cost * prorate_factor
|
||||||
|
|
||||||
elif plan.price_determination == "Based on price list":
|
elif plan.price_determination == "Based On Price List":
|
||||||
if customer:
|
if customer:
|
||||||
customer_group = frappe.db.get_value("Customer", customer, "customer_group")
|
customer_group = frappe.db.get_value("Customer", customer, "customer_group")
|
||||||
else:
|
else:
|
||||||
@ -32,4 +33,25 @@ def get_plan_rate(plan, quantity=1, customer=None):
|
|||||||
if not price:
|
if not price:
|
||||||
return 0
|
return 0
|
||||||
else:
|
else:
|
||||||
return price.price_list_rate
|
return price.price_list_rate * prorate_factor
|
||||||
|
|
||||||
|
elif plan.price_determination == 'Monthly Rate':
|
||||||
|
start_date = getdate(start_date)
|
||||||
|
end_date = getdate(end_date)
|
||||||
|
|
||||||
|
no_of_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1
|
||||||
|
cost = plan.cost * no_of_months
|
||||||
|
|
||||||
|
# Adjust cost if start or end date is not month start or end
|
||||||
|
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
|
||||||
|
|
||||||
|
if prorate:
|
||||||
|
prorate_factor = flt(date_diff(start_date, get_first_day(start_date)) / date_diff(
|
||||||
|
get_last_day(start_date), get_first_day(start_date)), 1)
|
||||||
|
|
||||||
|
prorate_factor += flt(date_diff(get_last_day(end_date), end_date) / date_diff(
|
||||||
|
get_last_day(end_date), get_first_day(end_date)), 1)
|
||||||
|
|
||||||
|
cost -= (plan.cost * prorate_factor)
|
||||||
|
|
||||||
|
return cost
|
||||||
@ -1,106 +1,40 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-02-25 07:35:07.736146",
|
"creation": "2018-02-25 07:35:07.736146",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"plan",
|
||||||
|
"qty"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "qty",
|
"fieldname": "qty",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Quantity",
|
"label": "Quantity",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "plan",
|
"fieldname": "plan",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Plan",
|
"label": "Plan",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Subscription Plan",
|
"options": "Subscription Plan",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-06-20 15:35:13.514699",
|
"modified": "2020-06-14 17:44:05.275100",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription Plan Detail",
|
"name": "Subscription Plan Detail",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@ -1,179 +1,76 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-02-26 06:13:37.910139",
|
"creation": "2018-02-26 06:13:37.910139",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"grace_period",
|
||||||
|
"cancel_after_grace",
|
||||||
|
"prorate"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid",
|
"description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid",
|
||||||
"fieldname": "grace_period",
|
"fieldname": "grace_period",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 0,
|
"label": "Grace Period"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Grace Period",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "cancel_after_grace",
|
"fieldname": "cancel_after_grace",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Cancel Subscription After Grace Period"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Cancel Invoice After Grace Period",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "prorate",
|
"fieldname": "prorate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Prorate"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Prorate",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"istable": 0,
|
"links": [],
|
||||||
"max_attachments": 0,
|
"modified": "2020-06-23 09:13:44.292792",
|
||||||
"modified": "2018-02-26 13:58:09.455832",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription Settings",
|
"name": "Subscription Settings",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
"role": "Accounts Manager",
|
||||||
"role": "Administrator",
|
"share": 1,
|
||||||
"set_user_permissions": 0,
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user