Merge branch 'develop' into dependabot/npm_and_yarn/lodash-4.17.19
This commit is contained in:
commit
786e0606f5
@ -147,10 +147,15 @@
|
||||
"link_to": "Trial Balance",
|
||||
"type": "Report"
|
||||
},
|
||||
{
|
||||
"label": "Point of Sale",
|
||||
"link_to": "point-of-sale",
|
||||
"type": "Page"
|
||||
},
|
||||
{
|
||||
"label": "Dashboard",
|
||||
"link_to": "Accounts",
|
||||
"type": "Dashboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ def build_tree_from_json(chart_template, chart_data=None):
|
||||
|
||||
account['parent_account'] = parent
|
||||
account['expandable'] = True if identify_is_group(child) else False
|
||||
account['value'] = (child.get('account_number') + ' - ' + account_name) \
|
||||
account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
|
||||
if child.get('account_number') else account_name
|
||||
accounts.append(account)
|
||||
_import_accounts(child, account['value'])
|
||||
|
@ -225,7 +225,7 @@
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-22 20:13:26.043092",
|
||||
"modified": "2020-08-03 20:13:26.043092",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -60,12 +60,12 @@ class BankClearance(Document):
|
||||
""".format(condition=condition), {"account": self.account, "from":self.from_date,
|
||||
"to": self.to_date, "bank_account": self.bank_account}, as_dict=1)
|
||||
|
||||
pos_entries = []
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if self.include_pos_transactions:
|
||||
pos_entries = frappe.db.sql("""
|
||||
pos_sales_invoices = frappe.db.sql("""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.debit_to as against_account, sip.clearance_date,
|
||||
si.posting_date, si.customer as against_account, sip.clearance_date,
|
||||
account.account_currency, 0 as credit
|
||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
||||
where
|
||||
@ -75,7 +75,20 @@ class BankClearance(Document):
|
||||
si.posting_date ASC, si.name DESC
|
||||
""", {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1)
|
||||
|
||||
entries = sorted(list(payment_entries)+list(journal_entries+list(pos_entries)),
|
||||
pos_purchase_invoices = frappe.db.sql("""
|
||||
select
|
||||
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
|
||||
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
|
||||
account.account_currency, 0 as debit
|
||||
from `tabPurchase Invoice` pi, `tabAccount` account
|
||||
where
|
||||
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
|
||||
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
|
||||
order by
|
||||
pi.posting_date ASC, pi.name DESC
|
||||
""", {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
|
||||
|
||||
entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
|
||||
key=lambda k: k['posting_date'] or getdate(nowdate()))
|
||||
|
||||
self.set('payment_entries', [])
|
||||
|
@ -26,22 +26,22 @@ def test_create_test_data():
|
||||
"item_group": "_Test Item Group",
|
||||
"item_name": "_Test Tesla Car",
|
||||
"apply_warehouse_wise_reorder_level": 0,
|
||||
"warehouse":"_Test Warehouse - _TC",
|
||||
"warehouse":"Stores - TCP1",
|
||||
"gst_hsn_code": "999800",
|
||||
"valuation_rate": 5000,
|
||||
"standard_rate":5000,
|
||||
"item_defaults": [{
|
||||
"company": "_Test Company",
|
||||
"default_warehouse": "_Test Warehouse - _TC",
|
||||
"company": "_Test Company with perpetual inventory",
|
||||
"default_warehouse": "Stores - TCP1",
|
||||
"default_price_list":"_Test Price List",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"buying_cost_center": "_Test Cost Center - _TC",
|
||||
"selling_cost_center": "_Test Cost Center - _TC",
|
||||
"income_account": "Sales - _TC"
|
||||
"expense_account": "Cost of Goods Sold - TCP1",
|
||||
"buying_cost_center": "Main - TCP1",
|
||||
"selling_cost_center": "Main - TCP1",
|
||||
"income_account": "Sales - TCP1"
|
||||
}],
|
||||
"show_in_website": 1,
|
||||
"route":"-test-tesla-car",
|
||||
"website_warehouse": "_Test Warehouse - _TC"
|
||||
"website_warehouse": "Stores - TCP1"
|
||||
})
|
||||
item.insert()
|
||||
# create test item price
|
||||
@ -63,12 +63,12 @@ def test_create_test_data():
|
||||
"items": [{
|
||||
"item_code": "_Test Tesla Car"
|
||||
}],
|
||||
"warehouse":"_Test Warehouse - _TC",
|
||||
"warehouse":"Stores - TCP1",
|
||||
"coupon_code_based":1,
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 30,
|
||||
"company": "_Test Company",
|
||||
"company": "_Test Company with perpetual inventory",
|
||||
"currency":"INR",
|
||||
"for_price_list":"_Test Price List"
|
||||
})
|
||||
@ -112,7 +112,10 @@ class TestCouponCode(unittest.TestCase):
|
||||
self.assertEqual(coupon_code.get("used"),0)
|
||||
|
||||
def test_2_sales_order_with_coupon_code(self):
|
||||
so = make_sales_order(customer="_Test Customer",selling_price_list="_Test Price List",item_code="_Test Tesla Car", rate=5000,qty=1, do_not_submit=True)
|
||||
so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
|
||||
customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1,
|
||||
do_not_submit=True)
|
||||
|
||||
so = frappe.get_doc('Sales Order', so.name)
|
||||
# check item price before coupon code is applied
|
||||
self.assertEqual(so.items[0].rate, 5000)
|
||||
@ -120,7 +123,7 @@ class TestCouponCode(unittest.TestCase):
|
||||
so.sales_partner='_Test Coupon Partner'
|
||||
so.save()
|
||||
# check item price after coupon code is applied
|
||||
self.assertEqual(so.items[0].rate, 3500)
|
||||
self.assertEqual(so.items[0].rate, 3500)
|
||||
so.submit()
|
||||
|
||||
def test_3_check_coupon_code_used_after_so(self):
|
||||
|
162
erpnext/accounts/doctype/dunning/dunning.js
Normal file
162
erpnext/accounts/doctype/dunning/dunning.js
Normal file
@ -0,0 +1,162 @@
|
||||
// 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"));
|
||||
}
|
||||
|
||||
if(frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(__('Ledger'), function() {
|
||||
frappe.route_options = {
|
||||
"voucher_no": frm.doc.name,
|
||||
"from_date": frm.doc.posting_date,
|
||||
"to_date": frm.doc.posting_date,
|
||||
"company": frm.doc.company,
|
||||
"show_cancelled_entries": frm.doc.docstatus === 2
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
}, __('View'));
|
||||
}
|
||||
},
|
||||
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 = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount'));
|
||||
const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount'));
|
||||
const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total'));
|
||||
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",
|
||||
"dunning_fee",
|
||||
"column_break_8",
|
||||
"rate_of_interest",
|
||||
"interest_amount",
|
||||
"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.rate_of_interest",
|
||||
"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-08-03 18:55:43.683053",
|
||||
"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
|
||||
}
|
121
erpnext/accounts/doctype/dunning/dunning.py
Normal file
121
erpnext/accounts/doctype/dunning/dunning.py
Normal file
@ -0,0 +1,121 @@
|
||||
# -*- 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, cint
|
||||
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 = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
|
||||
if self.dunning_amount != amounts.get('dunning_amount'):
|
||||
self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount'))
|
||||
if self.grand_total != amounts.get('grand_total'):
|
||||
self.grand_total = flt(amounts.get('grand_total'), self.precision('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 = flt(outstanding_amount) * flt(rate_of_interest) / 100
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
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
|
||||
}
|
17
erpnext/accounts/doctype/dunning/dunning_dashboard.py
Normal file
17
erpnext/accounts/doctype/dunning/dunning_dashboard.py
Normal file
@ -0,0 +1,17 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'dunning',
|
||||
'non_standard_fieldnames': {
|
||||
'Journal Entry': 'reference_name',
|
||||
'Payment Entry': 'reference_name'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Payment'),
|
||||
'items': ['Payment Entry', 'Journal Entry']
|
||||
}
|
||||
]
|
||||
}
|
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
|
@ -6,6 +6,7 @@ import unittest, frappe
|
||||
from frappe.utils import flt, nowdate
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.accounts.general_ledger import StockAccountInvalidTransaction
|
||||
|
||||
class TestJournalEntry(unittest.TestCase):
|
||||
def test_journal_entry_with_against_jv(self):
|
||||
@ -81,19 +82,46 @@ class TestJournalEntry(unittest.TestCase):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
||||
set_perpetual_inventory()
|
||||
|
||||
jv = frappe.copy_doc(test_records[0])
|
||||
jv = frappe.copy_doc({
|
||||
"cheque_date": nowdate(),
|
||||
"cheque_no": "33",
|
||||
"company": "_Test Company with perpetual inventory",
|
||||
"doctype": "Journal Entry",
|
||||
"accounts": [
|
||||
{
|
||||
"account": "Debtors - TCP1",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Customer",
|
||||
"credit_in_account_currency": 400.0,
|
||||
"debit_in_account_currency": 0.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "Main - TCP1"
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - TCP1",
|
||||
"credit_in_account_currency": 0.0,
|
||||
"debit_in_account_currency": 400.0,
|
||||
"doctype": "Journal Entry Account",
|
||||
"parentfield": "accounts",
|
||||
"cost_center": "Main - TCP1"
|
||||
}
|
||||
],
|
||||
"naming_series": "_T-Journal Entry-",
|
||||
"posting_date": nowdate(),
|
||||
"user_remark": "test",
|
||||
"voucher_type": "Bank Entry"
|
||||
})
|
||||
|
||||
jv.get("accounts")[0].update({
|
||||
"account": get_inventory_account('_Test Company'),
|
||||
"company": "_Test Company",
|
||||
"account": get_inventory_account('_Test Company with perpetual inventory'),
|
||||
"company": "_Test Company with perpetual inventory",
|
||||
"party_type": None,
|
||||
"party": None
|
||||
})
|
||||
|
||||
jv.insert()
|
||||
|
||||
from erpnext.accounts.general_ledger import StockAccountInvalidTransaction
|
||||
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
|
||||
|
||||
jv.cancel()
|
||||
set_perpetual_inventory(0)
|
||||
|
||||
def test_multi_currency(self):
|
||||
|
@ -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",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"creation": "2018-01-23 05:40:18.117583",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"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": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "loyalty_program",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "loyalty_program",
|
||||
"fieldtype": "Link",
|
||||
"label": "Loyalty Program",
|
||||
"options": "Loyalty Program"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "loyalty_program_tier",
|
||||
"fieldtype": "Data",
|
||||
"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 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
|
||||
},
|
||||
"fieldname": "loyalty_program_tier",
|
||||
"fieldtype": "Data",
|
||||
"label": "Loyalty Program Tier"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "customer",
|
||||
"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": "Customer",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
"fieldname": "redeem_against",
|
||||
"fieldtype": "Link",
|
||||
"label": "Redeem Against",
|
||||
"options": "Loyalty Point Entry"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "redeem_against",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "loyalty_points",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Loyalty Points"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "loyalty_points",
|
||||
"fieldtype": "Int",
|
||||
"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": "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
|
||||
},
|
||||
"fieldname": "purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Purchase Amount"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "purchase_amount",
|
||||
"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": "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
|
||||
},
|
||||
"fieldname": "expiry_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Expiry Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "expiry_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_standard_filter": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "posting_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": "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
|
||||
},
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"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",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"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
|
||||
"fieldname": "invoice_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Invoice Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice",
|
||||
"options": "invoice_type"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 1,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-08-29 16:05:22.810347",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Loyalty Point Entry",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"in_create": 1,
|
||||
"modified": "2020-01-30 17:27:55.964242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Loyalty Point Entry",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Auditor",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
},
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Auditor"
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
},
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "customer",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "customer",
|
||||
"track_changes": 1
|
||||
}
|
@ -18,7 +18,7 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No
|
||||
date = today()
|
||||
|
||||
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`
|
||||
where customer=%s and loyalty_program=%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}
|
||||
|
||||
@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)
|
||||
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))
|
||||
@ -59,10 +60,10 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None
|
||||
if not 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"))
|
||||
elif silent and not loyalty_program:
|
||||
return frappe._dict({"loyalty_program": None})
|
||||
return frappe._dict({"loyalty_programs": None})
|
||||
|
||||
if not company:
|
||||
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"})
|
||||
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(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)
|
||||
|
||||
lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_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_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', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
|
||||
|
||||
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
|
||||
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
|
||||
@ -66,7 +66,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
|
||||
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(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"})
|
||||
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_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.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', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
|
||||
|
||||
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
|
||||
self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
|
||||
@ -101,7 +101,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
si.insert()
|
||||
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))
|
||||
|
||||
# cancelling sales invoice
|
||||
@ -118,7 +118,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
si_original.submit()
|
||||
|
||||
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)
|
||||
|
||||
# create sales invoice return
|
||||
@ -130,10 +130,10 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
si_return.submit()
|
||||
|
||||
# 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)
|
||||
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(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points))
|
||||
|
||||
|
@ -25,7 +25,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
});
|
||||
frm.set_query("party_type", function() {
|
||||
return{
|
||||
"filters": {
|
||||
filters: {
|
||||
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("party_bank_account", function() {
|
||||
return {
|
||||
filters: {
|
||||
"is_company_account":0,
|
||||
is_company_account: 0,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party
|
||||
}
|
||||
@ -42,7 +42,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("bank_account", function() {
|
||||
return {
|
||||
filters: {
|
||||
"is_company_account":1
|
||||
is_company_account: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -90,7 +90,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
frm.set_query("reference_doctype", "references", function() {
|
||||
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") {
|
||||
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
|
||||
} else if (frm.doc.party_type=="Employee") {
|
||||
@ -125,7 +125,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
const child = locals[cdt][cdn];
|
||||
const filters = {"docstatus": 1, "company": doc.company};
|
||||
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)) {
|
||||
filters[doc.party_type.toLowerCase()] = doc.party;
|
||||
@ -342,7 +342,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
() => {
|
||||
frm.set_party_account_based_on_party = false;
|
||||
if (r.message.bank_account) {
|
||||
frm.set_value("party_bank_account", r.message.bank_account);
|
||||
frm.set_value("bank_account", r.message.bank_account);
|
||||
}
|
||||
}
|
||||
]);
|
||||
@ -863,10 +863,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
|
@ -199,8 +199,8 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def validate_account_type(self, account, account_types):
|
||||
account_type = frappe.db.get_value("Account", account, "account_type")
|
||||
if account_type not in account_types:
|
||||
frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
|
||||
# if account_type not in account_types:
|
||||
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
|
||||
|
||||
def set_exchange_rate(self):
|
||||
if self.paid_from and not self.source_exchange_rate:
|
||||
@ -223,7 +223,7 @@ class PaymentEntry(AccountsController):
|
||||
if self.party_type == "Student":
|
||||
valid_reference_doctypes = ("Fees")
|
||||
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":
|
||||
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
|
||||
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")
|
||||
exchange_rate = 1
|
||||
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:
|
||||
total_amount = ref_doc.get("total_amount")
|
||||
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":
|
||||
if party_account_currency == company_currency:
|
||||
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":
|
||||
total_amount = ref_doc.advance_amount
|
||||
else:
|
||||
@ -925,8 +929,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||
bill_no = ref_doc.get("bill_no")
|
||||
elif reference_doctype == "Expense Claim":
|
||||
outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) \
|
||||
- flt(ref_doc.get("total_amount+reimbursed")) - flt(ref_doc.get("total_advance_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"))
|
||||
elif reference_doctype == "Employee Advance":
|
||||
outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount)
|
||||
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:
|
||||
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"
|
||||
elif dt in ("Purchase Invoice", "Purchase Order"):
|
||||
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)
|
||||
|
||||
# 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):
|
||||
payment_type = "Receive"
|
||||
else:
|
||||
@ -1006,6 +1010,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
elif dt == "Fees":
|
||||
grand_total = doc.grand_total
|
||||
outstanding_amount = doc.outstanding_amount
|
||||
elif dt == "Dunning":
|
||||
grand_total = doc.grand_total
|
||||
outstanding_amount = doc.grand_total
|
||||
else:
|
||||
if party_account_currency == doc.company_currency:
|
||||
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||
@ -1075,15 +1082,35 @@ 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):
|
||||
pe.append('references', reference)
|
||||
else:
|
||||
pe.append("references", {
|
||||
'reference_doctype': dt,
|
||||
'reference_name': dn,
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
'total_amount': grand_total,
|
||||
'outstanding_amount': outstanding_amount,
|
||||
'allocated_amount': outstanding_amount
|
||||
})
|
||||
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:
|
||||
pe.append("references", {
|
||||
'reference_doctype': dt,
|
||||
'reference_name': dn,
|
||||
"bill_no": doc.get("bill_no"),
|
||||
"due_date": doc.get("due_date"),
|
||||
'total_amount': grand_total,
|
||||
'outstanding_amount': outstanding_amount,
|
||||
'allocated_amount': outstanding_amount
|
||||
})
|
||||
|
||||
pe.setup_party_account_field()
|
||||
pe.set_missing_values()
|
||||
@ -1172,4 +1199,4 @@ def make_payment_order(source_name, target_doc=None):
|
||||
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
return doclist
|
||||
return doclist
|
@ -140,9 +140,6 @@ class PaymentRequest(Document):
|
||||
})
|
||||
|
||||
def set_as_paid(self):
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
payment_entry = self.create_payment_entry()
|
||||
self.make_invoice()
|
||||
|
||||
@ -254,7 +251,7 @@ class PaymentRequest(Document):
|
||||
|
||||
if status in ["Authorized", "Completed"]:
|
||||
redirect_to = None
|
||||
self.run_method("set_as_paid")
|
||||
self.set_as_paid()
|
||||
|
||||
# if shopping cart enabled and in session
|
||||
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
|
||||
|
@ -12,15 +12,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-left">{{ _('Grand Total') }}</td>
|
||||
<td class='text-right'>{{ data.grand_total or '' }} {{ currency.symbol }}</td>
|
||||
<td class="text-left font-bold">{{ _('Grand Total') }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-left">{{ _('Net Total') }}</td>
|
||||
<td class='text-right'>{{ data.net_total or '' }} {{ currency.symbol }}</td>
|
||||
<td class="text-left font-bold">{{ _('Net Total') }}</td>
|
||||
<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td>
|
||||
</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>
|
||||
</tr>
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
{% for d in data.payment_reconciliation %}
|
||||
<tr>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -55,12 +55,14 @@
|
||||
<!-- Section end -->
|
||||
|
||||
<!-- Taxes section -->
|
||||
{% if data.taxes %}
|
||||
<div>
|
||||
<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Taxes") }}</h6>
|
||||
<div class="tax-break-up" style="overflow-x: auto;">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">{{ _("Account") }}</th>
|
||||
<th class="text-left">{{ _("Rate") }}</th>
|
||||
<th class="text-right">{{ _("Amount") }}</th>
|
||||
</tr>
|
||||
@ -68,14 +70,16 @@
|
||||
<tbody>
|
||||
{% for d in data.taxes %}
|
||||
<tr>
|
||||
<td class="text-left">{{ d.account_head }}</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- Section end -->
|
||||
|
||||
</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
|
||||
// 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();
|
||||
|
||||
// number of asserts
|
||||
assert.expect(1);
|
||||
|
||||
frappe.run_serially([
|
||||
// insert a new POS Closing Voucher
|
||||
() => frappe.tests.make('POS Closing Voucher', [
|
||||
// insert a new POS Closing Entry
|
||||
() => frappe.tests.make('POS Closing Entry', [
|
||||
// values to be set
|
||||
{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 frappe.model.document import Document
|
||||
|
||||
class POSClosingVoucherTaxes(Document):
|
||||
class POSClosingEntryDetail(Document):
|
||||
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 frappe.model.document import Document
|
||||
|
||||
class POSClosingVoucherDetails(Document):
|
||||
class POSClosingEntryTaxes(Document):
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
334
erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
Normal file
334
erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
Normal file
@ -0,0 +1,334 @@
|
||||
# -*- 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(company='_Test Company with perpetual inventory',
|
||||
target_warehouse="Stores - TCP1", cost_center='Main - TCP1', expense_account='Cost of Goods Sold - TCP1')
|
||||
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
|
||||
pos = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1',
|
||||
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1',
|
||||
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1',
|
||||
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 - TCP1', 'amount': 1000})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
pos2 = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1',
|
||||
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1',
|
||||
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1',
|
||||
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 - TCP1', '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 = args.account_for_change_amount or "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', {
|
||||
setup: function(frm) {
|
||||
frm.set_query("print_format_for_online", function() {
|
||||
frm.set_query("print_format", function() {
|
||||
return {
|
||||
filters: [
|
||||
['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"} };
|
||||
});
|
||||
|
||||
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) {
|
||||
if(!doc.company) {
|
||||
frappe.throw(__('Please set Company'));
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2013-05-24 12:15:51",
|
||||
@ -11,17 +12,12 @@
|
||||
"customer",
|
||||
"company",
|
||||
"country",
|
||||
"warehouse",
|
||||
"campaign",
|
||||
"company_address",
|
||||
"column_break_9",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"allow_delete",
|
||||
"allow_user_to_edit_rate",
|
||||
"allow_user_to_edit_discount",
|
||||
"allow_print_before_pay",
|
||||
"display_items_in_stock",
|
||||
"warehouse",
|
||||
"campaign",
|
||||
"company_address",
|
||||
"section_break_15",
|
||||
"applicable_for_users",
|
||||
"section_break_11",
|
||||
@ -31,16 +27,11 @@
|
||||
"column_break_16",
|
||||
"customer_groups",
|
||||
"section_break_16",
|
||||
"print_format_for_online",
|
||||
"print_format",
|
||||
"letter_head",
|
||||
"column_break0",
|
||||
"tc_name",
|
||||
"select_print_heading",
|
||||
"offline_pos_section",
|
||||
"territory",
|
||||
"column_break_31",
|
||||
"print_format",
|
||||
"customer_group",
|
||||
"section_break_19",
|
||||
"selling_price_list",
|
||||
"currency",
|
||||
@ -104,15 +95,6 @@
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Country"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"oldfieldname": "warehouse",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "campaign",
|
||||
"fieldtype": "Link",
|
||||
@ -129,48 +111,6 @@
|
||||
"fieldname": "column_break_9",
|
||||
"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",
|
||||
"fieldtype": "Section Break",
|
||||
@ -185,13 +125,13 @@
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Mode of Payment"
|
||||
"label": "Payment Methods"
|
||||
},
|
||||
{
|
||||
"fieldname": "payments",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sales Invoice Payment",
|
||||
"options": "Sales Invoice Payment"
|
||||
"options": "POS Payment Method",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
@ -220,12 +160,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_format_for_online",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Format for Online",
|
||||
"options": "Print Format"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "letter_head",
|
||||
@ -258,39 +192,6 @@
|
||||
"oldfieldtype": "Select",
|
||||
"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",
|
||||
"fieldtype": "Section Break",
|
||||
@ -380,20 +281,49 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "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",
|
||||
"idx": 1,
|
||||
"modified": "2020-01-24 15:52:03.797701",
|
||||
"links": [],
|
||||
"modified": "2020-06-29 12:20:30.977272",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
@ -5,8 +5,6 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import msgprint, _
|
||||
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 frappe.model.document import Document
|
||||
|
||||
@ -16,7 +14,6 @@ class POSProfile(Document):
|
||||
self.validate_all_link_fields()
|
||||
self.validate_duplicate_groups()
|
||||
self.check_default_payment()
|
||||
self.validate_customer_territory_group()
|
||||
|
||||
def validate_default_profile(self):
|
||||
for row in self.applicable_for_users:
|
||||
@ -64,19 +61,6 @@ class POSProfile(Document):
|
||||
if len(default_mode_of_payment) > 1:
|
||||
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):
|
||||
self.set_defaults()
|
||||
|
||||
@ -111,9 +95,14 @@ def get_item_groups(pos_profile):
|
||||
|
||||
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()
|
||||
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()
|
||||
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
@ -8,7 +8,7 @@ def get_data():
|
||||
'fieldname': 'pos_profile',
|
||||
'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 unittest
|
||||
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):
|
||||
def test_pos_profile(self):
|
||||
@ -29,6 +29,44 @@ class TestPOSProfile(unittest.TestCase):
|
||||
|
||||
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):
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
@ -50,6 +88,12 @@ def make_pos_profile(**args):
|
||||
"write_off_account": args.write_off_account or "_Test Write Off - _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"):
|
||||
pos_profile.insert()
|
||||
|
@ -26,7 +26,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-01 09:46:47.599173",
|
||||
"modified": "2020-05-13 23:57:33.627305",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile User",
|
||||
|
@ -6,27 +6,19 @@ frappe.ui.form.on('POS Settings', {
|
||||
frm.trigger("get_invoice_fields");
|
||||
},
|
||||
|
||||
use_pos_in_offline_mode: function(frm) {
|
||||
frm.trigger("get_invoice_fields");
|
||||
},
|
||||
|
||||
get_invoice_fields: function(frm) {
|
||||
if (!frm.doc.use_pos_in_offline_mode) {
|
||||
frappe.model.with_doctype("Sales Invoice", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) {
|
||||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
||||
d.fieldtype === 'Table') {
|
||||
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
|
||||
frappe.model.with_doctype("Sales Invoice", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) {
|
||||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
||||
d.fieldtype === 'Table') {
|
||||
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""];
|
||||
}
|
||||
|
||||
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -5,24 +5,11 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"use_pos_in_offline_mode",
|
||||
"section_break_2",
|
||||
"fields"
|
||||
"invoice_fields"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"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",
|
||||
"fieldname": "invoice_fields",
|
||||
"fieldtype": "Table",
|
||||
"label": "POS Field",
|
||||
"options": "POS Field"
|
||||
@ -30,7 +17,7 @@
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2019-12-26 11:50:47.122997",
|
||||
"modified": "2020-06-01 15:46:41.478928",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"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.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
|
||||
|
||||
@ -366,7 +366,7 @@ def set_discount_amount(rate, item_details):
|
||||
|
||||
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
|
||||
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
|
||||
pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
|
||||
|
||||
|
@ -448,7 +448,7 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
doc.set_missing_values()
|
||||
|
||||
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 [])
|
||||
|
||||
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.events.create_invoice_discounting(cur_frm);
|
||||
}, __('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) {
|
||||
@ -276,7 +282,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
"customer": this.frm.doc.customer
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message && r.message.length) {
|
||||
if(r.message && r.message.length > 1) {
|
||||
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",
|
||||
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": [],
|
||||
"allow_import": 1,
|
||||
"allow_workflow": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-05-24 19:29:05",
|
||||
"doctype": "DocType",
|
||||
@ -13,6 +14,7 @@
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"is_pos",
|
||||
"is_consolidated",
|
||||
"pos_profile",
|
||||
"offline_pos_name",
|
||||
"is_return",
|
||||
@ -1158,6 +1160,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words (Company Currency)",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
@ -1215,6 +1218,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words_export",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1,
|
||||
@ -1921,6 +1925,13 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_consolidated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Consolidated",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "customer.is_internal_customer",
|
||||
@ -1936,7 +1947,7 @@
|
||||
"idx": 181,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-30 12:00:03.890180",
|
||||
"modified": "2020-07-18 05:07:16.725974",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"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 erpnext.accounts.party import get_party_account, get_due_date
|
||||
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.accounts.utils import get_account_currency
|
||||
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:
|
||||
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)
|
||||
|
||||
def validate_fixed_asset(self):
|
||||
@ -200,13 +198,13 @@ class SalesInvoice(SellingController):
|
||||
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
|
||||
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()
|
||||
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.delete_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()
|
||||
|
||||
# Healthcare Service Invoice.
|
||||
@ -265,9 +263,9 @@ class SalesInvoice(SellingController):
|
||||
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
|
||||
update_company_current_month_sales(self.company)
|
||||
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()
|
||||
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.delete_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)
|
||||
|
||||
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')):
|
||||
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')
|
||||
|
||||
if pos:
|
||||
self.allow_print_before_pay = pos.allow_print_before_pay
|
||||
|
||||
if not for_validate:
|
||||
self.tax_category = pos.get("tax_category")
|
||||
|
||||
@ -432,8 +428,8 @@ class SalesInvoice(SellingController):
|
||||
if 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',
|
||||
'company', 'select_print_heading', 'cash_bank_account', 'write_off_account', 'taxes_and_charges',
|
||||
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))
|
||||
@ -1123,7 +1119,8 @@ class SalesInvoice(SellingController):
|
||||
"loyalty_program": lp_details.loyalty_program,
|
||||
"loyalty_program_tier": lp_details.tier_name,
|
||||
"customer": self.customer,
|
||||
"sales_invoice": self.name,
|
||||
"invoice_type": self.doctype,
|
||||
"invoice": self.name,
|
||||
"loyalty_points": points_earned,
|
||||
"purchase_amount": eligible_amount,
|
||||
"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
|
||||
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)
|
||||
|
||||
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)
|
||||
if against_lp_entry:
|
||||
invoice_list = ", ".join([d.sales_invoice for d in against_lp_entry])
|
||||
frappe.throw(_('''Sales Invoice can't be cancelled since the Loyalty Points earned has been redeemed.
|
||||
First cancel the Sales Invoice No {0}''').format(invoice_list))
|
||||
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
|
||||
frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed.
|
||||
First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
|
||||
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
|
||||
self.set_loyalty_program_tier()
|
||||
|
||||
@ -1172,7 +1169,9 @@ class SalesInvoice(SellingController):
|
||||
|
||||
points_to_redeem = self.loyalty_points
|
||||
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
|
||||
available_points = lp_entry.loyalty_points - flt(redemption_details.get(lp_entry.name))
|
||||
if available_points > points_to_redeem:
|
||||
@ -1185,7 +1184,8 @@ class SalesInvoice(SellingController):
|
||||
"loyalty_program": self.loyalty_program,
|
||||
"loyalty_program_tier": lp_entry.loyalty_program_tier,
|
||||
"customer": self.customer,
|
||||
"sales_invoice": self.name,
|
||||
"invoice_type": self.doctype,
|
||||
"invoice": self.name,
|
||||
"redeem_against": lp_entry.name,
|
||||
"loyalty_points": -1*redeemed_points,
|
||||
"purchase_amount": self.grand_total,
|
||||
@ -1576,13 +1576,13 @@ def get_loyalty_programs(customer):
|
||||
from erpnext.selling.doctype.customer.customer import get_loyalty_programs
|
||||
|
||||
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)
|
||||
|
||||
if len(lp_details) == 1:
|
||||
frappe.db.set(customer, 'loyalty_program', lp_details[0])
|
||||
return []
|
||||
return lp_details
|
||||
else:
|
||||
return lp_details
|
||||
|
||||
@ -1602,3 +1602,71 @@ def create_invoice_discounting(source_name, target_doc=None):
|
||||
})
|
||||
|
||||
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
|
||||
|
@ -18,7 +18,7 @@ def get_data():
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Payment'),
|
||||
'items': ['Payment Entry', 'Payment Request', 'Journal Entry', 'Invoice Discounting']
|
||||
'items': ['Payment Entry', 'Payment Request', 'Journal Entry', 'Invoice Discounting', 'Dunning']
|
||||
},
|
||||
{
|
||||
'label': _('Reference'),
|
||||
|
@ -706,37 +706,15 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
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):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
|
||||
pos_profile.payments = []
|
||||
pos_profile.append('payments', {
|
||||
'default': 1,
|
||||
'mode_of_payment': 'Cash',
|
||||
'amount': 0.0
|
||||
'mode_of_payment': 'Cash'
|
||||
})
|
||||
|
||||
pos_profile.save()
|
||||
@ -751,18 +729,12 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
pos_return = create_sales_invoice(is_return=1,
|
||||
return_against=pos.name, qty=-5, do_not_save=True)
|
||||
pos_return = make_sales_return(pos.name)
|
||||
|
||||
pos_return.is_pos = 1
|
||||
pos_return.pos_profile = pos_profile.name
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
self.assertEqual(pos_return.get('payments')[0].amount, -500)
|
||||
pos_profile.payments = []
|
||||
pos_profile.save()
|
||||
|
||||
self.assertEqual(pos_return.get('payments')[0].amount, -1000)
|
||||
|
||||
def test_pos_change_amount(self):
|
||||
make_pos_profile()
|
||||
@ -788,82 +760,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(pos.grand_total, 100.0)
|
||||
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):
|
||||
# check stock ledger entries
|
||||
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
|
||||
|
@ -1,314 +1,91 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"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",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2016-05-08 23:49:38.842621",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"default",
|
||||
"mode_of_payment",
|
||||
"amount",
|
||||
"column_break_3",
|
||||
"account",
|
||||
"type",
|
||||
"base_amount",
|
||||
"clearance_date"
|
||||
],
|
||||
"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
|
||||
},
|
||||
"fieldname": "mode_of_payment",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Mode of Payment",
|
||||
"options": "Mode of Payment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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": "Mode of Payment",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Mode of Payment",
|
||||
"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
|
||||
},
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"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": "Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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
|
||||
},
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"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
|
||||
},
|
||||
"fetch_from": "mode_of_payment.type",
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Type"
|
||||
},
|
||||
{
|
||||
"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_if_empty": 0,
|
||||
"fieldname": "type",
|
||||
"fieldtype": "Read Only",
|
||||
"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": "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
|
||||
},
|
||||
"fieldname": "base_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Base Amount (Company Currency)",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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)",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"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
|
||||
},
|
||||
"fieldname": "clearance_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Clearance Date",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"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
|
||||
"default": "0",
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Default",
|
||||
"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,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-19 14:54:56.524556",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 12:45:39.986598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -2,6 +2,16 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Subscription', {
|
||||
setup: function(frm) {
|
||||
frm.set_query('party_type', function() {
|
||||
return {
|
||||
filters : {
|
||||
name: ['in', ['Customer', 'Supplier']]
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if(!frm.is_new()){
|
||||
if(frm.doc.status !== 'Cancelled'){
|
||||
|
@ -6,14 +6,18 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer",
|
||||
"cb_1",
|
||||
"party_type",
|
||||
"status",
|
||||
"cb_1",
|
||||
"party",
|
||||
"subscription_period",
|
||||
"start",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"cancelation_date",
|
||||
"trial_period_start",
|
||||
"trial_period_end",
|
||||
"follow_calendar_months",
|
||||
"generate_new_invoices_past_due_date",
|
||||
"column_break_11",
|
||||
"current_invoice_start",
|
||||
"current_invoice_end",
|
||||
@ -23,7 +27,8 @@
|
||||
"sb_4",
|
||||
"plans",
|
||||
"sb_1",
|
||||
"tax_template",
|
||||
"sales_tax_template",
|
||||
"purchase_tax_template",
|
||||
"sb_2",
|
||||
"apply_additional_discount",
|
||||
"cb_2",
|
||||
@ -32,18 +37,10 @@
|
||||
"sb_3",
|
||||
"invoices",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Customer",
|
||||
"options": "Customer",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cb_1",
|
||||
@ -53,7 +50,7 @@
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid",
|
||||
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -61,12 +58,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription Period"
|
||||
},
|
||||
{
|
||||
"fieldname": "start",
|
||||
"fieldtype": "Date",
|
||||
"label": "Subscription Start Date",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cancelation_date",
|
||||
"fieldtype": "Date",
|
||||
@ -137,16 +128,11 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
|
||||
"fieldname": "sb_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Taxes"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sales Taxes and Charges Template",
|
||||
"options": "Sales Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb_2",
|
||||
"fieldtype": "Section Break",
|
||||
@ -195,10 +181,74 @@
|
||||
{
|
||||
"fieldname": "dimension_col_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": [],
|
||||
"modified": "2020-01-27 14:37:32.845173",
|
||||
"modified": "2020-06-25 10:52:52.265105",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
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.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):
|
||||
def before_insert(self):
|
||||
# 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):
|
||||
"""
|
||||
@ -35,7 +35,9 @@ class Subscription(Document):
|
||||
If the `date` parameter is not given , it will be automatically set as today's
|
||||
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
|
||||
elif date:
|
||||
self.current_invoice_start = date
|
||||
@ -53,15 +55,45 @@ class Subscription(Document):
|
||||
current billing period where `x` is the billing interval from the
|
||||
`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
|
||||
else:
|
||||
billing_cycle_info = self.get_billing_cycle_data()
|
||||
if billing_cycle_info:
|
||||
self.current_invoice_end = add_to_date(self.current_invoice_start, **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)
|
||||
else:
|
||||
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
|
||||
def validate_plans_billing_cycle(billing_cycle_data):
|
||||
"""
|
||||
@ -132,21 +164,22 @@ class Subscription(Document):
|
||||
"""
|
||||
if self.is_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')
|
||||
self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
|
||||
elif self.status == 'Past Due Date' and not self.has_outstanding_invoice():
|
||||
self.status = 'Active'
|
||||
elif self.current_invoice_is_past_due():
|
||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||
self.status = 'Past Due Date'
|
||||
elif not self.has_outstanding_invoice():
|
||||
self.status = 'Active'
|
||||
elif self.is_new_subscription():
|
||||
self.status = 'Active'
|
||||
# todo: then generate new invoice
|
||||
self.save()
|
||||
|
||||
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()
|
||||
|
||||
@ -160,7 +193,7 @@ class Subscription(Document):
|
||||
return True
|
||||
|
||||
end_date = getdate(end_date)
|
||||
return getdate(nowdate()) > getdate(end_date)
|
||||
return getdate() > getdate(end_date)
|
||||
|
||||
def is_past_grace_period(self):
|
||||
"""
|
||||
@ -171,7 +204,7 @@ class Subscription(Document):
|
||||
subscription_settings = frappe.get_single('Subscription Settings')
|
||||
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):
|
||||
"""
|
||||
@ -180,22 +213,24 @@ class Subscription(Document):
|
||||
if not current_invoice:
|
||||
current_invoice = self.get_current_invoice()
|
||||
|
||||
if not current_invoice:
|
||||
if not current_invoice or self.is_paid(current_invoice):
|
||||
return False
|
||||
else:
|
||||
return getdate(nowdate()) > getdate(current_invoice.due_date)
|
||||
return getdate() > getdate(current_invoice.due_date)
|
||||
|
||||
def get_current_invoice(self):
|
||||
"""
|
||||
Returns the most recent generated invoice.
|
||||
"""
|
||||
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||
|
||||
if len(self.invoices):
|
||||
current = self.invoices[-1]
|
||||
if frappe.db.exists('Sales Invoice', current.invoice):
|
||||
doc = frappe.get_doc('Sales Invoice', current.invoice)
|
||||
if frappe.db.exists(doctype, current.get('invoice')):
|
||||
doc = frappe.get_doc(doctype, current.get('invoice'))
|
||||
return doc
|
||||
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):
|
||||
"""
|
||||
@ -206,6 +241,8 @@ class Subscription(Document):
|
||||
def validate(self):
|
||||
self.validate_trial_period()
|
||||
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):
|
||||
"""
|
||||
@ -215,34 +252,72 @@ class Subscription(Document):
|
||||
if getdate(self.trial_period_end) < getdate(self.trial_period_start):
|
||||
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'))
|
||||
|
||||
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):
|
||||
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
|
||||
self.set_subscription_status()
|
||||
|
||||
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`.
|
||||
"""
|
||||
|
||||
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||
|
||||
invoice = self.create_invoice(prorate)
|
||||
self.append('invoices', {'invoice': invoice.name})
|
||||
self.append('invoices', {
|
||||
'document_type': doctype,
|
||||
'invoice': invoice.name
|
||||
})
|
||||
|
||||
self.save()
|
||||
|
||||
return invoice
|
||||
|
||||
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')
|
||||
invoice.set_posting_time = 1
|
||||
invoice.posting_date = self.current_invoice_start
|
||||
invoice.customer = self.customer
|
||||
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||
|
||||
## 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()
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
@ -255,18 +330,25 @@ class Subscription(Document):
|
||||
# for that reason
|
||||
items_list = self.get_items_from_plans(self.plans, prorate)
|
||||
for item in items_list:
|
||||
invoice.append('items', item)
|
||||
invoice.append('items', item)
|
||||
|
||||
# Taxes
|
||||
if self.tax_template:
|
||||
invoice.taxes_and_charges = self.tax_template
|
||||
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()
|
||||
|
||||
# Due date
|
||||
invoice.append(
|
||||
'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
|
||||
}
|
||||
)
|
||||
@ -300,13 +382,42 @@ class Subscription(Document):
|
||||
prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start)
|
||||
|
||||
items = []
|
||||
customer = self.customer
|
||||
party = self.party
|
||||
for plan in plans:
|
||||
item_code = frappe.db.get_value("Subscription Plan", plan.plan, "item")
|
||||
if not prorate:
|
||||
items.append({'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, customer)})
|
||||
plan_doc = frappe.get_doc('Subscription Plan', plan.plan)
|
||||
|
||||
item_code = plan_doc.item
|
||||
|
||||
if self.party == 'Customer':
|
||||
deferred_field = 'enable_deferred_revenue'
|
||||
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
|
||||
|
||||
@ -322,12 +433,13 @@ class Subscription(Document):
|
||||
elif self.status in ['Past Due Date', 'Unpaid']:
|
||||
self.process_for_past_due_date()
|
||||
|
||||
self.set_subscription_status()
|
||||
|
||||
self.save()
|
||||
|
||||
def is_postpaid_to_invoice(self):
|
||||
return getdate(nowdate()) > 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 \
|
||||
not self.has_outstanding_invoice()
|
||||
return getdate() > getdate(self.current_invoice_end) or \
|
||||
(getdate() >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start))
|
||||
|
||||
def is_prepaid_to_invoice(self):
|
||||
if not self.generate_invoice_at_period_start:
|
||||
@ -337,14 +449,12 @@ class Subscription(Document):
|
||||
return True
|
||||
|
||||
# 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):
|
||||
if self.is_new_subscription():
|
||||
return False
|
||||
def is_current_invoice_generated(self):
|
||||
invoice = self.get_current_invoice()
|
||||
|
||||
last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice)
|
||||
if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid':
|
||||
if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -358,21 +468,23 @@ class Subscription(Document):
|
||||
2. Change the `Subscription` status to 'Past Due Date'
|
||||
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()):
|
||||
self.generate_invoice()
|
||||
if self.current_invoice_is_past_due():
|
||||
self.status = 'Past Due Date'
|
||||
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end):
|
||||
self.status = 'Past Due Date'
|
||||
if 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)
|
||||
|
||||
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()
|
||||
|
||||
def cancel_subscription_at_period_end(self):
|
||||
"""
|
||||
Called when `Subscription.cancel_at_period_end` is truthy
|
||||
"""
|
||||
if self.end_date and getdate() < getdate(self.end_date):
|
||||
return
|
||||
|
||||
self.status = 'Cancelled'
|
||||
if not self.cancelation_date:
|
||||
self.cancelation_date = nowdate()
|
||||
@ -390,14 +502,22 @@ class Subscription(Document):
|
||||
if not current_invoice:
|
||||
frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice))
|
||||
else:
|
||||
if self.is_not_outstanding(current_invoice):
|
||||
if not self.has_outstanding_invoice():
|
||||
self.status = 'Active'
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
else:
|
||||
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
|
||||
def is_not_outstanding(invoice):
|
||||
def is_paid(invoice):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
|
||||
current_invoice = self.get_current_invoice()
|
||||
if not current_invoice:
|
||||
return False
|
||||
invoice_list = [d.invoice for d in self.invoices]
|
||||
|
||||
outstanding_invoices = frappe.get_all(doctype, fields=['name'],
|
||||
filters={'status': ('!=', 'Paid'), 'name': ('in', invoice_list)})
|
||||
|
||||
if outstanding_invoices:
|
||||
return True
|
||||
else:
|
||||
return not self.is_not_outstanding(current_invoice)
|
||||
False
|
||||
|
||||
def cancel_subscription(self):
|
||||
"""
|
||||
@ -419,7 +545,7 @@ class Subscription(Document):
|
||||
but it will not affect already created invoices.
|
||||
"""
|
||||
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')
|
||||
self.status = 'Cancelled'
|
||||
self.cancelation_date = nowdate()
|
||||
@ -435,7 +561,7 @@ class Subscription(Document):
|
||||
"""
|
||||
if self.status == 'Cancelled':
|
||||
self.status = 'Active'
|
||||
self.db_set('start', nowdate())
|
||||
self.db_set('start_date', nowdate())
|
||||
self.update_subscription_period(nowdate())
|
||||
self.invoices = []
|
||||
self.save()
|
||||
@ -447,6 +573,14 @@ class Subscription(Document):
|
||||
if invoice:
|
||||
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):
|
||||
diff = flt(date_diff(nowdate(), period_start) + 1)
|
||||
@ -469,10 +603,7 @@ def get_all_subscriptions():
|
||||
"""
|
||||
Returns all `Subscription` documents
|
||||
"""
|
||||
return frappe.db.sql(
|
||||
'select name from `tabSubscription` where status != "Cancelled"',
|
||||
as_dict=1
|
||||
)
|
||||
return frappe.db.get_all('Subscription', {'status': ('!=','Cancelled')})
|
||||
|
||||
|
||||
def process(data):
|
||||
|
@ -4,6 +4,8 @@ frappe.listview_settings['Subscription'] = {
|
||||
return [__("Trialling"), "green"];
|
||||
} else if(doc.status === 'Active') {
|
||||
return [__("Active"), "green"];
|
||||
} else if(doc.status === 'Completed') {
|
||||
return [__("Completed"), "green"];
|
||||
} else if(doc.status === 'Past Due Date') {
|
||||
return [__("Past Due Date"), "orange"];
|
||||
} else if(doc.status === 'Unpaid') {
|
||||
|
@ -7,15 +7,15 @@ import unittest
|
||||
|
||||
import frappe
|
||||
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,
|
||||
get_first_day, get_last_day)
|
||||
|
||||
def create_plan():
|
||||
if not frappe.db.exists('Subscription Plan', '_Test Plan Name'):
|
||||
plan = frappe.new_doc('Subscription Plan')
|
||||
plan.plan_name = '_Test Plan Name'
|
||||
plan.item = '_Test Non Stock Item'
|
||||
plan.price_determination = "Fixed rate"
|
||||
plan.price_determination = "Fixed Rate"
|
||||
plan.cost = 900
|
||||
plan.billing_interval = 'Month'
|
||||
plan.billing_interval_count = 1
|
||||
@ -25,7 +25,7 @@ def create_plan():
|
||||
plan = frappe.new_doc('Subscription Plan')
|
||||
plan.plan_name = '_Test Plan Name 2'
|
||||
plan.item = '_Test Non Stock Item'
|
||||
plan.price_determination = "Fixed rate"
|
||||
plan.price_determination = "Fixed Rate"
|
||||
plan.cost = 1999
|
||||
plan.billing_interval = 'Month'
|
||||
plan.billing_interval_count = 1
|
||||
@ -35,12 +35,29 @@ def create_plan():
|
||||
plan = frappe.new_doc('Subscription Plan')
|
||||
plan.plan_name = '_Test Plan Name 3'
|
||||
plan.item = '_Test Non Stock Item'
|
||||
plan.price_determination = "Fixed rate"
|
||||
plan.price_determination = "Fixed Rate"
|
||||
plan.cost = 1999
|
||||
plan.billing_interval = 'Day'
|
||||
plan.billing_interval_count = 14
|
||||
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):
|
||||
|
||||
def setUp(self):
|
||||
@ -48,16 +65,17 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_create_subscription_with_trial_with_correct_period(self):
|
||||
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_end = add_days(nowdate(), 30)
|
||||
subscription.trial_period_end = add_months(nowdate(), 1)
|
||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||
subscription.save()
|
||||
|
||||
self.assertEqual(subscription.trial_period_start, nowdate())
|
||||
self.assertEqual(subscription.trial_period_end, add_days(nowdate(), 30))
|
||||
self.assertEqual(subscription.trial_period_start, subscription.current_invoice_start)
|
||||
self.assertEqual(subscription.trial_period_end, subscription.current_invoice_end)
|
||||
self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
|
||||
self.assertEqual(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start))
|
||||
self.assertEqual(add_to_date(subscription.current_invoice_start, months=1, days=-1), get_date_str(subscription.current_invoice_end))
|
||||
self.assertEqual(subscription.invoices, [])
|
||||
self.assertEqual(subscription.status, 'Trialling')
|
||||
|
||||
@ -65,7 +83,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_create_subscription_without_trial_with_correct_period(self):
|
||||
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.save()
|
||||
|
||||
@ -81,7 +100,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_create_subscription_trial_with_wrong_dates(self):
|
||||
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_start = add_days(nowdate(), 30)
|
||||
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):
|
||||
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_start = add_days(nowdate(), 30)
|
||||
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):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
subscription.customer = '_Test Customer'
|
||||
subscription.start = '2018-01-01'
|
||||
subscription.party_type = 'Customer'
|
||||
subscription.party = '_Test Customer'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||
subscription.insert()
|
||||
|
||||
@ -114,18 +136,22 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
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()
|
||||
|
||||
def test_status_goes_back_to_active_after_invoice_is_paid(self):
|
||||
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.start = '2018-01-01'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.insert()
|
||||
subscription.process() # generate first invoice
|
||||
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()
|
||||
current_invoice = subscription.get_current_invoice()
|
||||
@ -137,7 +163,7 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.process()
|
||||
|
||||
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)
|
||||
|
||||
subscription.delete()
|
||||
@ -149,16 +175,17 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.start = '2018-01-01'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.insert()
|
||||
|
||||
self.assertEqual(subscription.status, 'Active')
|
||||
|
||||
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
|
||||
# And is backdated subscription so subscription will be cancelled after processing
|
||||
self.assertEqual(subscription.status, 'Cancelled')
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
@ -172,16 +199,14 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.start = '2018-01-01'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.insert()
|
||||
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
|
||||
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||
self.assertEqual(subscription.status, 'Unpaid')
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
@ -190,10 +215,11 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_subscription_invoice_days_until_due(self):
|
||||
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.days_until_due = 10
|
||||
subscription.start = add_months(nowdate(), -1)
|
||||
subscription.start_date = add_months(nowdate(), -1)
|
||||
subscription.insert()
|
||||
subscription.process() # generate first invoice
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
@ -208,9 +234,10 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.start = '2018-01-01'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.insert()
|
||||
subscription.process() # generate first invoice
|
||||
|
||||
@ -232,7 +259,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_subscription_remains_active_during_invoice_period(self):
|
||||
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.save()
|
||||
subscription.process() # no changes expected
|
||||
@ -258,7 +286,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_subscription_cancelation(self):
|
||||
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.save()
|
||||
subscription.cancel_subscription()
|
||||
@ -274,7 +303,8 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.save()
|
||||
|
||||
@ -309,7 +339,8 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.save()
|
||||
subscription.cancel_subscription()
|
||||
@ -329,7 +360,8 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.save()
|
||||
subscription.cancel_subscription()
|
||||
@ -353,16 +385,14 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.start = '2018-01-01'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.insert()
|
||||
subscription.process() # generate first invoice
|
||||
invoices = len(subscription.invoices)
|
||||
|
||||
self.assertEqual(subscription.status, 'Past Due Date')
|
||||
self.assertEqual(len(subscription.invoices), invoices)
|
||||
|
||||
subscription.cancel_subscription()
|
||||
self.assertEqual(subscription.status, 'Cancelled')
|
||||
self.assertEqual(len(subscription.invoices), invoices)
|
||||
@ -387,15 +417,14 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.start = '2018-01-01'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.insert()
|
||||
subscription.process() # generate first invoice
|
||||
|
||||
self.assertEqual(subscription.status, 'Past Due Date')
|
||||
|
||||
subscription.process()
|
||||
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||
self.assertEqual(subscription.status, 'Unpaid')
|
||||
|
||||
subscription.cancel_subscription()
|
||||
@ -424,16 +453,14 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.start = '2018-01-01'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.insert()
|
||||
|
||||
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 Unpaid since grace period is 0
|
||||
self.assertEqual(subscription.status, 'Unpaid')
|
||||
|
||||
invoice = subscription.get_current_invoice()
|
||||
@ -445,7 +472,7 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
# A new invoice is generated
|
||||
subscription.process()
|
||||
self.assertEqual(subscription.status, 'Past Due Date')
|
||||
self.assertEqual(subscription.status, 'Unpaid')
|
||||
|
||||
settings.cancel_after_grace = default_grace_period_action
|
||||
settings.save()
|
||||
@ -453,7 +480,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_restart_active_subscription(self):
|
||||
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.save()
|
||||
|
||||
@ -463,7 +491,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_subscription_invoice_discount_percentage(self):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
subscription.customer = '_Test Customer'
|
||||
subscription.party_type = 'Customer'
|
||||
subscription.party = '_Test Customer'
|
||||
subscription.additional_discount_percentage = 10
|
||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||
subscription.save()
|
||||
@ -478,7 +507,8 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
def test_subscription_invoice_discount_amount(self):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
subscription.customer = '_Test Customer'
|
||||
subscription.party_type = 'Customer'
|
||||
subscription.party = '_Test Customer'
|
||||
subscription.additional_discount_amount = 11
|
||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||
subscription.save()
|
||||
@ -495,7 +525,8 @@ class TestSubscription(unittest.TestCase):
|
||||
# Create a non pre-billed subscription, processing should not create
|
||||
# invoices.
|
||||
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.save()
|
||||
subscription.process()
|
||||
@ -517,10 +548,12 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||
subscription.save()
|
||||
subscription.process()
|
||||
subscription.cancel_subscription()
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
@ -538,3 +571,65 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
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,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-02-26 04:21:41.265055",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2018-02-26 04:21:41.265055",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
"invoice"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "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": "Invoice",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Document Type ",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice",
|
||||
"options": "document_type",
|
||||
"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,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-02-26 10:48:07.033422",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Invoice",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-01 22:23:54.462718",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Invoice",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:plan_name",
|
||||
"creation": "2018-02-24 11:31:23.066506",
|
||||
@ -24,6 +25,7 @@
|
||||
"column_break_16",
|
||||
"payment_gateway",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
@ -60,8 +62,8 @@
|
||||
{
|
||||
"fieldname": "price_determination",
|
||||
"fieldtype": "Select",
|
||||
"label": "Price Determination",
|
||||
"options": "\nFixed rate\nBased on price list",
|
||||
"label": "Subscription Price Based On",
|
||||
"options": "\nFixed Rate\nBased On Price List\nMonthly Rate",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -69,7 +71,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.price_determination==\"Fixed rate\"",
|
||||
"depends_on": "eval:['Fixed Rate', 'Monthly Rate'].includes(doc.price_determination)",
|
||||
"fieldname": "cost",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
@ -136,9 +138,16 @@
|
||||
{
|
||||
"fieldname": "dimension_col_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",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Plan",
|
||||
@ -155,6 +164,30 @@
|
||||
"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": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
@ -5,6 +5,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_first_day, get_last_day, date_diff, flt, getdate
|
||||
from frappe.model.document import Document
|
||||
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.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)
|
||||
if plan.price_determination == "Fixed rate":
|
||||
return plan.cost
|
||||
if plan.price_determination == "Fixed Rate":
|
||||
return plan.cost * prorate_factor
|
||||
|
||||
elif plan.price_determination == "Based on price list":
|
||||
elif plan.price_determination == "Based On Price List":
|
||||
if customer:
|
||||
customer_group = frappe.db.get_value("Customer", customer, "customer_group")
|
||||
else:
|
||||
@ -32,4 +33,25 @@ def get_plan_rate(plan, quantity=1, customer=None):
|
||||
if not price:
|
||||
return 0
|
||||
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,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-02-25 07:35:07.736146",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2018-02-25 07:35:07.736146",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"plan",
|
||||
"qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Int",
|
||||
"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": "Quantity",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "plan",
|
||||
"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": "Plan",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Subscription Plan",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldname": "plan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Plan",
|
||||
"options": "Subscription Plan",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-06-20 15:35:13.514699",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Plan Detail",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-14 17:44:05.275100",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Plan Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -1,179 +1,76 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-02-26 06:13:37.910139",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2018-02-26 06:13:37.910139",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"grace_period",
|
||||
"cancel_after_grace",
|
||||
"prorate"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid",
|
||||
"fieldname": "grace_period",
|
||||
"fieldtype": "Int",
|
||||
"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": "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
|
||||
},
|
||||
"default": "1",
|
||||
"description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid",
|
||||
"fieldname": "grace_period",
|
||||
"fieldtype": "Int",
|
||||
"label": "Grace Period"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "cancel_after_grace",
|
||||
"fieldtype": "Check",
|
||||
"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": "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
|
||||
},
|
||||
"default": "0",
|
||||
"fieldname": "cancel_after_grace",
|
||||
"fieldtype": "Check",
|
||||
"label": "Cancel Subscription After Grace Period"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "prorate",
|
||||
"fieldtype": "Check",
|
||||
"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": "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
|
||||
"default": "1",
|
||||
"fieldname": "prorate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Prorate"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-02-26 13:58:09.455832",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Settings",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-23 09:13:44.292792",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "Administrator",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -158,8 +158,10 @@ def validate_account_for_perpetual_inventory(gl_map):
|
||||
if account not in aii_accounts:
|
||||
continue
|
||||
|
||||
# Always use current date to get stock and account balance as there can future entries for
|
||||
# other items
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
|
||||
gl_map[0].posting_date, gl_map[0].company)
|
||||
getdate(), gl_map[0].company)
|
||||
|
||||
if gl_map[0].voucher_type=="Journal Entry":
|
||||
# In case of Journal Entry, there are no corresponding SL entries,
|
||||
@ -169,7 +171,6 @@ def validate_account_for_perpetual_inventory(gl_map):
|
||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||
.format(account), StockAccountInvalidTransaction)
|
||||
|
||||
# This has been comment for a temporary, will add this code again on release of immutable ledger
|
||||
elif account_bal != stock_bal:
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
|
||||
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user